diff --git a/addons/cetmix_tower_git/README.rst b/addons/cetmix_tower_git/README.rst new file mode 100644 index 0000000..176d47b --- /dev/null +++ b/addons/cetmix_tower_git/README.rst @@ -0,0 +1,91 @@ +================ +Cetmix Tower Git +================ + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:2a3dd557b4ae104188b8e2912c02b135bcfe7cc6518f8633daa149ccb94850fe + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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-cetmix%2Fcetmix--tower-lightgray.png?logo=github + :target: https://github.com/cetmix/cetmix-tower/tree/18.0/cetmix_tower_git + :alt: cetmix/cetmix-tower + +|badge1| |badge2| |badge3| + +This module implements Git Management functionality for `Cetmix +Tower `__. + +Please refer to the `official +documentation `__ for detailed information. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +Please refer to the `official +documentation `__ for detailed configuration +instructions. + +Usage +===== + +Please refer to the `official +documentation `__ for detailed usage +instructions. + +Changelog +========= + +18.0.1.0.2 (2026-03-10) +----------------------- + +- Features: Provide git project name using the ``__git_project__`` + custom value when creating a project in flight plan. Improve the UI + and UX of Git Projects. (5197) + +- Bugfixes: Link server to git project only once. (5214) + +18.0.1.0.1 (2025-12-17) +----------------------- + +- Features: Improve search views, implement the search panel for + selected views. (5139) + +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 +------- + +* Cetmix + +Maintainers +----------- + +This module is part of the `cetmix/cetmix-tower `_ project on GitHub. + +You are welcome to contribute. diff --git a/addons/cetmix_tower_git/__init__.py b/addons/cetmix_tower_git/__init__.py new file mode 100644 index 0000000..0650744 --- /dev/null +++ b/addons/cetmix_tower_git/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/addons/cetmix_tower_git/__manifest__.py b/addons/cetmix_tower_git/__manifest__.py new file mode 100644 index 0000000..b7050ef --- /dev/null +++ b/addons/cetmix_tower_git/__manifest__.py @@ -0,0 +1,40 @@ +# Copyright Cetmix OU +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +{ + "name": "Cetmix Tower Git", + "summary": "Cetmix Tower Git Management Tools", + "version": "18.0.1.0.2", + "development_status": "Beta", + "category": "Productivity", + "website": "https://tower.cetmix.com", + "author": "Cetmix", + "license": "AGPL-3", + "application": False, + "depends": ["cetmix_tower_yaml"], + "external_dependencies": { + "python": ["giturlparse==0.12.0"], + }, + "data": [ + "security/ir.model.access.csv", + "security/cx_tower_git_project_security.xml", + "security/cx_tower_git_source_security.xml", + "security/cx_tower_git_remote_security.xml", + "security/cx_tower_git_repo_security.xml", + "security/cx_tower_git_repo_owner_security.xml", + "security/cx_tower_git_project_rel_security.xml", + "security/cx_tower_git_project_file_template_rel_security.xml", + "views/cx_tower_git_project_views.xml", + "views/cx_tower_git_source_views.xml", + "views/cx_tower_git_remote_views.xml", + "views/cx_tower_git_repo_views.xml", + "views/cx_tower_git_repo_owner_views.xml", + "views/cx_tower_file_views.xml", + "views/cx_tower_file_template_views.xml", + "views/cx_tower_server_view.xml", + "views/cx_tower_plan_line_view.xml", + "views/menuitems.xml", + ], + "demo": [ + "demo/demo_data.xml", + ], +} diff --git a/addons/cetmix_tower_git/demo/demo_data.xml b/addons/cetmix_tower_git/demo/demo_data.xml new file mode 100644 index 0000000..de7a7be --- /dev/null +++ b/addons/cetmix_tower_git/demo/demo_data.xml @@ -0,0 +1,164 @@ + + + + + Demo Git Project + demo_git_project + This is a demo git project. + + + + https://github.com/cetmix-demo/cetmix-tower-demo.git + + + https://github.com/oca-demo/web-demo.git + + + https://github.com/odoo-demo/enterprise-demo.git + + + + https://gitlab.com/cetmix-demo/cetmix-tower-demo.git + + + + https://bitbucket.com/cetmix-demo/cetmix-tower-demo-enterprise.git + + + + + + Cetmix Tower + cetmix_tower + + + + + + + branch + 14.0 + + + + + pr + 176 + + + + OCA Web + oca_web + + + + + + + branch + 14.0 + + + + Odoo Enterprise (Private) + odoo_enterprise + + + + + + + branch + 19.0 + + + + Sample Semi Private Gitlab + gitlab_private + + + + + + + branch + main + + + + + pr + 1234 + + + + Sample Private Bitbucket + bitbucket_private + + + + + + + branch + dev + + + + + commit + 1234567890 + + + + + repos.yaml + + tower + text + {{ instance_name }}/config + + + + + + + + git_aggregator + + + + + Demo Git URL + demo_git_url + + + + + Parse Git URL + python_code + +if {{ demo_git_url }}: + parsed_url = giturlparse.parse({{ demo_git_url }}) + repo = parsed_url.repo + owner = parsed_url.owner + host = parsed_url.host + platform = parsed_url.platform + message = "Repo: " + repo + ", Owner: " + owner + ", Host: " + host + ", Platform: " + platform + result={"exit_code": 0, "message": message} +else: + result={"exit_code": -100, "message": "Git URL is not defined!"} + + 1 + + Run Python Code: Check Branch + + diff --git a/addons/cetmix_tower_git/i18n/cetmix_tower_git.pot b/addons/cetmix_tower_git/i18n/cetmix_tower_git.pot new file mode 100644 index 0000000..d9e8d12 --- /dev/null +++ b/addons/cetmix_tower_git/i18n/cetmix_tower_git.pot @@ -0,0 +1,1041 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * cetmix_tower_git +# +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: cetmix_tower_git +#. odoo-python +#: code:addons/cetmix_tower_git/models/cx_tower_git_project.py:0 +msgid "" +"\n" +"# You need to set the following variables in your environment:\n" +"# %(vars)s\n" +"# and run git-aggregator with '--expand-env' parameter.\n" +msgstr "" + +#. module: cetmix_tower_git +#. odoo-python +#: code:addons/cetmix_tower_git/models/cx_tower_git_project.py:0 +msgid "" +"# This file is generated with Cetmix Tower https://cetmix.com/tower\n" +"# It's designed to be used with git-aggregator tool developed by Acsone.\n" +"# Documentation for git-aggregator: https://github.com/acsone/git-aggregator\n" +msgstr "" + +#. module: cetmix_tower_git +#. odoo-python +#: code:addons/cetmix_tower_git/models/cx_tower_git_project.py:0 +msgid "%(name)s (copy)" +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "* Sources where all remotes are private" +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "* Sources where some remotes are private" +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "..to be autogenerated" +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "" +"Managers. All users who have \"Manager\" group and are set as \"Managers\" in \n" +" all\n" +" related servers.\n" +" This is done to avoid unpredictable consequences when some of the servers are not updated due to access restrictions when a project is updated." +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "" +"Users. All users who have \"Manager\" group and are either set in \"Users\" or in \"Managers\" in \n" +" all\n" +" related servers." +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.constraint,message:cetmix_tower_git.constraint_cx_tower_git_repo_unique_repo_host_owner +msgid "A repository with the same name, host, and owner already exists." +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "Access" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__active +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__active +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_repo__active +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__active +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_search +msgid "Active" +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_search +msgid "Archived" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_rel__auto_sync +msgid "Auto Sync" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields.selection,name:cetmix_tower_git.selection__cx_tower_git_remote__head_type__branch +msgid "Branch" +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_remote_view_form +msgid "Branch/PR/commit number or link" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_project__reference +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_project_rel__reference +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_remote__reference +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_repo__reference +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_repo_owner__reference +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_source__reference +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_repo_owner_view_form +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_repo_view_form +msgid "" +"Can contain English letters, digits and '_'. Leave blank to autogenerate" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model,name:cetmix_tower_git.model_cx_tower_command +msgid "Cetmix Tower Command" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model,name:cetmix_tower_git.model_cx_tower_file +msgid "Cetmix Tower File" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model,name:cetmix_tower_git.model_cx_tower_file_template +msgid "Cetmix Tower File Template" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model,name:cetmix_tower_git.model_cx_tower_plan_line +msgid "Cetmix Tower Flight Plan Line" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model,name:cetmix_tower_git.model_cx_tower_git_project +msgid "Cetmix Tower Git Project" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model,name:cetmix_tower_git.model_cx_tower_git_project_file_template_rel +msgid "Cetmix Tower Git Project relation to File Templates" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model,name:cetmix_tower_git.model_cx_tower_git_project_rel +msgid "Cetmix Tower Git Project relation to Files and Servers" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model,name:cetmix_tower_git.model_cx_tower_git_remote +msgid "Cetmix Tower Git Remote" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model,name:cetmix_tower_git.model_cx_tower_git_repo +msgid "Cetmix Tower Git Repository" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model,name:cetmix_tower_git.model_cx_tower_git_repo_owner +msgid "Cetmix Tower Git Repository Owner" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model,name:cetmix_tower_git.model_cx_tower_git_source +msgid "Cetmix Tower Git Source" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model,name:cetmix_tower_git.model_cetmix_tower +msgid "Cetmix Tower Odoo Automation" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model,name:cetmix_tower_git.model_cx_tower_server +msgid "Cetmix Tower Server" +msgstr "" + +#. module: cetmix_tower_git +#. odoo-python +#: code:addons/cetmix_tower_git/models/cx_tower_git_project_file_template_rel.py:0 +#: code:addons/cetmix_tower_git/models/cx_tower_git_project_rel.py:0 +msgid "Code generator function for '%(project_format)s' format not found." +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields.selection,name:cetmix_tower_git.selection__cx_tower_git_remote__head_type__commit +msgid "Commit" +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_server_view_form +msgid "Configure" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_plan_line__is_make_copy +msgid "" +"Create a copy of the Git Project instead of linking the file to the existing" +" one." +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.actions.act_window,help:cetmix_tower_git.cx_tower_git_project_action +msgid "Create your first git project!" +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.actions.act_window,help:cetmix_tower_git.action_cx_tower_git_remote +msgid "Create your first git remote!" +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.actions.act_window,help:cetmix_tower_git.action_cx_tower_git_repo_owner +msgid "Create your first repository owner!" +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.actions.act_window,help:cetmix_tower_git.action_cx_tower_git_repo +msgid "Create your first repository!" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__create_uid +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__create_uid +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_repo__create_uid +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_repo_owner__create_uid +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__create_uid +msgid "Created by" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__create_date +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__create_date +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_repo__create_date +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_repo_owner__create_date +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__create_date +msgid "Created on" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_repo__secret_id +msgid "Custom secret used for this repository" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_repo_owner__secret_id +msgid "Custom secret used for this repository owner" +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_remote_view_form +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_source_view_form +msgid "Disabled" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__display_name +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_file_template_rel__display_name +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_rel__display_name +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__display_name +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_repo__display_name +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_repo_owner__display_name +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__display_name +msgid "Display Name" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_repo__url +msgid "Displayed in 'https' format, but can be entered in any format" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_remote__enabled +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_source__enabled +msgid "Enable in configuration and exported to files" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__enabled +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__enabled +msgid "Enabled" +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_tree +msgid "Export YAML" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_rel__file_id +msgid "File" +msgstr "" + +#. module: cetmix_tower_git +#. odoo-python +#: code:addons/cetmix_tower_git/models/cx_tower_git_project_rel.py:0 +msgid "File '%(file)s' doesn't belong to server '%(server)s'" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_file_template_rel__file_template_id +msgid "File Template" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__file_template_ids +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "File Templates" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.constraint,message:cetmix_tower_git.constraint_cx_tower_git_project_rel_project_server_file_format_uniq +msgid "File is already related to the same project and format" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.constraint,message:cetmix_tower_git.constraint_cx_tower_git_project_file_template_rel_project_server_file_template_format_uniq +msgid "File template is already related to the same project and format" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__file_ids +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "Files" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_file_template_rel__project_format +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_rel__project_format +msgid "Format" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields.selection,name:cetmix_tower_git.selection__cx_tower_git_remote__url_protocol__git +msgid "GIT" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_repo__url_git +msgid "GIT URL" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_repo__url_git +msgid "GIT URL of the repository" +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "General" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_repo__url +msgid "Generic URL" +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "Git Aggregator" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__git_aggregator_root_dir +msgid "Git Aggregator Root Dir" +msgstr "" + +#. module: cetmix_tower_git +#. odoo-python +#: code:addons/cetmix_tower_git/models/cx_tower_git_remote.py:0 +msgid "" +"Git Aggregator: Bitbucket does not support fetching PRs. Please use branch instead.\n" +"\n" +"Source: %(src)s\n" +"URL: %(url)s\n" +"Head: %(head)s" +msgstr "" + +#. module: cetmix_tower_git +#. odoo-python +#: code:addons/cetmix_tower_git/models/cx_tower_git_remote.py:0 +msgid "Git Aggregator: Head number is empty in %(head)s" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__git_project_id +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__git_project_id +msgid "Git Configuration" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_file__git_project_id +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_file_template__git_project_id +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_file_template_rel__git_project_id +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_rel__git_project_id +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_repo__git_project_ids +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_plan_line__git_project_id +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_server__git_project_ids +msgid "Git Project" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_repo__git_project_count +msgid "Git Project Count" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__git_project_file_template_rel_ids +msgid "Git Project File Template Relations" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_server__git_project_rel_ids +msgid "Git Project Rel" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_file__git_project_rel_ids +msgid "Git Project Relations" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__git_project_rel_ids +msgid "Git Project Server File Relations" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.actions.act_window,name:cetmix_tower_git.cx_tower_git_project_action +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_file_template__git_project_ids +#: model:ir.ui.menu,name:cetmix_tower_git.menu_cx_tower_git_project +#: model:ir.ui.menu,name:cetmix_tower_git.menu_cx_tower_git_project_settings +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_server_view_form +msgid "Git Projects" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.actions.act_window,name:cetmix_tower_git.action_cx_tower_git_remote +msgid "Git Remotes" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_project__git_aggregator_root_dir +msgid "" +"Git aggregator root directory where sources will be cloned. Eg '/tmp/git-" +"aggregator' Will use '.' if not set" +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "" +"Git aggregator root directory where sources will be cloned. Leave blank to " +"use '.'" +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.actions.act_window,help:cetmix_tower_git.cx_tower_git_project_action +msgid "" +"Git projects represent collections of git repositories with their metadata and configuration.\n" +" They are used in deployment processes to define the source code to be deployed." +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_remote__head +msgid "Git remote head. Link to branch, PR, commit or commit hash." +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.actions.act_window,help:cetmix_tower_git.action_cx_tower_git_remote +msgid "" +"Git remotes represent branches, pull requests, or commits from git " +"repositories." +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_repo_view_form +msgid "GitProjects" +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_repo_view_search +msgid "Group By" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields.selection,name:cetmix_tower_git.selection__cx_tower_git_remote__url_protocol__https +msgid "HTTPS" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__has_partially_private_remotes +msgid "Has Partially Private Remotes" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__has_private_remotes +msgid "Has Private Remotes" +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_search +msgid "Has Servers" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__head +msgid "Head" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__head_type +msgid "Head Type" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_repo__host +msgid "Host" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__id +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_file_template_rel__id +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_rel__id +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__id +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_repo__id +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_repo_owner__id +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__id +msgid "ID" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_project_rel__auto_sync +msgid "If enabled file will be synced automatically using cron" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_remote__repo_id +msgid "" +"If selected, the remote URL will be filled from the repo settings based on " +"the remote protocol" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_project__has_partially_private_remotes +msgid "Indicates if the project has any partially private remotes." +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_project__has_private_remotes +msgid "Indicates if the project has any private remotes." +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_repo__active +msgid "Indicates if the repository is active" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_repo__is_private +msgid "Indicates if the repository is private" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__write_uid +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__write_uid +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_repo__write_uid +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_repo_owner__write_uid +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__write_date +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__write_date +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_repo__write_date +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_repo_owner__write_date +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__write_date +msgid "Last Updated on" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_plan_line__is_make_copy +msgid "Make a Copy" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__manager_ids +msgid "Managers" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_project__manager_ids +msgid "Managers who can modify this record" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__name +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_file_template_rel__name +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_rel__name +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__name +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_repo__name +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_repo_owner__name +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__name +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "Name" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_repo_owner__name +msgid "Name of the repository owner (e.g., 'cetmix', 'OCA')" +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_source_view_form +msgid "Name. Leave blank to autogenerate" +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_search +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_repo_view_search +msgid "Name/Reference" +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_search +msgid "No Servers" +msgstr "" + +#. module: cetmix_tower_git +#. odoo-python +#: code:addons/cetmix_tower_git/models/cx_tower_git_repo.py:0 +msgid "Not a valid repository URL!" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__note +msgid "Note" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_repo__git_project_count +msgid "Number of projects this repository is used in" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_repo__remote_count +msgid "Number of remotes this repository is used in" +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "Open File Template" +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_server_view_form +msgid "Open Git Project" +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "Open Server" +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "Open file template" +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_repo_view_search +msgid "Org" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_repo__owner_id +msgid "Owner" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__is_private +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_repo__is_private +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_repo_view_search +msgid "Private" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__remote_count_private +msgid "Private Remotes" +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_repo_view_form +msgid "Projects" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_repo__git_project_ids +msgid "Projects this repository is used in" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__url_protocol +msgid "Protocol" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__repo_provider +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_repo__provider +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_repo_view_search +msgid "Provider" +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_repo_view_search +msgid "Provider: Other" +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_repo_view_search +msgid "Public" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields.selection,name:cetmix_tower_git.selection__cx_tower_git_remote__head_type__pr +msgid "Pull/Merge Request" +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "Put your notes here..." +msgstr "" + +#. module: cetmix_tower_git +#. odoo-python +#: code:addons/cetmix_tower_git/models/cx_tower_command.py:0 +msgid "" +"Python library for Git URL parsing. Available methods: 'parse', 'validate'." +" Documentation on GitHub." +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__reference +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_rel__reference +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__reference +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_repo__reference +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_repo_owner__reference +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__reference +msgid "Reference" +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_source_view_form +msgid "" +"Reference. Can contain English letters, digits and '_'. Leave blank to " +"autogenerate" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_repo__remote_ids +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__remote_ids +msgid "Remote" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_repo__remote_count +msgid "Remote Count" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__remote_count +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_repo_view_form +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_source_view_form +msgid "Remotes" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_repo__remote_ids +msgid "Remotes that use this repository" +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "Repos" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.actions.act_window,name:cetmix_tower_git.action_cx_tower_git_repo +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__repo_ids +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_repo_owner__repo_ids +#: model:ir.ui.menu,name:cetmix_tower_git.menu_cx_tower_git_repositories +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_repo_owner_view_form +msgid "Repositories" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_repo_owner__repo_ids +msgid "Repositories owned by this organization/user" +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.actions.act_window,help:cetmix_tower_git.action_cx_tower_git_repo +msgid "" +"Repositories represent git repositories with their metadata and configuration.\n" +" They can be linked to remotes to automatically populate URL information." +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_project__repo_ids +msgid "Repositories used in this project through its sources and remotes" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__repo_id +msgid "Repository" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_repo__repo +msgid "Repository Name" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.actions.act_window,name:cetmix_tower_git.action_cx_tower_git_repo_owner +#: model:ir.ui.menu,name:cetmix_tower_git.menu_cx_tower_git_repository_owners +msgid "Repository Owners" +msgstr "" + +#. module: cetmix_tower_git +#. odoo-python +#: code:addons/cetmix_tower_git/models/cx_tower_git_remote.py:0 +msgid "Repository URL is not set" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_repo__host +msgid "Repository host (e.g., 'github.com', 'gitlab.com')" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_remote__is_private +msgid "Repository is private" +msgstr "" + +#. module: cetmix_tower_git +#. odoo-python +#: code:addons/cetmix_tower_git/models/cx_tower_git_remote.py:0 +msgid "Repository is required" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_repo__repo +msgid "Repository name (e.g., 'cetmix-tower', 'odoo')" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_repo__owner_id +msgid "Repository owner (e.g., 'cetmix' or 'OCA')" +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.actions.act_window,help:cetmix_tower_git.action_cx_tower_git_repo_owner +msgid "" +"Repository owners represent organizations or users that own git repositories.\n" +" Examples include \"cetmix\", \"OCA\", etc." +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_remote__repo_provider +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_repo__provider +msgid "Repository provider to determine provider-based behaviour" +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "Root Directory" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields.selection,name:cetmix_tower_git.selection__cx_tower_git_remote__url_protocol__ssh +msgid "SSH" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_repo__url_ssh +msgid "SSH URL" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_repo__url_ssh +msgid "SSH URL of the repository" +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "Save record as YAML" +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_tree +msgid "Save records as YAML" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_repo__secret_id +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_repo_owner__secret_id +msgid "Secret" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_plan_line__git_project_id +msgid "Select a git project to be linked to the file and server." +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__sequence +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__sequence +msgid "Sequence" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_file__server_id +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__server_ids +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_rel__server_id +msgid "Server" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_project__server_ids +msgid "" +"Servers are added automatically based on the files linked to the project." +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__source_id +msgid "Source" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__source_ids +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "Sources" +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_source_view_form +msgid "" +"The top one remote will be used as a merge target.\n" +" You can re-arrange remotes by dragging them or changing their sequence value." +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__user_ids +msgid "Users" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_project__user_ids +msgid "Users who can view this record" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_rel__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_repo__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_repo_owner__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__yaml_code +msgid "Yaml Code" +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_plan_line_view_form +msgid "" +"You can also provide a Git Project reference using the __git_project__ variable in the flight plan custom values.
\n" +" Python command code example:\n" +" \n" +"custom_values['__git_project__'] = 'my_git_project'\n" +" \n" +"
\n" +" Important: if defined, this variable value overrides the Git Project selected in the form." +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "" +"You can edit these fields at your own risk. However keep in mind that they " +"will be automatically updated each time related servers are added, removed " +"or updated." +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_repo_owner_view_form +msgid "e.g., Cetmix, OCA" +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_repo_owner_view_form +msgid "e.g., cetmix, oca" +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_repo_view_form +msgid "e.g., cetmix-tower, odoo" +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_repo_view_form +msgid "https, ssh or git formats are accepted" +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "managers who can modify this record" +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_remote_view_form +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_remote_view_tree +msgid "select or enter a link" +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "users who can view this record" +msgstr "" diff --git a/addons/cetmix_tower_git/i18n/fi.po b/addons/cetmix_tower_git/i18n/fi.po new file mode 100644 index 0000000..4d98520 --- /dev/null +++ b/addons/cetmix_tower_git/i18n/fi.po @@ -0,0 +1,595 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * cetmix_tower_git +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server \n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: fi\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" + +#. module: cetmix_tower_git +#: code:addons/cetmix_tower_git/models/cx_tower_git_project.py:0 +#, python-format +msgid "" +"\n" +"# You need to set the following variables in your environment:\n" +"# %(vars)s \n" +"# and run git-aggregator with '--expand-env' parameter.\n" +msgstr "" + +#. module: cetmix_tower_git +#: code:addons/cetmix_tower_git/models/cx_tower_git_project.py:0 +#, python-format +msgid "" +"# This file is generated with Cetmix Tower https://cetmix.com/tower\n" +"# It's designed to be used with git-aggregator tool developed by Acsone.\n" +"# Documentation for git-aggregator: https://github.com/acsone/git-aggregator\n" +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "* Sources where all remotes are private" +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "* Sources where some remotes are private" +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "" +"Managers. All users who have \"Manager\" group and are set as \"Managers\" in all related servers.\n" +" This is done to avoid unpredictable consequences when some of the servers are not updated due to access restrictions when a project is updated." +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "" +"Users. All users who have \"Manager\" group and are either set in " +"\"Users\" or in \"Managers\" in all related servers." +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "Access" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__active +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__active +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__active +msgid "Active" +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "Archived" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields.selection,name:cetmix_tower_git.selection__cx_tower_git_remote__repo_provider__bitbucket +msgid "Bitbucket" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields.selection,name:cetmix_tower_git.selection__cx_tower_git_remote__head_type__branch +msgid "Branch" +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_remote_view_form +msgid "Branch/PR/commit number or link" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_project__reference +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_remote__reference +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_source__reference +msgid "" +"Can contain English letters, digits and '_'. Leave blank to autogenerate" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model,name:cetmix_tower_git.model_cx_tower_git_project +msgid "Cetmix Tower Git Configuration" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model,name:cetmix_tower_git.model_cx_tower_git_remote +msgid "Cetmix Tower Git Remote" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model,name:cetmix_tower_git.model_cx_tower_git_source +msgid "Cetmix Tower Git Source" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model,name:cetmix_tower_git.model_cx_tower_server +msgid "Cetmix Tower Server" +msgstr "" + +#. module: cetmix_tower_git +#: code:addons/cetmix_tower_git/models/cx_tower_git_project_rel.py:0 +#, python-format +msgid "Code generator function for '%(project_format)s' format not found." +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields.selection,name:cetmix_tower_git.selection__cx_tower_git_remote__head_type__commit +msgid "Commit" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__create_uid +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__create_uid +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__create_uid +msgid "Created by" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__create_date +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__create_date +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__create_date +msgid "Created on" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model,name:cetmix_tower_git.model_cx_tower_file +msgid "Cx Tower File" +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_remote_view_form +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_source_view_form +msgid "Disabled" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_file__display_name +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__display_name +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_rel__display_name +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__display_name +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__display_name +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_server__display_name +msgid "Display Name" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_remote__enabled +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_source__enabled +msgid "Enable in configuration and exported to files" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__enabled +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__enabled +msgid "Enabled" +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "Export YAML" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_rel__file_id +msgid "File" +msgstr "" + +#. module: cetmix_tower_git +#: code:addons/cetmix_tower_git/models/cx_tower_git_project_rel.py:0 +#, python-format +msgid "File '%(file)s' doesn't belong to server '%(server)s'" +msgstr "" + +#. module: cetmix_tower_git +#: code:addons/cetmix_tower_git/models/cx_tower_file.py:0 +#, python-format +msgid "" +"File '%(file)s' is related to multiple projects: %(projects)s \n" +"Please select only one project." +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.constraint,message:cetmix_tower_git.constraint_cx_tower_git_project_rel_project_server_file_format_uniq +msgid "File is already related to the same project and format" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__file_ids +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "Files" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_rel__project_format +msgid "Format" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__git_aggregator_root_dir +msgid "Git Aggregator Root Dir" +msgstr "" + +#. module: cetmix_tower_git +#: code:addons/cetmix_tower_git/models/cx_tower_git_remote.py:0 +#, python-format +msgid "" +"Git Aggregator: Bitbucket does not support fetching PRs. Please use branch instead.\n" +"\n" +"Source: %(src)s\n" +"URL: %(url)s\n" +"Head: %(head)s" +msgstr "" + +#. module: cetmix_tower_git +#: code:addons/cetmix_tower_git/models/cx_tower_git_remote.py:0 +#: code:addons/cetmix_tower_git/models/cx_tower_git_remote.py:0 +#: code:addons/cetmix_tower_git/models/cx_tower_git_remote.py:0 +#, python-format +msgid "Git Aggregator: Head number is empty in %(head)s" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__git_project_id +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__git_project_id +msgid "Git Configuration" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_file__git_project_id +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_rel__git_project_id +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_server__git_project_ids +msgid "Git Project" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_server__git_project_rel_ids +msgid "Git Project Rel" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__git_project_rel_ids +msgid "Git Project Server File Relations" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model,name:cetmix_tower_git.model_cx_tower_git_project_rel +msgid "Git Project relation to other model records" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.actions.act_window,name:cetmix_tower_git.cx_tower_git_project_action +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_file__git_project_ids +#: model:ir.ui.menu,name:cetmix_tower_git.menu_cx_tower_git_project +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_server_view_form +msgid "Git Projects" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_project__git_aggregator_root_dir +msgid "" +"Git aggregator root directory where sources will be cloned. Eg '/tmp/git-" +"aggregator' Will use '.' if not set" +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "" +"Git aggregator root directory where sources will be cloned. Leave blank to " +"use '.'" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_remote__url +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_remote_view_form +msgid "" +"Git remote URL. Eg 'https://github.com/cetmix/cetmix-tower.git' or " +"'git@github.com:cetmix/cetmix-tower.git'" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_remote__head +msgid "" +"Git remote head. Link to branch, PR, commit or commit hash. Leave blank to " +"auto-detect" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields.selection,name:cetmix_tower_git.selection__cx_tower_git_remote__repo_provider__github +msgid "GitHub" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields.selection,name:cetmix_tower_git.selection__cx_tower_git_remote__repo_provider__gitlab +msgid "GitLab" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields.selection,name:cetmix_tower_git.selection__cx_tower_git_remote__url_protocol__https +msgid "HTTPS" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__has_partially_private_remotes +msgid "Has Partially Private Remotes" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__has_private_remotes +msgid "Has Private Remotes" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__head +msgid "Head" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__head_type +msgid "Head Type" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_file__id +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__id +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_rel__id +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__id +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__id +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_server__id +msgid "ID" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_project__has_partially_private_remotes +msgid "Indicates if the project has any partially private remotes." +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_project__has_private_remotes +msgid "Indicates if the project has any private remotes." +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__is_private +msgid "Is Private" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_file____last_update +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project____last_update +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_rel____last_update +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote____last_update +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source____last_update +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_server____last_update +msgid "Last Modified on" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__write_uid +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__write_uid +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__write_date +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__write_date +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__write_date +msgid "Last Updated on" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__manager_ids +msgid "Managers" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_project__manager_ids +msgid "Managers who can modify this record" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__name +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_rel__name +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__name +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__name +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_source_view_form +msgid "Name" +msgstr "" + +#. module: cetmix_tower_git +#: code:addons/cetmix_tower_git/models/cx_tower_git_remote.py:0 +#, python-format +msgid "Not a valid URL. URL must end with '.git'" +msgstr "" + +#. module: cetmix_tower_git +#: code:addons/cetmix_tower_git/models/cx_tower_git_remote.py:0 +#, python-format +msgid "Not a valid URL. URL must start with 'https://' or 'git@'" +msgstr "" + +#. module: cetmix_tower_git +#: code:addons/cetmix_tower_git/models/cx_tower_git_remote.py:0 +#, python-format +msgid "" +"Not a valid URL: %(url_msg)s\n" +"URL must contain at least two parts separated by dot." +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields.selection,name:cetmix_tower_git.selection__cx_tower_git_remote__repo_provider__other +msgid "Other" +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_source_view_form +msgid "Private" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__remote_count_private +msgid "Private Remotes" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields.selection,name:cetmix_tower_git.selection__cx_tower_git_remote__head_type__pr +msgid "Pull/Merge Request" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__reference +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__reference +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__reference +msgid "Reference" +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_source_view_form +msgid "" +"Reference. Can contain English letters, digits and '_'. Leave blank to " +"autogenerate" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__remote_count +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__remote_ids +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_source_view_form +msgid "Remotes" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__repo_provider +msgid "Repository Provider" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_remote__is_private +msgid "Repository is private" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields.selection,name:cetmix_tower_git.selection__cx_tower_git_remote__url_protocol__ssh +msgid "SSH" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__sequence +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__sequence +msgid "Sequence" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_rel__server_id +msgid "Server" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__server_ids +msgid "Servers" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_project__server_ids +msgid "" +"Servers are added automatically based on the files linked to the project.\n" +"IMPORTANT: This field may contain duplicates because of the relation nature!" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__source_id +msgid "Source" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__source_ids +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "Sources" +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_source_view_form +msgid "" +"The top one remote will be used as a merge target.\n" +" You can re-arrange remotes by dragging them or changing their sequence value." +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__url +msgid "URL" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__url_protocol +msgid "URL Protocol" +msgstr "" + +#. module: cetmix_tower_git +#: code:addons/cetmix_tower_git/models/cx_tower_git_remote.py:0 +#, python-format +msgid "URL is required" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__user_ids +msgid "Users" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_project__user_ids +msgid "Users who can view this record" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_remote__repo_provider +msgid "" +"Will be tried to be determined from the URL. Please select manually if auto-" +"detection fails." +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "YAML" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__yaml_code +msgid "Yaml Code" +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "" +"You can edit these fields at your own risk. However keep in mind that they " +"will be automatically updated each time related servers are added, removed " +"or updated." +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "You must be a member of the \"YAML/Export\" group to export data as YAML." +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "managers who can modify this record" +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "users who can view this record" +msgstr "" diff --git a/addons/cetmix_tower_git/i18n/hr.po b/addons/cetmix_tower_git/i18n/hr.po new file mode 100644 index 0000000..cbf3682 --- /dev/null +++ b/addons/cetmix_tower_git/i18n/hr.po @@ -0,0 +1,635 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * cetmix_tower_git +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server \n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2025-03-06 09:11+0000\n" +"Last-Translator: Bole \n" +"Language-Team: Croatian \n" +"Language: hr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " +"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" +"X-Generator: Weblate 5.10.3-dev\n" + +#. module: cetmix_tower_git +#: code:addons/cetmix_tower_git/models/cx_tower_git_project.py:0 +#, python-format +msgid "" +"\n" +"# You need to set the following variables in your environment:\n" +"# %(vars)s \n" +"# and run git-aggregator with '--expand-env' parameter.\n" +msgstr "" +"\n" +"# Potrebno je postaviti sljedeće varijable u vaše okruženje:\n" +"# %(vars)s \n" +"# i pokrenuti git-aggregator sa ' --expand-env' parametrom\n" + +#. module: cetmix_tower_git +#: code:addons/cetmix_tower_git/models/cx_tower_git_project.py:0 +#, python-format +msgid "" +"# This file is generated with Cetmix Tower https://cetmix.com/tower\n" +"# It's designed to be used with git-aggregator tool developed by Acsone.\n" +"# Documentation for git-aggregator: https://github.com/acsone/git-aggregator\n" +msgstr "" +"# Ova datotek aje generiran pomoću Cetmix Tower sustava: https://cetmix.com/" +"tower\n" +"# Dizajniran je za korištenje sa git-aggregator alatom razvijenim od Ascone." +"\n" +"# Dokumentacija za git-aggregator : https://github.com/acsone/git-" +"aggregator\n" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "* Sources where all remotes are private" +msgstr "* izvori sa svim udaljenim lokacijama koje su privatne" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "* Sources where some remotes are private" +msgstr "* Izvori u kojima su neke udaljene lokacije privatne" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "" +"Managers. All users who have \"Manager\" group and are set as \"Managers\" in all related servers.\n" +" This is done to avoid unpredictable consequences when some of the servers are not updated due to access restrictions when a project is updated." +msgstr "" +"Manageri. Svi korisnici koji imaju \"Manager\" grupu i postavljeni su " +"kao \"Manageri\" u svim povezanim serverima.\n" +" " +"Ovo je napravljeno kako bi izbjegli nepredviđene posljedice kad neki od " +"servera nisu ažurirani zbog ograničenog pristupa prilikom ažuriranja " +"projekta." + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "" +"Users. All users who have \"Manager\" group and are either set in " +"\"Users\" or in \"Managers\" in all related servers." +msgstr "" +"Korisnici. Svi korisnici koji imaju \"Manager\" grupu i postavljeni " +"su ili kao \"Korisnik\" ili kao \"Manager\" u svim povezanim " +"serverima." + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "Access" +msgstr "Pristup" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__active +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__active +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__active +msgid "Active" +msgstr "Aktivno" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "Archived" +msgstr "Arhivirano" + +#. module: cetmix_tower_git +#: model:ir.model.fields.selection,name:cetmix_tower_git.selection__cx_tower_git_remote__repo_provider__bitbucket +msgid "Bitbucket" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields.selection,name:cetmix_tower_git.selection__cx_tower_git_remote__head_type__branch +msgid "Branch" +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_remote_view_form +msgid "Branch/PR/commit number or link" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_project__reference +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_remote__reference +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_source__reference +msgid "" +"Can contain English letters, digits and '_'. Leave blank to autogenerate" +msgstr "" +"Može sadržavati slova engleske abecede, brojke i ':'. Ostavite prazno za " +"automatsko generiranje" + +#. module: cetmix_tower_git +#: model:ir.model,name:cetmix_tower_git.model_cx_tower_git_project +msgid "Cetmix Tower Git Configuration" +msgstr "Cetmix Tower Git postavke" + +#. module: cetmix_tower_git +#: model:ir.model,name:cetmix_tower_git.model_cx_tower_git_remote +msgid "Cetmix Tower Git Remote" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model,name:cetmix_tower_git.model_cx_tower_git_source +msgid "Cetmix Tower Git Source" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model,name:cetmix_tower_git.model_cx_tower_server +msgid "Cetmix Tower Server" +msgstr "" + +#. module: cetmix_tower_git +#: code:addons/cetmix_tower_git/models/cx_tower_git_project_rel.py:0 +#, python-format +msgid "Code generator function for '%(project_format)s' format not found." +msgstr "" +"Funkcija generiranja koda za '%(project_format)s' format nije pronađena." + +#. module: cetmix_tower_git +#: model:ir.model.fields.selection,name:cetmix_tower_git.selection__cx_tower_git_remote__head_type__commit +msgid "Commit" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__create_uid +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__create_uid +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__create_uid +msgid "Created by" +msgstr "Kreirao" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__create_date +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__create_date +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__create_date +msgid "Created on" +msgstr "Kreirano" + +#. module: cetmix_tower_git +#: model:ir.model,name:cetmix_tower_git.model_cx_tower_file +msgid "Cx Tower File" +msgstr "Cx Tower datoteka" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_remote_view_form +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_source_view_form +msgid "Disabled" +msgstr "Onemogućen" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_file__display_name +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__display_name +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_rel__display_name +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__display_name +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__display_name +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_server__display_name +msgid "Display Name" +msgstr "Naziv" + +#. module: cetmix_tower_git +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_remote__enabled +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_source__enabled +msgid "Enable in configuration and exported to files" +msgstr "Omogućen u postavkama i izvezen u datoteku" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__enabled +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__enabled +msgid "Enabled" +msgstr "Omogućen" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "Export YAML" +msgstr "Izvoz YAML" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_rel__file_id +msgid "File" +msgstr "Datoteka" + +#. module: cetmix_tower_git +#: code:addons/cetmix_tower_git/models/cx_tower_git_project_rel.py:0 +#, python-format +msgid "File '%(file)s' doesn't belong to server '%(server)s'" +msgstr "Datoteka '%(file)s' ne pripada serveru '%(server)s'" + +#. module: cetmix_tower_git +#: code:addons/cetmix_tower_git/models/cx_tower_file.py:0 +#, python-format +msgid "" +"File '%(file)s' is related to multiple projects: %(projects)s \n" +"Please select only one project." +msgstr "" +"Datoteka '%(file)s' je povezana sa višestrukim projektima: %(projects)s \n" +"Molim odaberite samo jedan projekt." + +#. module: cetmix_tower_git +#: model:ir.model.constraint,message:cetmix_tower_git.constraint_cx_tower_git_project_rel_project_server_file_format_uniq +msgid "File is already related to the same project and format" +msgstr "Datoteka je već povezana sa ovim projektom i ovim formatom" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__file_ids +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "Files" +msgstr "Datoteke" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_rel__project_format +msgid "Format" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__git_aggregator_root_dir +msgid "Git Aggregator Root Dir" +msgstr "" + +#. module: cetmix_tower_git +#: code:addons/cetmix_tower_git/models/cx_tower_git_remote.py:0 +#, python-format +msgid "" +"Git Aggregator: Bitbucket does not support fetching PRs. Please use branch instead.\n" +"\n" +"Source: %(src)s\n" +"URL: %(url)s\n" +"Head: %(head)s" +msgstr "" +"Git Aggregator: BitBucket ne podržava dohvaćanje PRova. Molim koristite " +"branch.\n" +"\n" +"Source: %(src)s\n" +"URL: %(url)s\n" +"Head: %(head)s" + +#. module: cetmix_tower_git +#: code:addons/cetmix_tower_git/models/cx_tower_git_remote.py:0 +#: code:addons/cetmix_tower_git/models/cx_tower_git_remote.py:0 +#: code:addons/cetmix_tower_git/models/cx_tower_git_remote.py:0 +#, python-format +msgid "Git Aggregator: Head number is empty in %(head)s" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__git_project_id +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__git_project_id +msgid "Git Configuration" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_file__git_project_id +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_rel__git_project_id +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_server__git_project_ids +msgid "Git Project" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_server__git_project_rel_ids +msgid "Git Project Rel" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__git_project_rel_ids +msgid "Git Project Server File Relations" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model,name:cetmix_tower_git.model_cx_tower_git_project_rel +msgid "Git Project relation to other model records" +msgstr "Git projekt povezan sa ostalim zapisima modela" + +#. module: cetmix_tower_git +#: model:ir.actions.act_window,name:cetmix_tower_git.cx_tower_git_project_action +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_file__git_project_ids +#: model:ir.ui.menu,name:cetmix_tower_git.menu_cx_tower_git_project +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_server_view_form +msgid "Git Projects" +msgstr "Git projekti" + +#. module: cetmix_tower_git +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_project__git_aggregator_root_dir +msgid "" +"Git aggregator root directory where sources will be cloned. Eg '/tmp/git-" +"aggregator' Will use '.' if not set" +msgstr "" +"GitAgregator izvorni direktorij u koji će izvori biti klonirani. Npr. '/tmp/" +"git-aggregator' Ako ništa nije postavljeno koristi se '.'" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "" +"Git aggregator root directory where sources will be cloned. Leave blank to " +"use '.'" +msgstr "" +"GitAgregtor izvorni dirketorij u koji će izvori biti klonirani. Ostavite " +"prazno za korištenje '.'" + +#. module: cetmix_tower_git +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_remote__url +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_remote_view_form +msgid "" +"Git remote URL. Eg 'https://github.com/cetmix/cetmix-tower.git' or " +"'git@github.com:cetmix/cetmix-tower.git'" +msgstr "" +"Git remote URL. Eg 'https://github.com/cetmix/cetmix-tower.git' ili " +"'git@github.com:cetmix/cetmix-tower.git'" + +#. module: cetmix_tower_git +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_remote__head +msgid "" +"Git remote head. Link to branch, PR, commit or commit hash. Leave blank to " +"auto-detect" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields.selection,name:cetmix_tower_git.selection__cx_tower_git_remote__repo_provider__github +msgid "GitHub" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields.selection,name:cetmix_tower_git.selection__cx_tower_git_remote__repo_provider__gitlab +msgid "GitLab" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields.selection,name:cetmix_tower_git.selection__cx_tower_git_remote__url_protocol__https +msgid "HTTPS" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__has_partially_private_remotes +msgid "Has Partially Private Remotes" +msgstr "Ima djelomično privatne udaljene izvore" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__has_private_remotes +msgid "Has Private Remotes" +msgstr "Ima privatne udaljene izvore" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__head +msgid "Head" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__head_type +msgid "Head Type" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_file__id +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__id +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_rel__id +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__id +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__id +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_server__id +msgid "ID" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_project__has_partially_private_remotes +msgid "Indicates if the project has any partially private remotes." +msgstr "Indicira ima li projekt djelomično privatnih udaljenih izvora." + +#. module: cetmix_tower_git +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_project__has_private_remotes +msgid "Indicates if the project has any private remotes." +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__is_private +msgid "Is Private" +msgstr "Je privatno" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_file____last_update +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project____last_update +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_rel____last_update +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote____last_update +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source____last_update +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_server____last_update +msgid "Last Modified on" +msgstr "Zadnje modificirano" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__write_uid +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__write_uid +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__write_uid +msgid "Last Updated by" +msgstr "Zadnji ažurirao" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__write_date +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__write_date +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__write_date +msgid "Last Updated on" +msgstr "Zadnje ažurirano" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__manager_ids +msgid "Managers" +msgstr "Manageri" + +#. module: cetmix_tower_git +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_project__manager_ids +msgid "Managers who can modify this record" +msgstr "Manageri koji mogu urediti ovaj zapis" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__name +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_rel__name +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__name +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__name +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_source_view_form +msgid "Name" +msgstr "Naziv" + +#. module: cetmix_tower_git +#: code:addons/cetmix_tower_git/models/cx_tower_git_remote.py:0 +#, python-format +msgid "Not a valid URL. URL must end with '.git'" +msgstr "Nije valjani URL. URL mora završavati sa '.git'" + +#. module: cetmix_tower_git +#: code:addons/cetmix_tower_git/models/cx_tower_git_remote.py:0 +#, python-format +msgid "Not a valid URL. URL must start with 'https://' or 'git@'" +msgstr "Nije valjani URL. URL mora počinjati sa 'https://' ili 'git@'" + +#. module: cetmix_tower_git +#: code:addons/cetmix_tower_git/models/cx_tower_git_remote.py:0 +#, python-format +msgid "" +"Not a valid URL: %(url_msg)s\n" +"URL must contain at least two parts separated by dot." +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields.selection,name:cetmix_tower_git.selection__cx_tower_git_remote__repo_provider__other +msgid "Other" +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_source_view_form +msgid "Private" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__remote_count_private +msgid "Private Remotes" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields.selection,name:cetmix_tower_git.selection__cx_tower_git_remote__head_type__pr +msgid "Pull/Merge Request" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__reference +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__reference +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__reference +msgid "Reference" +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_source_view_form +msgid "" +"Reference. Can contain English letters, digits and '_'. Leave blank to " +"autogenerate" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__remote_count +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__remote_ids +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_source_view_form +msgid "Remotes" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__repo_provider +msgid "Repository Provider" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_remote__is_private +msgid "Repository is private" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields.selection,name:cetmix_tower_git.selection__cx_tower_git_remote__url_protocol__ssh +msgid "SSH" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__sequence +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__sequence +msgid "Sequence" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_rel__server_id +msgid "Server" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__server_ids +msgid "Servers" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_project__server_ids +msgid "" +"Servers are added automatically based on the files linked to the project.\n" +"IMPORTANT: This field may contain duplicates because of the relation nature!" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__source_id +msgid "Source" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__source_ids +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "Sources" +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_source_view_form +msgid "" +"The top one remote will be used as a merge target.\n" +" You can re-arrange remotes by dragging them or changing their sequence value." +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__url +msgid "URL" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__url_protocol +msgid "URL Protocol" +msgstr "" + +#. module: cetmix_tower_git +#: code:addons/cetmix_tower_git/models/cx_tower_git_remote.py:0 +#, python-format +msgid "URL is required" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__user_ids +msgid "Users" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_project__user_ids +msgid "Users who can view this record" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_remote__repo_provider +msgid "" +"Will be tried to be determined from the URL. Please select manually if auto-" +"detection fails." +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "YAML" +msgstr "" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__yaml_code +msgid "Yaml Code" +msgstr "YAML kod" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "" +"You can edit these fields at your own risk. However keep in mind that they " +"will be automatically updated each time related servers are added, removed " +"or updated." +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "You must be a member of the \"YAML/Export\" group to export data as YAML." +msgstr "Morate bit član \"YAML/Izvoz\" grupe za izvoz podataka u YAML." + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "managers who can modify this record" +msgstr "" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "users who can view this record" +msgstr "" diff --git a/addons/cetmix_tower_git/i18n/it.po b/addons/cetmix_tower_git/i18n/it.po new file mode 100644 index 0000000..d1b6a28 --- /dev/null +++ b/addons/cetmix_tower_git/i18n/it.po @@ -0,0 +1,740 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * cetmix_tower_git +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: 2025-11-17 10:33+0100\n" +"Last-Translator: Stefano Consolaro \n" +"Language-Team: Italian \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: it\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Poedit 2.3\n" + +#. module: cetmix_tower_git +#. odoo-python +#: code:addons/cetmix_tower_git/models/cx_tower_git_project.py:0 +#, python-format +msgid "" +"\n" +"# You need to set the following variables in your environment:\n" +"# %(vars)s \n" +"# and run git-aggregator with '--expand-env' parameter.\n" +msgstr "" +"\n" +"# È necessario impostare le seguenti variabili d'ambiente:\n" +"# %(vars)s \n" +"# ed eseguire git-aggregator con il parametro '--expand-env'.\n" + +#. module: cetmix_tower_git +#. odoo-python +#: code:addons/cetmix_tower_git/models/cx_tower_git_project.py:0 +#, python-format +msgid "" +"# This file is generated with Cetmix Tower https://cetmix.com/tower\n" +"# It's designed to be used with git-aggregator tool developed by Acsone.\n" +"# Documentation for git-aggregator: https://github.com/acsone/git-aggregator\n" +msgstr "" +"# Questo file è generato con Cetmix Tower https://cetmix.com/tower\n" +"# È progettato per essere usato con il tool git-aggregator sviluppato da Acsone.\n" +"# Documentazione per git-aggregator: https://github.com/acsone/git-aggregator\n" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "* Sources where all remotes are private" +msgstr "* Origini dove tutti i remote sono privati" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "* Sources where some remotes are private" +msgstr "* Origini dove alcuni remote sono privati" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "" +"Managers. All users who have \"Manager\" group and are set as \"Managers\" in all related servers.\n" +" This is done to avoid unpredictable consequences when some of the servers are not updated due to access restrictions when a project is updated." +msgstr "" +"Responsabili. Tutti gli utenti che hanno il gruppo \"Responsabile\" e sono impostati come \"Responsabili\" in tutti i server relativi.\n" +" Questo è fatto per evitare conseguenze imprevedibili quando qualcuno dei server non è aggiornato per limitazioni di accesso quando viene aggiornato un progetto." + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "Users. All users who have \"Manager\" group and are either set in \"Users\" or in \"Managers\" in all related servers." +msgstr "Utenti. Tutti gli utenti che hanno il gruppo \"Responsabile\" e sono impostati come \"Utenti\" o \"Responsabili\" in tutti i server relativi." + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "Access" +msgstr "Accesso" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__active +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__active +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__active +msgid "Active" +msgstr "Attivo" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "Archived" +msgstr "In archivio" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_rel__auto_sync +msgid "Auto Sync" +msgstr "Sincronizzazione automatica" + +#. module: cetmix_tower_git +#: model:ir.model.fields.selection,name:cetmix_tower_git.selection__cx_tower_git_remote__repo_provider__bitbucket +msgid "Bitbucket" +msgstr "Bitbucket" + +#. module: cetmix_tower_git +#: model:ir.model.fields.selection,name:cetmix_tower_git.selection__cx_tower_git_remote__head_type__branch +msgid "Branch" +msgstr "Branch" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_remote_view_form +msgid "Branch/PR/commit number or link" +msgstr "Numero o link branch/PR/commit" + +#. module: cetmix_tower_git +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_project__reference +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_project_rel__reference +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_remote__reference +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_source__reference +msgid "Can contain English letters, digits and '_'. Leave blank to autogenerate" +msgstr "Può contenere lettere inglesi, cifre e '_'. Lasciare vuoto per la generazione automatica" + +#. module: cetmix_tower_git +#: model:ir.model,name:cetmix_tower_git.model_cx_tower_file +msgid "Cetmix Tower File" +msgstr "File Cetmix Tower" + +#. module: cetmix_tower_git +#: model:ir.model,name:cetmix_tower_git.model_cx_tower_file_template +msgid "Cetmix Tower File Template" +msgstr "Modello file Cetmix Tower" + +#. module: cetmix_tower_git +#: model:ir.model,name:cetmix_tower_git.model_cx_tower_plan_line +msgid "Cetmix Tower Flight Plan Line" +msgstr "Riga piano di volo Cetmix Tower" + +#. module: cetmix_tower_git +#: model:ir.model,name:cetmix_tower_git.model_cx_tower_git_project +msgid "Cetmix Tower Git Project" +msgstr "Progetto Git Cetmix Tower" + +#. module: cetmix_tower_git +#: model:ir.model,name:cetmix_tower_git.model_cx_tower_git_project_file_template_rel +msgid "Cetmix Tower Git Project relation to File Templates" +msgstr "Relazione progetto Git Cetmix Tower al modello file" + +#. module: cetmix_tower_git +#: model:ir.model,name:cetmix_tower_git.model_cx_tower_git_project_rel +msgid "Cetmix Tower Git Project relation to Files and Servers" +msgstr "Relazione progetto Git Cetmix Tower a file e server" + +#. module: cetmix_tower_git +#: model:ir.model,name:cetmix_tower_git.model_cx_tower_git_remote +msgid "Cetmix Tower Git Remote" +msgstr "Remote Git Cetmix Tower" + +#. module: cetmix_tower_git +#: model:ir.model,name:cetmix_tower_git.model_cx_tower_git_source +msgid "Cetmix Tower Git Source" +msgstr "Origine Git Cetmix Tower" + +#. module: cetmix_tower_git +#: model:ir.model,name:cetmix_tower_git.model_cetmix_tower +msgid "Cetmix Tower Odoo Automation" +msgstr "Automazione Odoo Cetmix Tower" + +#. module: cetmix_tower_git +#: model:ir.model,name:cetmix_tower_git.model_cx_tower_server +msgid "Cetmix Tower Server" +msgstr "Server Cetmix Tower" + +#. module: cetmix_tower_git +#. odoo-python +#: code:addons/cetmix_tower_git/models/cx_tower_git_project_file_template_rel.py:0 +#: code:addons/cetmix_tower_git/models/cx_tower_git_project_rel.py:0 +#, python-format +msgid "Code generator function for '%(project_format)s' format not found." +msgstr "Funzione generazione codice per il formato '%(project_format)s' non trovata." + +#. module: cetmix_tower_git +#: model:ir.model.fields.selection,name:cetmix_tower_git.selection__cx_tower_git_remote__head_type__commit +msgid "Commit" +msgstr "Commit" + +#. module: cetmix_tower_git +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_plan_line__is_make_copy +msgid "Create a copy of the Git Project instead of linking the file to the existing one." +msgstr "Crea una copia del progetto Git invece di collegare il file a uno esistente." + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__create_uid +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__create_uid +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__create_uid +msgid "Created by" +msgstr "Creato da" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__create_date +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__create_date +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__create_date +msgid "Created on" +msgstr "Creato il" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_remote_view_form +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_source_view_form +msgid "Disabled" +msgstr "Disabilitato" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__display_name +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_file_template_rel__display_name +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_rel__display_name +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__display_name +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__display_name +msgid "Display Name" +msgstr "Nome visualizzato" + +#. module: cetmix_tower_git +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_remote__enabled +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_source__enabled +msgid "Enable in configuration and exported to files" +msgstr "Abilitato in configurazione ed esportato nei file" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__enabled +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__enabled +msgid "Enabled" +msgstr "Abilitato" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "Export YAML" +msgstr "Esporta YAML" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_rel__file_id +msgid "File" +msgstr "File" + +#. module: cetmix_tower_git +#. odoo-python +#: code:addons/cetmix_tower_git/models/cx_tower_git_project_rel.py:0 +#, python-format +msgid "File '%(file)s' doesn't belong to server '%(server)s'" +msgstr "Il file '%(file)s' non appartiene al server '%(server)s'" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_file_template_rel__file_template_id +msgid "File Template" +msgstr "Modello file" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__file_template_ids +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "File Templates" +msgstr "Modelli file" + +#. module: cetmix_tower_git +#: model:ir.model.constraint,message:cetmix_tower_git.constraint_cx_tower_git_project_rel_project_server_file_format_uniq +msgid "File is already related to the same project and format" +msgstr "Il file è già associato allo stesso progetto e formato" + +#. module: cetmix_tower_git +#: model:ir.model.constraint,message:cetmix_tower_git.constraint_cx_tower_git_project_file_template_rel_project_server_file_format_uniq +msgid "File template is already related to the same project and format" +msgstr "Il modello di file è già associato allo stesso progetto e formato" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__file_ids +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "Files" +msgstr "File" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_file_template_rel__project_format +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_rel__project_format +msgid "Format" +msgstr "Formato" + +#. module: cetmix_tower_git +#: model:ir.model.fields.selection,name:cetmix_tower_git.selection__cx_tower_git_remote__url_protocol__git +msgid "GIT" +msgstr "GIT" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "General" +msgstr "Generale" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "Git Aggregator" +msgstr "Git Aggregator" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__git_aggregator_root_dir +msgid "Git Aggregator Root Dir" +msgstr "Directory radice Git Aggregator" + +#. module: cetmix_tower_git +#. odoo-python +#: code:addons/cetmix_tower_git/models/cx_tower_git_remote.py:0 +#, python-format +msgid "" +"Git Aggregator: Bitbucket does not support fetching PRs. Please use branch instead.\n" +"\n" +"Source: %(src)s\n" +"URL: %(url)s\n" +"Head: %(head)s" +msgstr "" +"Git Aggregator: Bitbucket non supporta il recupero delle PR. In alternativa usare il branch.\n" +"\n" +"Origine: %(src)s\n" +"URL: %(url)s\n" +"Head: %(head)s" + +#. module: cetmix_tower_git +#. odoo-python +#: code:addons/cetmix_tower_git/models/cx_tower_git_remote.py:0 +#, python-format +msgid "Git Aggregator: Head number is empty in %(head)s" +msgstr "Git Aggregator: il numero dell'head è vuoto in %(head)s" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__git_project_id +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__git_project_id +msgid "Git Configuration" +msgstr "Configurazione Git" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_file__git_project_id +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_file_template__git_project_id +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_file_template_rel__git_project_id +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_rel__git_project_id +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_plan_line__git_project_id +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_server__git_project_ids +msgid "Git Project" +msgstr "Progetto Git" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__git_project_file_template_rel_ids +msgid "Git Project File Template Relations" +msgstr "Relazioni modello file progetto Git" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_server__git_project_rel_ids +msgid "Git Project Rel" +msgstr "Rel progetto Git" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_file__git_project_rel_ids +msgid "Git Project Relations" +msgstr "Relazioni progetto Git" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__git_project_rel_ids +msgid "Git Project Server File Relations" +msgstr "Relazioni file server progetto Git" + +#. module: cetmix_tower_git +#: model:ir.actions.act_window,name:cetmix_tower_git.cx_tower_git_project_action +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_file_template__git_project_ids +#: model:ir.ui.menu,name:cetmix_tower_git.menu_cx_tower_git_project +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_server_view_form +msgid "Git Projects" +msgstr "Progetti Git" + +#. module: cetmix_tower_git +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_project__git_aggregator_root_dir +msgid "Git aggregator root directory where sources will be cloned. Eg '/tmp/git-aggregator' Will use '.' if not set" +msgstr "La directory radice di Git Aggregator dove le origini verranno clonate. Es. '/tmp/git-aggregator'. Verrà usato '.' se non impostata" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "Git aggregator root directory where sources will be cloned. Leave blank to use '.'" +msgstr "La directory radice di Git Aggregator dove le origini verranno clonate. Lasciare vuoto per usare '.'" + +#. module: cetmix_tower_git +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_remote__url +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_remote_view_form +msgid "Git remote URL. Eg 'https://github.com/cetmix/cetmix-tower.git' or 'git@github.com:cetmix/cetmix-tower.git'" +msgstr "URL del remote Git. Es. 'https://github.com/cetmix/cetmix-tower.git' o 'git@github.com:cetmix/cetmix-tower.git'" + +#. module: cetmix_tower_git +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_remote__head +msgid "Git remote head. Link to branch, PR, commit or commit hash. Leave blank to auto-detect" +msgstr "Head remoto Git. Collegamento al branch, alla PR, al commit o all'hash del commit. Lasciare vuoto per il rilevamento automatico" + +#. module: cetmix_tower_git +#: model:ir.model.fields.selection,name:cetmix_tower_git.selection__cx_tower_git_remote__repo_provider__github +msgid "GitHub" +msgstr "GitHub" + +#. module: cetmix_tower_git +#: model:ir.model.fields.selection,name:cetmix_tower_git.selection__cx_tower_git_remote__repo_provider__gitlab +msgid "GitLab" +msgstr "GitLab" + +#. module: cetmix_tower_git +#: model:ir.model.fields.selection,name:cetmix_tower_git.selection__cx_tower_git_remote__url_protocol__https +msgid "HTTPS" +msgstr "HTTPS" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__has_partially_private_remotes +msgid "Has Partially Private Remotes" +msgstr "Ha remote parzialmente privati" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__has_private_remotes +msgid "Has Private Remotes" +msgstr "Ha remote privati" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__head +msgid "Head" +msgstr "Head" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__head_type +msgid "Head Type" +msgstr "Tipo di head" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__id +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_file_template_rel__id +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_rel__id +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__id +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__id +msgid "ID" +msgstr "ID" + +#. module: cetmix_tower_git +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_project_rel__auto_sync +msgid "If enabled file will be synced automatically using cron" +msgstr "Se abilitato, il file verrà sincronizzato automaticamente tramite cron" + +#. module: cetmix_tower_git +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_project__has_partially_private_remotes +msgid "Indicates if the project has any partially private remotes." +msgstr "Indica se il progetto ha qualche remote parzialmente privato." + +#. module: cetmix_tower_git +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_project__has_private_remotes +msgid "Indicates if the project has any private remotes." +msgstr "Indica se il progetto ha qualche remote privato." + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__is_private +msgid "Is Private" +msgstr "È privato" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__write_uid +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__write_uid +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__write_uid +msgid "Last Updated by" +msgstr "Ultimo aggiornamento di" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__write_date +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__write_date +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__write_date +msgid "Last Updated on" +msgstr "Ultimo aggiornamento il" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_plan_line__is_make_copy +msgid "Make a Copy" +msgstr "Crea una copia" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__manager_ids +msgid "Managers" +msgstr "Responsabili" + +#. module: cetmix_tower_git +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_project__manager_ids +msgid "Managers who can modify this record" +msgstr "Responsabili che possono modificare questo record" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__name +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_file_template_rel__name +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_rel__name +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__name +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__name +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_source_view_form +msgid "Name" +msgstr "Nome" + +#. module: cetmix_tower_git +#. odoo-python +#: code:addons/cetmix_tower_git/models/cx_tower_git_remote.py:0 +#, python-format +msgid "Not a valid URL. URL must end with '.git'" +msgstr "URL non valido. L'URL deve terminare con '.git'" + +#. module: cetmix_tower_git +#. odoo-python +#: code:addons/cetmix_tower_git/models/cx_tower_git_remote.py:0 +#, python-format +msgid "Not a valid URL. URL must start with 'https://', 'git@', or 'git://'" +msgstr "URL non valido. L'URL deve iniziare con 'https://', 'git@' o 'git://'" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__note +msgid "Note" +msgstr "Nota" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_server_view_form +msgid "Open" +msgstr "Apri" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "Open File Template" +msgstr "Apri modello file" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_server_view_form +msgid "Open Git Project" +msgstr "Apri progetto Git" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "Open Server" +msgstr "Apri server" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "Open file template" +msgstr "Apri modello file" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "Open server" +msgstr "Apri server" + +#. module: cetmix_tower_git +#: model:ir.model.fields.selection,name:cetmix_tower_git.selection__cx_tower_git_remote__repo_provider__other +msgid "Other" +msgstr "Altro" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_source_view_form +msgid "Private" +msgstr "Privato" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__remote_count_private +msgid "Private Remotes" +msgstr "Remote privati" + +#. module: cetmix_tower_git +#: model:ir.model.fields.selection,name:cetmix_tower_git.selection__cx_tower_git_remote__head_type__pr +msgid "Pull/Merge Request" +msgstr "Pull/Merge Request" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "Put your notes here..." +msgstr "Inserire qui le note..." + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__reference +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_rel__reference +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__reference +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__reference +msgid "Reference" +msgstr "Riferimento" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_source_view_form +msgid "Reference. Can contain English letters, digits and '_'. Leave blank to autogenerate" +msgstr "Riferimento. Può contenere lettere inglesi, cifre e '_'. Lasciare vuoto per la generazione automatica" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__remote_ids +msgid "Remote" +msgstr "Remote" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__remote_count +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_source_view_form +msgid "Remotes" +msgstr "Remote" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__repo_provider +msgid "Repository Provider" +msgstr "Fornitore repository" + +#. module: cetmix_tower_git +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_remote__is_private +msgid "Repository is private" +msgstr "Il repository è privato" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "Root Directory" +msgstr "Cartella radice" + +#. module: cetmix_tower_git +#: model:ir.model.fields.selection,name:cetmix_tower_git.selection__cx_tower_git_remote__url_protocol__ssh +msgid "SSH" +msgstr "SSH" + +#. module: cetmix_tower_git +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_plan_line__git_project_id +#, fuzzy +msgid "Select a git project to be linked to the file and server." +msgstr "Seleziona un progetto Git da collegare al file e al server." + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__sequence +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__sequence +msgid "Sequence" +msgstr "Sequenza" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_file__server_id +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_rel__server_id +msgid "Server" +msgstr "Server" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__server_ids +msgid "Servers" +msgstr "Server" + +#. module: cetmix_tower_git +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_project__server_ids +msgid "" +"Servers are added automatically based on the files linked to the project.\n" +"IMPORTANT: This field may contain duplicates because of the relation nature!" +msgstr "" +"I server sono aggiunti automaticamente in base ai file collegati al progetto.\n" +"IMPORTANTE: questo campo può contenere duplicati a causa della natura della relazione!" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__source_id +msgid "Source" +msgstr "Origine" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__source_ids +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "Sources" +msgstr "Origini" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_source_view_form +msgid "" +"The top one remote will be used as a merge target.\n" +" You can re-arrange remotes by dragging them or changing their sequence value." +msgstr "" +"Il remote in cima verrà utilizzato come destinazione del merge.\n" +" È possibile riorganizzare i remote trascinandoli o modificando il loro valore di sequenza." + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__url +msgid "URL" +msgstr "URL" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__url_protocol +msgid "URL Protocol" +msgstr "Protocollo URL" + +#. module: cetmix_tower_git +#. odoo-python +#: code:addons/cetmix_tower_git/models/cx_tower_git_remote.py:0 +#, python-format +msgid "URL is required" +msgstr "È richiesto l'URL" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__user_ids +msgid "Users" +msgstr "Utenti" + +#. module: cetmix_tower_git +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_project__user_ids +msgid "Users who can view this record" +msgstr "Utenti che possono vedere questo record" + +#. module: cetmix_tower_git +#: model:ir.model.fields,help:cetmix_tower_git.field_cx_tower_git_remote__repo_provider +msgid "Will be tried to be determined from the URL. Please select manually if auto-detection fails." +msgstr "Si cercherà di determinarlo dall'URL. Selezionarlo manualmente se fallisce l'auto determinazione." + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "YAML" +msgstr "YAML" + +#. module: cetmix_tower_git +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_project_rel__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_remote__yaml_code +#: model:ir.model.fields,field_description:cetmix_tower_git.field_cx_tower_git_source__yaml_code +msgid "Yaml Code" +msgstr "Codice YAML" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "You can edit these fields at your own risk. However keep in mind that they will be automatically updated each time related servers are added, removed or updated." +msgstr "È possibile modificare questi campi a proprio rischio. Tuttavia, tenere presente che verranno aggiornati automaticamente ogni volta che i server correlati vengono aggiunti, rimossi o aggiornati." + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "You must be a member of the \"YAML/Export\" group to export data as YAML." +msgstr "Bisogna appartenere al gruppo \"YAML/Export\" per esportare dati in YAML." + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "managers who can modify this record" +msgstr "responsabili che possono modificare questo record" + +#. module: cetmix_tower_git +#: model_terms:ir.ui.view,arch_db:cetmix_tower_git.cx_tower_git_project_view_form +msgid "users who can view this record" +msgstr "utenti che possono visualizzare questo record" + +#~ msgid "" +#~ "File '%(file)s' is related to multiple projects: %(projects)s \n" +#~ "Please select only one project." +#~ msgstr "" +#~ "Il file '%(file)s' è relativo a progetti multipli: %(projects)s \n" +#~ "Selezionare solo un progetto." + +#~ msgid "Last Modified on" +#~ msgstr "Ultima modifica il" + +#~ msgid "" +#~ "Not a valid URL: %(url_msg)s\n" +#~ "URL must contain at least two parts separated by dot." +#~ msgstr "" +#~ "URL non valdio: %(url_msg)s\n" +#~ "URL deve contenere almeno due parti separate da un punto." diff --git a/addons/cetmix_tower_git/models/__init__.py b/addons/cetmix_tower_git/models/__init__.py new file mode 100644 index 0000000..06e0f96 --- /dev/null +++ b/addons/cetmix_tower_git/models/__init__.py @@ -0,0 +1,15 @@ +# cx_tower_git_project_rel must be the first one in the list +# in order to create the relation table properly +from . import cx_tower_git_project_rel +from . import cx_tower_git_project_file_template_rel +from . import cx_tower_file +from . import cx_tower_file_template +from . import cx_tower_git_project +from . import cx_tower_git_remote +from . import cx_tower_git_repo +from . import cx_tower_git_repo_owner +from . import cx_tower_git_source +from . import cx_tower_server +from . import cetmix_tower +from . import cx_tower_plan_line +from . import cx_tower_command diff --git a/addons/cetmix_tower_git/models/cetmix_tower.py b/addons/cetmix_tower_git/models/cetmix_tower.py new file mode 100644 index 0000000..4a7448f --- /dev/null +++ b/addons/cetmix_tower_git/models/cetmix_tower.py @@ -0,0 +1,35 @@ +# Copyright (C) 2024 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, models + + +class CetmixTower(models.AbstractModel): + _inherit = "cetmix.tower" + + @api.model + def servers_by_git_ref(self, repository_url, head=None, head_type=None): + """ + Return servers linked to a given Git repository reference. + + This is a thin shortcut that delegates to + :meth:`cx.tower.server.get_servers_by_git_ref`. + + Parameters + ---------- + repository_url : str + Pre-normalized canonical Git URL + (e.g. ``https://host/owner/repo.git``). + head : str, optional + Branch name, commit SHA, or PR identifier. + head_type : {'branch', 'commit', 'pr'}, optional + Type of the ``head`` argument. + + Returns + ------- + recordset of cx.tower.server + Matching servers. Empty recordset if no matches. + """ + return self.env["cx.tower.server"].get_servers_by_git_ref( + repository_url, head, head_type + ) diff --git a/addons/cetmix_tower_git/models/cx_tower_command.py b/addons/cetmix_tower_git/models/cx_tower_command.py new file mode 100644 index 0000000..7726e36 --- /dev/null +++ b/addons/cetmix_tower_git/models/cx_tower_command.py @@ -0,0 +1,37 @@ +# Copyright 2024 Cetmix OÜ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import _, models +from odoo.tools.safe_eval import wrap_module + +# Wrap giturlparse safely +giturlparse = wrap_module(__import__("giturlparse"), ["parse", "validate"]) + + +class CxTowerCommand(models.Model): + """Extends cx.tower.command to add giturlparse functionality.""" + + _inherit = "cx.tower.command" + + def _custom_python_libraries(self): + """ + Add the giturlparse library to the available libraries. + """ + custom_python_libraries = super()._custom_python_libraries() + custom_python_libraries.update( + { + "cetmix_tower_git": { + "giturlparse": { + "import": giturlparse, + "help": _( + "Python library for Git URL parsing. " + "Available methods: 'parse', 'validate'. " + " Documentation on GitHub." + ), + }, + } + } + ) + return custom_python_libraries diff --git a/addons/cetmix_tower_git/models/cx_tower_file.py b/addons/cetmix_tower_git/models/cx_tower_file.py new file mode 100644 index 0000000..38fbd04 --- /dev/null +++ b/addons/cetmix_tower_git/models/cx_tower_file.py @@ -0,0 +1,54 @@ +# Copyright (C) 2024 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import api, fields, models + + +class CxTowerFile(models.Model): + _inherit = "cx.tower.file" + + git_project_id = fields.Many2one( + comodel_name="cx.tower.git.project", + compute="_compute_git_project_id", + store=True, + ) + git_project_rel_ids = fields.One2many( + comodel_name="cx.tower.git.project.rel", + inverse_name="file_id", + string="Git Project Relations", + copy=False, + ) + + # Get server from the first related git project relation + # This is needed for YAML import + server_id = fields.Many2one( + comodel_name="cx.tower.server", + compute="_compute_git_project_id", + store=True, + readonly=False, + ) + + @api.depends("git_project_rel_ids.server_id", "git_project_rel_ids.git_project_id") + def _compute_git_project_id(self): + """ + Link to project using the proxy model. + """ + for record in self: + # File is related to project via proxy model. + # So there can be only one record in o2m field. + git_project_relation = ( + record.git_project_rel_ids and record.git_project_rel_ids[0] + ) + if git_project_relation: + record.update( + { + "git_project_id": git_project_relation.git_project_id, + "server_id": git_project_relation.server_id, + } + ) + else: + # Reset only git project id as file still belongs to the server + record.update( + { + "git_project_id": False, + } + ) diff --git a/addons/cetmix_tower_git/models/cx_tower_file_template.py b/addons/cetmix_tower_git/models/cx_tower_file_template.py new file mode 100644 index 0000000..792c453 --- /dev/null +++ b/addons/cetmix_tower_git/models/cx_tower_file_template.py @@ -0,0 +1,32 @@ +# Copyright (C) 2024 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import api, fields, models + + +class CxTowerFileTemplate(models.Model): + _inherit = "cx.tower.file.template" + + git_project_ids = fields.Many2many( + comodel_name="cx.tower.git.project", + relation="cx_tower_git_project_file_template_rel", + column1="file_template_id", + column2="git_project_id", + string="Git Projects", + copy=False, + ) + git_project_id = fields.Many2one( + comodel_name="cx.tower.git.project", + compute="_compute_git_project_id", + ) + + @api.depends("git_project_ids") + def _compute_git_project_id(self): + """ + Link to project using the proxy model. + """ + for record in self: + # File is related to project via proxy model. + # So there can be only one record in o2m field. + record.git_project_id = ( + record.git_project_ids and record.git_project_ids[0].id + ) diff --git a/addons/cetmix_tower_git/models/cx_tower_git_project.py b/addons/cetmix_tower_git/models/cx_tower_git_project.py new file mode 100644 index 0000000..cd74c2d --- /dev/null +++ b/addons/cetmix_tower_git/models/cx_tower_git_project.py @@ -0,0 +1,368 @@ +# Copyright (C) 2024 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import re + +from odoo import _, api, fields, models + + +class CxTowerGitProject(models.Model): + """ + Git Project. + Implements pre-defined git configuration. + """ + + _name = "cx.tower.git.project" + _description = "Cetmix Tower Git Project" + _order = "name" + + _inherit = [ + "cx.tower.reference.mixin", + "cx.tower.yaml.mixin", + "cx.tower.access.role.mixin", + ] + + def _get_post_create_fields(self): + res = super()._get_post_create_fields() + return res + [ + "source_ids", + "git_project_rel_ids", + "git_project_file_template_rel_ids", + ] + + active = fields.Boolean(default=True) + server_ids = fields.Many2many( + comodel_name="cx.tower.server", + relation="cx_tower_git_project_server_rel", + readonly=True, + copy=False, + compute="_compute_server_ids", + store=True, + context={"active_test": False}, + help="Servers are added automatically based on the files" + " linked to the project.", + ) + source_ids = fields.One2many( + comodel_name="cx.tower.git.source", + inverse_name="git_project_id", + string="Sources", + auto_join=True, + copy=True, + ) + git_project_rel_ids = fields.One2many( + comodel_name="cx.tower.git.project.rel", + inverse_name="git_project_id", + string="Git Project Server File Relations", + copy=False, + ) + # Helper field to get all files related to git project + file_ids = fields.Many2many( + comodel_name="cx.tower.file", + relation="cx_tower_git_project_rel", + column1="git_project_id", + column2="file_id", + string="Files", + readonly=True, + depends=["git_project_rel_ids"], + copy=False, + ) + git_project_file_template_rel_ids = fields.One2many( + comodel_name="cx.tower.git.project.file.template.rel", + inverse_name="git_project_id", + string="Git Project File Template Relations", + copy=False, + ) + # Helper field to get all file templates related to git project + file_template_ids = fields.Many2many( + comodel_name="cx.tower.file.template", + relation="cx_tower_git_project_file_template_rel", + column1="git_project_id", + column2="file_template_id", + string="File Templates", + readonly=True, + depends=["git_project_file_template_rel_ids"], + copy=False, + ) + # Helper field to get all repositories used in this project + repo_ids = fields.Many2many( + comodel_name="cx.tower.git.repo", + relation="cx_tower_git_repo_project_rel", + column1="project_id", + column2="repo_id", + string="Repositories", + readonly=True, + copy=False, + help="Repositories used in this project through its sources and remotes", + ) + note = fields.Text() + + # ---- Access. Add relation for mixin fields + user_ids = fields.Many2many( + relation="cx_tower_git_project_user_rel", + compute="_compute_user_ids", + readonly=False, + store=True, + precompute=True, + ) + manager_ids = fields.Many2many( + relation="cx_tower_git_project_manager_rel", + compute="_compute_user_ids", + readonly=False, + store=True, + precompute=True, + ) + + # -- UI/UX fields + has_private_remotes = fields.Boolean( + compute="_compute_has_private_remotes", + help="Indicates if the project has any private remotes.", + ) + has_partially_private_remotes = fields.Boolean( + compute="_compute_has_private_remotes", + help="Indicates if the project has any partially private remotes.", + ) + + # -- Git Aggregator related fields + git_aggregator_root_dir = fields.Char( + help="Git aggregator root directory where sources will be cloned." + " Eg '/tmp/git-aggregator'" + " Will use '.' if not set", + ) + + def _selection_project_format(self): + """ + Possible project formats. + Inherit and extend when adding new project formats. + + Returns: + List of tuples: (code, name) + """ + return [ + ("git_aggregator", "Git Aggregator"), + ] + + def _default_project_format(self): + """ + Default project format. + """ + return "git_aggregator" + + @api.depends("git_project_rel_ids", "git_project_rel_ids.server_id") + def _compute_server_ids(self): + """Compute server ids for git projects. + + Why? Because a git project can be linked to multiple files + on the same server. + So we need to use a set to avoid duplicates so every server + is listed only once. + """ + for project in self: + project.server_ids = ( + list(set(project.git_project_rel_ids.server_id.ids)) + if project.git_project_rel_ids + else False + ) + + @api.depends( + "git_project_rel_ids.server_id", + "git_project_rel_ids.server_id.user_ids", + "git_project_rel_ids.server_id.manager_ids", + ) + def _compute_user_ids(self): + """ + Users. All users who have "Manager" group and are either set in "Users" + or in "Managers" in all related servers. + Managers. All users who have "Manager" group and are set as "Managers" + in all related servers. + + This is done to avoid unpredictable consequences when some of the servers + are not updated due to access restrictions when a project is updated. + """ + for project in self: + # Do not compute if no servers are related + server_ids = project.git_project_rel_ids.server_id + if not server_ids: + continue + + # Get all user and manager ids from related servers + all_user_ids = server_ids.user_ids.filtered( + lambda u: u.has_group("cetmix_tower_server.group_manager") + ).ids + all_manager_ids = server_ids.manager_ids.ids + + # Create a final list of user and manager ids + user_ids = [] + manager_ids = [] + # Check if user is present in all servers + for user_id in all_user_ids: + if all( + user_id in server.user_ids.ids or user_id in server.manager_ids.ids + for server in server_ids + ): + user_ids.append(user_id) + # Check if manager is present in all servers + for manager_id in all_manager_ids: + if all(manager_id in server.manager_ids.ids for server in server_ids): + manager_ids.append(manager_id) + + # Set the final lists + project.update( + { + "user_ids": [(6, 0, user_ids)], + "manager_ids": [(6, 0, manager_ids)], + } + ) + + @api.depends( + "source_ids", "source_ids.remote_ids", "source_ids.remote_ids.is_private" + ) + def _compute_has_private_remotes(self): + for project in self: + project.has_private_remotes = any( + source.remote_count > 0 + and source.remote_count_private == source.remote_count + for source in project.source_ids + ) + project.has_partially_private_remotes = any( + source.remote_count_private > 0 + and source.remote_count_private != source.remote_count + for source in project.source_ids + ) + + @api.model_create_multi + def create(self, vals_list): + res = super().create(vals_list) + # Update related files and templates on create + res._update_related_files_and_templates() + return res + + def write(self, vals): + res = super().write(vals) + # Update related files and templates on update + self._update_related_files_and_templates() + return res + + # ------------------------------ + # Helper methods + # ------------------------------ + def _update_related_files_and_templates(self): + # Update related files and templates + if self.git_project_rel_ids: + self.git_project_rel_ids._save_to_file() + if self.git_project_file_template_rel_ids: + self.git_project_file_template_rel_ids._save_to_file_template() + + def _extract_variables_from_text(self, text): + """Extract environment variables from text. + Helper method for file content generation. + + Args: + text (str): Text to extract variables from + Returns: + List: List of variables + """ + # This regex will find all variables where variables are denoted + # as $VAR or ${VAR}, e.g., $FOO or ${FOO_BAR123} + variables = re.findall(r"\$\{?([A-Z0-9_]+)\}?", text) + return sorted(list(set(variables))) + + def _compose_copy_name(self, server=False): + """ + Compose copy name of a git project copy. + Helper method used when creating a copy of a git project. + + Args: + server (cx.tower.server): Server to get the copy name for. + + Returns: + Char: Copy name + """ + self.ensure_one() + if server: + return server.name + return _("%(name)s (copy)", name=self.name) + + # ------------------------------ + # YAML mixin methods + # ------------------------------ + def _get_fields_for_yaml(self): + res = super()._get_fields_for_yaml() + res += [ + "name", + "note", + "source_ids", + "git_aggregator_root_dir", + ] + return res + + # ------------------------------- + # Git Aggregator related methods + # ------------------------------- + def _git_aggregator_prepare_record(self): + """Prepare json structure for git aggregator. + + Returns: + Dict: Json structure for git aggregator + """ + self.ensure_one() + values = {} + for source in self.source_ids: + if source.enabled and source.remote_count: + root_dir = self.git_aggregator_root_dir or "." + values.update( + { + f"/{source.reference}" + if root_dir == "/" + else f"{root_dir}/{source.reference}": source._git_aggregator_prepare_record() # noqa: E501 + } + ) + return values + + def _git_aggregator_prepare_yaml_comment(self, yaml_code): + """Generate commentary for yaml file. + It includes brief instructions for git aggregator + and lists environment variables that are required. + + Args: + yaml_code (str): Yaml code + + Returns: + Char: comment text or None + """ + + comment_text = _( + "# This file is generated with Cetmix Tower https://cetmix.com/tower\n" + "# It's designed to be used with git-aggregator tool developed by Acsone.\n" + "# Documentation for git-aggregator: https://github.com/acsone/git-aggregator\n" + ) + variable_list = self._extract_variables_from_text(yaml_code) + if variable_list: + comment_text += _( + "\n# You need to set the following variables in your environment:\n# %(vars)s\n" # noqa: E501 + "# and run git-aggregator with '--expand-env' parameter.\n", # noqa: E501 + vars=(", ".join(variable_list)), + ) + return comment_text + + def _generate_code_git_aggregator(self, record): + """Generate code in git-aggregator format. + + Args: + record (recordset()): Model record to generate code for. + must be a single record and have git_project_id field. + + Returns: + Text: Yaml code + """ + yaml_mixin = self.env["cx.tower.yaml.mixin"] + + # Do not generate code if record values are empty + record_values = record.git_project_id._git_aggregator_prepare_record() + if record_values: + yaml_code = yaml_mixin._convert_dict_to_yaml(record_values) + # Prepend comment to yaml code + comment = record.git_project_id._git_aggregator_prepare_yaml_comment( + yaml_code + ) + return f"{comment}\n{yaml_code}" + return "" diff --git a/addons/cetmix_tower_git/models/cx_tower_git_project_file_template_rel.py b/addons/cetmix_tower_git/models/cx_tower_git_project_file_template_rel.py new file mode 100644 index 0000000..906a7b9 --- /dev/null +++ b/addons/cetmix_tower_git/models/cx_tower_git_project_file_template_rel.py @@ -0,0 +1,115 @@ +# Copyright (C) 2024 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + + +class CxTowerGitProjectFileTemplateRel(models.Model): + """ + Relation between git projects and file templates. + """ + + _name = "cx.tower.git.project.file.template.rel" + _table = "cx_tower_git_project_file_template_rel" + _description = "Cetmix Tower Git Project relation to File Templates" + _log_access = False + + name = fields.Char(related="git_project_id.name", readonly=True) + git_project_id = fields.Many2one( + comodel_name="cx.tower.git.project", + index=True, + required=True, + ondelete="cascade", + ) + file_template_id = fields.Many2one( + comodel_name="cx.tower.file.template", + required=True, + ondelete="cascade", + ) + project_format = fields.Selection( + selection=lambda self: self.env[ + "cx.tower.git.project" + ]._selection_project_format(), + default=lambda self: self.env["cx.tower.git.project"]._default_project_format(), + required=True, + string="Format", + ) + + _sql_constraints = [ + ( + "project_server_file_template_format_uniq", + "unique(git_project_id, file_template_id, project_format)", + "File template is already related to the same project and format", + ), + ] + + @api.model_create_multi + def create(self, vals_list): + res = super().create(vals_list) + + # Export project to file + res._save_to_file_template() + return res + + def write(self, vals): + res = super().write(vals) + # Export project to file + self._save_to_file_template() + return res + + def action_open_file_template(self): + """ + Open file template record in current window + """ + self.ensure_one() + return { + "type": "ir.actions.act_window", + "name": self.file_template_id.name, + "res_model": "cx.tower.file.template", + "res_id": self.file_template_id.id, # pylint: disable=no-member + "view_mode": "form", + "view_type": "form", + "target": "current", + } + + # ---------------------------------------------------- + # Save project to linked file based on selected format + # ---------------------------------------------------- + def _save_to_file_template(self): + """Save project to linked file using format-specific function.""" + + # Get required function based on project format + # Following the pattern: _generate_code__ where format + # is one of the values in _selection_project_format + # Function gets a single record as an argument. + + # Save resolved functions to dict for faster access + code_generator_functions = {} + + for record in self: + code_generator_function = code_generator_functions.get( + record.project_format + ) + if not code_generator_function: + code_generator_function = getattr( + record.git_project_id, + f"_generate_code_{record.project_format}", + None, + ) + if not code_generator_function: + raise ValidationError( + _( + "Code generator function for '%(project_format)s'" + " format not found.", + project_format=record.project_format, + ) + ) + code_generator_functions[record.project_format] = ( + code_generator_function + ) + + # Generate code for current record + code = code_generator_function(record) + if record.file_template_id.code != code: + record.file_template_id.write({"code": code}) diff --git a/addons/cetmix_tower_git/models/cx_tower_git_project_rel.py b/addons/cetmix_tower_git/models/cx_tower_git_project_rel.py new file mode 100644 index 0000000..f8b6d97 --- /dev/null +++ b/addons/cetmix_tower_git/models/cx_tower_git_project_rel.py @@ -0,0 +1,179 @@ +# Copyright (C) 2024 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + + +class CxTowerGitProjectRel(models.Model): + """ + Relation between git projects and other model records. + """ + + _name = "cx.tower.git.project.rel" + _inherit = [ + "cx.tower.reference.mixin", + "cx.tower.yaml.mixin", + ] + _table = "cx_tower_git_project_rel" + _description = "Cetmix Tower Git Project relation to Files and Servers" + _log_access = False + + name = fields.Char(related="git_project_id.name", readonly=True) + git_project_id = fields.Many2one( + comodel_name="cx.tower.git.project", + index=True, + required=True, + ondelete="cascade", + ) + server_id = fields.Many2one( + comodel_name="cx.tower.server", + index=True, + required=True, + ondelete="cascade", + ) + file_id = fields.Many2one( + comodel_name="cx.tower.file", + domain="[('server_id', '=', server_id)," + "('source', '=', 'tower')," + "('file_type', '=', 'text')]", + required=True, + ondelete="cascade", + ) + project_format = fields.Selection( + selection=lambda self: self.env[ + "cx.tower.git.project" + ]._selection_project_format(), + default=lambda self: self.env["cx.tower.git.project"]._default_project_format(), + required=True, + string="Format", + ) + auto_sync = fields.Boolean(related="file_id.auto_sync", readonly=False) + + _sql_constraints = [ + ( + "project_server_file_format_uniq", + "unique(git_project_id, file_id, project_format)", + "File is already related to the same project and format", + ), + ] + + @api.constrains("server_id", "file_id") + def _check_server_file_relation(self): + """ + Check if server and file are related. + """ + for record in self: + if ( + record.file_id.server_id + and record.server_id != record.file_id.server_id + ): + raise ValidationError( + _( + "File '%(file)s' doesn't belong to server '%(server)s'", + file=record.file_id.name, + server=record.server_id.name, + ) + ) + + @api.model_create_multi + def create(self, vals_list): + res = super().create(vals_list) + + # Export project to file + res._save_to_file() + return res + + def write(self, vals): + res = super().write(vals) + # Export project to file + self._save_to_file() + return res + + def action_open_project(self): + """ + Open project record in current window + """ + self.ensure_one() + return { + "type": "ir.actions.act_window", + "name": self.name, + "res_model": "cx.tower.git.project", + "res_id": self.git_project_id.id, # pylint: disable=no-member + "view_mode": "form", + "view_type": "form", + "target": "current", + } + + def action_open_server(self): + """ + Open server record in current window + """ + self.ensure_one() + return { + "type": "ir.actions.act_window", + "name": self.server_id.name, + "res_model": "cx.tower.server", + "res_id": self.server_id.id, # pylint: disable=no-member + "view_mode": "form", + "view_type": "form", + "target": "current", + } + + # ---------------------------------------------------- + # Save project to linked file based on selected format + # ---------------------------------------------------- + def _save_to_file(self): + """Save project to linked file using format-specific function.""" + + # Get required function based on project format + # Following the pattern: _generate_code_ where format + # is one of the values in _selection_project_format + # Function gets a single record as an argument. + + # Save resolved functions to dict for faster access + code_generator_functions = {} + + for record in self: + # Disconnect file from file template if it is connected + if record.file_id.template_id: + record.file_id.action_unlink_from_template() + + code_generator_function = code_generator_functions.get( + record.project_format + ) + if not code_generator_function: + code_generator_function = getattr( + record.git_project_id, + f"_generate_code_{record.project_format}", + None, + ) + if not code_generator_function: + raise ValidationError( + _( + "Code generator function for '%(project_format)s'" + " format not found.", + project_format=record.project_format, + ) + ) + code_generator_functions[record.project_format] = ( + code_generator_function + ) + + # Generate code for current record + code = code_generator_function(record) + if record.file_id.code != code: + record.file_id.write({"code": code}) + + # ------------------------------ + # YAML mixin methods + # ------------------------------ + def _get_fields_for_yaml(self): + res = super()._get_fields_for_yaml() + res += [ + "file_id", + "git_project_id", + "project_format", + "auto_sync", + ] + return res diff --git a/addons/cetmix_tower_git/models/cx_tower_git_remote.py b/addons/cetmix_tower_git/models/cx_tower_git_remote.py new file mode 100644 index 0000000..1bcd408 --- /dev/null +++ b/addons/cetmix_tower_git/models/cx_tower_git_remote.py @@ -0,0 +1,403 @@ +# Copyright (C) 2024 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import giturlparse + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + + +class CxTowerGitRemote(models.Model): + """ + Git Remote. + Implements single git remote. + Eg a branch or a pull request. + """ + + _name = "cx.tower.git.remote" + _inherit = [ + "cx.tower.reference.mixin", + "cx.tower.yaml.mixin", + ] + _description = "Cetmix Tower Git Remote" + _order = "sequence, name" + + active = fields.Boolean(related="source_id.active", store=True, readonly=True) + enabled = fields.Boolean( + default=True, help="Enable in configuration and exported to files" + ) + sequence = fields.Integer(default=10) + name = fields.Char(compute="_compute_name", store=True, default="remote") + source_id = fields.Many2one( + comodel_name="cx.tower.git.source", + required=True, + ondelete="cascade", + auto_join=True, + ) + git_project_id = fields.Many2one( + comodel_name="cx.tower.git.project", + related="source_id.git_project_id", + store=True, + readonly=True, + ) + repo_id = fields.Many2one( + comodel_name="cx.tower.git.repo", + string="Repository", + required=True, + ondelete="restrict", + help="If selected, the remote URL will be filled from the" + " repo settings based on the remote protocol", + ) + repo_provider = fields.Selection( + related="repo_id.provider", + readonly=True, + ) + # -- Repo related fields + url_protocol = fields.Selection( + string="Protocol", + selection=[ + ("ssh", "SSH"), + ("https", "HTTPS"), + ("git", "GIT"), + ], + required=True, + default=lambda self: self._get_default_url_protocol(), + ) + is_private = fields.Boolean( + string="Private", + help="Repository is private", + related="repo_id.is_private", + store=True, + readonly=True, + ) + head_type = fields.Selection( + selection=[ + ("branch", "Branch"), + ("pr", "Pull/Merge Request"), + ("commit", "Commit"), + ], + required=True, + ) + head = fields.Char( + help="Git remote head. Link to branch, PR, commit or commit hash.", + required=True, + index=True, + ) + + def _get_default_url_protocol(self): + """Default URL protocol for new remote. + + Returns: + Char: Default URL protocol. + """ + return "https" + + @api.depends("source_id", "sequence") + def _compute_name(self): + """ + Compute remote name. + By default all remotes are named `remote_` + where position is the position of the remote in the source. + Eg first remote is `remote_1`, second is `remote_2`, etc. + """ + for remote in self: + if remote.source_id: + for index, source_remote in enumerate(remote.source_id.remote_ids): + source_remote.name = f"remote_{index + 1}" + + @api.onchange("head") + def onchange_head(self): + """ + Extract head number from head url + and set it as head. + """ + for remote in self: + if remote.head and "/" in remote.head: + remote.head = self._sanitize_head(remote.head) + + @api.model_create_multi + def create(self, vals_list): + # Sanitize head + for vals in vals_list: + head = vals.get("head") + if head and "/" in head: + vals["head"] = self._sanitize_head(head) + res = super().create(vals_list) + # Export project to related files and templates + res._update_related_files_and_templates() + return res + + def write(self, vals): + # Sanitize head + if "head" in vals: + head = vals["head"] + if head and "/" in head: + vals["head"] = self._sanitize_head(head) + res = super().write(vals) + # Update related files and templates on update + self._update_related_files_and_templates() + return res + + def unlink(self): + """ + Override to update related files and templates on unlink + """ + projects = self.git_project_id + res = super().unlink() + + # Update related files and templates on unlink + if projects: + file_relations = projects.git_project_rel_ids # type: ignore + if file_relations: + file_relations._save_to_file() + template_relations = projects.git_project_file_template_rel_ids # type: ignore + if template_relations: + template_relations._save_to_file_template() + return res + + def _sanitize_head(self, head): + """Sanitize head. + Extract head number from head url + and set it as head. + + Args: + head (Char): Head to sanitize + + Returns: + Char: Sanitized head + """ + if head and "/" in head: + return head.split("/")[-1].strip() + return head + + def _update_related_files_and_templates(self): + # Update related files on update + related_files = self.mapped("git_project_id").mapped("git_project_rel_ids") + if related_files: + related_files._save_to_file() + related_templates = self.mapped("git_project_id").mapped( + "git_project_file_template_rel_ids" + ) + if related_templates: + related_templates._save_to_file_template() + + # ------------------------------ + # Reference mixin methods + # ------------------------------ + def _get_pre_populated_model_data(self): + res = super()._get_pre_populated_model_data() + res.update({"cx.tower.git.remote": ["cx.tower.git.source", "source_id"]}) + return res + + # ------------------------------ + # YAML mixin methods + # ------------------------------ + def _get_fields_for_yaml(self): + res = super()._get_fields_for_yaml() + res += [ + "name", + "enabled", + "sequence", + "repo_id", + "head", + "head_type", + ] + return res + + # ------------------------------ + # Git Aggregator related methods + # ------------------------------ + def _git_aggregator_prepare_url(self): + """Prepare url for git aggregator + + Returns: + Char: Prepared url for git aggregator + """ + self.ensure_one() + + if not self.repo_id: + raise ValidationError(_("Repository is required")) + if not self.repo_id.url: + raise ValidationError(_("Repository URL is not set")) + + url = self.repo_id.url + prepared_url = giturlparse.parse(url).urls.get(self.url_protocol, url) + + # If repo is public or is not using HTTPS protocol return URL as is + if not self.is_private or self.url_protocol != "https": + return prepared_url + + if self.repo_provider == "github": + prepared_url = self._git_aggregator_prepare_url_github(prepared_url) + elif self.repo_provider == "gitlab": + prepared_url = self._git_aggregator_prepare_url_gitlab(prepared_url) + elif self.repo_provider == "bitbucket": + prepared_url = self._git_aggregator_prepare_url_bitbucket(prepared_url) + + return prepared_url + + def _git_aggregator_prepare_authenticated_url(self, url, auth_token): + """Helper to inject authentication token into HTTPS URL. + + Args: + url (Char): URL to prepare + auth_token (Char): Authentication token + + Returns: + Char: Prepared url for git aggregator + """ + url_without_protocol = url.replace("https://", "") + return f"https://{auth_token}@{url_without_protocol}" + + def _git_aggregator_prepare_url_github(self, url): + """ + Prepare url for git aggregator for private Github repo + using https protocol. + + Args: + url (Char): URL to prepare + + Returns: + Char: Prepared url for git aggregator + """ + self.ensure_one() + return self._git_aggregator_prepare_authenticated_url( + url, + "$GITHUB_TOKEN:x-oauth-basic", + ) + + def _git_aggregator_prepare_url_gitlab(self, url): + """ + Prepare url for git aggregator for private GitLab repo + using https protocol. + + Args: + url (Char): URL to prepare + + Returns: + Char: Prepared url for git aggregator + """ + self.ensure_one() + return self._git_aggregator_prepare_authenticated_url( + url, "$GITLAB_TOKEN_NAME:$GITLAB_TOKEN" + ) + + def _git_aggregator_prepare_url_bitbucket(self, url): + """ + Prepare url for git aggregator for private Bitbucket repo + using https protocol. + + Args: + url (Char): URL to prepare + + Returns: + Char: Prepared url for git aggregator + """ + self.ensure_one() + return self._git_aggregator_prepare_authenticated_url( + url, "x-token-auth:$BITBUCKET_TOKEN" + ) + + def _git_aggregator_prepare_head(self): + """Prepare head for git aggregator + + Returns: + Char: Prepared head for git aggregator + """ + self.ensure_one() + if self.repo_provider == "github": + return self._git_aggregator_prepare_head_github() + if self.repo_provider == "gitlab": + return self._git_aggregator_prepare_head_gitlab() + if self.repo_provider == "bitbucket": + return self._git_aggregator_prepare_head_bitbucket() + return self.head + + def _extract_head_number(self): + """ + Extract the last component from head + (branch name, PR number, or commit hash). + + Raises: + ValidationError: If head number is empty + + Returns: + Char: Extracted head number + """ + self.ensure_one() + head_number = self.head.split("/")[-1] + if not head_number: + raise ValidationError( + _("Git Aggregator: Head number is empty in %(head)s", head=self.head) + ) + return head_number + + def _git_aggregator_prepare_head_github(self): + """Prepare head for git aggregator for Github. + + Returns: + Char: Prepared head for git aggregator + """ + self.ensure_one() + head_number = self._extract_head_number() + # PR/MR + if self.head_type == "pr": + return f"refs/pull/{head_number}/head" + + # Commit + if self.head_type in ["commit", "branch"]: + return f"{head_number}" + + # Fallback to original head + return self.head + + def _git_aggregator_prepare_head_gitlab(self): + """Prepare head for git aggregator for GitLab. + + Returns: + Char: Prepared head for git aggregator + """ + self.ensure_one() + head_number = self._extract_head_number() + # PR/MR + if self.head_type == "pr": + return f"merge-requests/{head_number}/head" + + # Commit + # https://gitlab.com/cetmix/test/-/list/17.0-test-branch?ref_type=heads + if self.head_type in ["commit", "branch"]: + head_parts = head_number.split("?") + return f"{head_parts[0]}" + + # Fallback to original head + return self.head + + def _git_aggregator_prepare_head_bitbucket(self): + """Prepare head for git aggregator for Bitbucket. + + Returns: + Char: Prepared head for git aggregator + """ + self.ensure_one() + # PR/MR + if self.head_type == "pr": + raise ValidationError( + _( + "Git Aggregator: " + "Bitbucket does not support" + " fetching PRs. Please use branch instead.\n\n" + "Source: %(src)s\n" + "URL: %(url)s\n" + "Head: %(head)s", + src=self.source_id.name, + url=self.repo_id.url, + head=self.head, + ) + ) + + head_number = self._extract_head_number() + # Commit + if self.head_type in ["commit", "branch"]: + return f"{head_number}" + + # Fallback to original head + return self.head diff --git a/addons/cetmix_tower_git/models/cx_tower_git_repo.py b/addons/cetmix_tower_git/models/cx_tower_git_repo.py new file mode 100644 index 0000000..6e360a3 --- /dev/null +++ b/addons/cetmix_tower_git/models/cx_tower_git_repo.py @@ -0,0 +1,416 @@ +# Copyright (C) 2024 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import logging + +import giturlparse + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError +from odoo.tools import ormcache + +_logger = logging.getLogger(__name__) + + +class CxTowerGitRepo(models.Model): + """ + Git Repository. + Represents a git repository with its metadata and configuration. + """ + + _name = "cx.tower.git.repo" + _inherit = [ + "cx.tower.reference.mixin", + "cx.tower.yaml.mixin", + ] + _description = "Cetmix Tower Git Repository" + _order = "name" + _rec_names_search = ["repo", "host", "owner_id"] + + active = fields.Boolean(default=True, help="Indicates if the repository is active") + name = fields.Char( + compute="_compute_name", store=True, required=False, index="trigram" + ) + reference = fields.Char( + index=True, + compute="_compute_name", + required=False, + store=True, + ) + repo = fields.Char( + string="Repository Name", + readonly=True, + help="Repository name (e.g., 'cetmix-tower', 'odoo')", + ) + url = fields.Char( + string="Generic URL", + help="Displayed in 'https' format, but can be entered in any format", + compute="_compute_url", + inverse="_inverse_url", + required=True, + compute_sudo=True, + ) + url_ssh = fields.Char( + string="SSH URL", + help="SSH URL of the repository", + compute="_compute_url", + compute_sudo=True, + ) + url_git = fields.Char( + string="GIT URL", + help="GIT URL of the repository", + compute="_compute_url", + compute_sudo=True, + ) + is_private = fields.Boolean( + string="Private", default=False, help="Indicates if the repository is private" + ) + provider = fields.Selection( + selection="_selection_provider", + required=True, + default="other", + help="Repository provider to determine provider-based behaviour", + ) + host = fields.Char( + readonly=True, + index=True, + help="Repository host (e.g., 'github.com', 'gitlab.com')", + ) + owner_id = fields.Many2one( + comodel_name="cx.tower.git.repo.owner", + readonly=True, + help="Repository owner (e.g., 'cetmix' or 'OCA')", + ) + secret_id = fields.Many2one( + comodel_name="cx.tower.key", + string="Secret", + domain="[('key_type', '=', 's')]", + help="Custom secret used for this repository", + ) + remote_ids = fields.One2many( + comodel_name="cx.tower.git.remote", + inverse_name="repo_id", + help="Remotes that use this repository", + ) + git_project_ids = fields.Many2many( + comodel_name="cx.tower.git.project", + relation="cx_tower_git_repo_project_rel", + column1="repo_id", + column2="project_id", + compute="_compute_git_project_ids", + store=True, + help="Projects this repository is used in", + ) + remote_count = fields.Integer( + compute="_compute_remote_count", + help="Number of remotes this repository is used in", + ) + git_project_count = fields.Integer( + compute="_compute_git_project_count", + help="Number of projects this repository is used in", + ) + + _sql_constraints = [ + ( + "unique_repo_host_owner", + "unique(repo, host, owner_id)", + "A repository with the same name, host, and owner already exists.", + ), + ] + + # -- Selection + def _selection_provider(self): + """Available repository providers. + + Returns: + List of tuples: available options. + """ + return [ + ("github", "GitHub"), + ("gitlab", "GitLab"), + ("bitbucket", "Bitbucket"), + ("assembla", "Assembla"), + ("other", "Other"), + ] + + # -- Computes + @api.depends("host", "owner_id", "owner_id.name", "repo") + def _compute_name(self): + """ + Compute name in format: host/owner/name. + Compute reference based on name. + """ + for repo in self: + if repo.host and repo.owner_id and repo.repo: + name = f"{repo.host}/{repo.owner_id.name}/{repo.repo}" + reference = repo._generate_or_fix_reference(name) + repo.update( + { + "name": name, + "reference": reference, + } + ) + else: + repo.update( + { + "name": False, + "reference": False, + } + ) + + @api.depends("remote_ids", "remote_ids.git_project_id") + def _compute_git_project_ids(self): + """Compute projects this repository is used in.""" + for repo in self: + repo.git_project_ids = repo.remote_ids.git_project_id + + @api.depends("remote_ids") + def _compute_remote_count(self): + """Compute remote count field.""" + for repo in self: + repo.remote_count = len(repo.remote_ids) + + @api.depends("git_project_ids") + def _compute_git_project_count(self): + """Compute project count field.""" + for repo in self: + repo.git_project_count = len(repo.git_project_ids) + + @api.depends("repo", "host", "owner_id") + def _compute_url(self): + """Compute URL from repository properties.""" + for repo in self: + if repo.repo and repo.host and repo.owner_id: + https_url = f"https://{repo.host}/{repo.owner_id.name}/{repo.repo}.git" + elif repo.repo and repo.host: + https_url = f"https://{repo.host}/{repo.repo}.git" + else: + https_url = "" + if https_url: + try: + parsed_urls = giturlparse.parse(https_url).urls + urls = { + "url": https_url, + "url_ssh": parsed_urls["ssh"], + "url_git": parsed_urls["git"], + } + except Exception as e: # noqa: F841 catch all errors + _logger.error( + "Failed to parse constructed URL '%s' for repo %s", + https_url, + repo.display_name, + ) + urls = { + "url": "", + "url_ssh": "", + "url_git": "", + } + else: + urls = { + "url": "", + "url_ssh": "", + "url_git": "", + } + repo.update(urls) + + def _inverse_url(self): + """Parse URL to update repository properties.""" + for repo in self: + if not repo.url: + continue + # Parse URL + parsed_url_dict = self._parse_url(repo.url) + # Update repository properties + repo.update(parsed_url_dict) + + def action_view_remotes(self): + """Open remotes list view.""" + self.ensure_one() + action = self.env["ir.actions.actions"]._for_xml_id( + "cetmix_tower_git.action_cx_tower_git_remote" + ) + action.update( + { + "domain": [("repo_id", "=", self.id)], + "context": {"default_repo_id": self.id}, + } + ) + return action + + def action_view_projects(self): + """Open projects list view.""" + self.ensure_one() + action = self.env["ir.actions.actions"]._for_xml_id( + "cetmix_tower_git.cx_tower_git_project_action" + ) + action.update( + { + "domain": [("repo_ids", "in", self.id)], + "context": {"default_repo_ids": [(4, self.id)]}, + } + ) + return action + + @api.model_create_multi + def create(self, vals_list): + """Create multiple repositories.""" + # Check if any of the repositories already exist + # This is needed to allow creating repositories using just an URL. + # Eg when importing repositories from a YAML file. + res = self.browse() + existing_repo_ids = [] + vals_list_to_create = [] + for vals in vals_list: + url = vals.get("url") + if url: + # Try to get repository by URL + repo_id = self._get_repo_id_by_url( + url=url, create=False, raise_if_invalid=False + ) + if repo_id: + existing_repo_ids.append(repo_id) + continue + # Parse URL and update vals + parsed_url_dict = self._parse_url(url=url, raise_if_invalid=True) + vals.update(parsed_url_dict) + # Add to create list (with or without URL) + vals_list_to_create.append(vals) + # Compose the result + if vals_list_to_create: + res |= super().create(vals_list_to_create) + if existing_repo_ids: + res |= self.browse(existing_repo_ids) + self.env.registry.clear_cache() + return res + + def write(self, vals): + """Write repositories.""" + res = super().write(vals) + self.env.registry.clear_cache() + return res + + def unlink(self): + """Unlink repositories.""" + res = super().unlink() + self.env.registry.clear_cache() + return res + + @api.model + def name_create(self, name): + """ + Create a new repository from a URL. + """ + repo_id = self._get_repo_id_by_url(url=name, create=True, raise_if_invalid=True) + repo = self.browse(repo_id) + + return repo_id, repo.display_name + + @ormcache("self.env.uid", "self.env.su", "url", "create", "raise_if_invalid") + def _get_repo_id_by_url(self, url, create=False, raise_if_invalid=False): + """Get repository id by URL. + + Args: + url (Char): URL to get repository id + create (Bool, optional): Create repository if not found. + Default is False. + raise_if_invalid (Bool, optional): Raise ValidationError + if the URL is not valid. Default is False. + + Returns: + int: Repository ID + or False if the URL is not valid and raise_if_invalid is False + + Raises: + ValidationError: If the URL is not valid and raise_if_invalid is True + """ + # Parse URL + parsed_url_dict = self._parse_url(url, raise_if_invalid=raise_if_invalid) + if not parsed_url_dict: + return False + + # Check if repository already exists and use it + repo = self.search( + [ + ("repo", "=", parsed_url_dict["repo"]), + ("host", "=", parsed_url_dict["host"]), + ("owner_id", "=", parsed_url_dict["owner_id"]), + ], + limit=1, + ) + + # Otherwise, create a new one + if not repo and create: + repo = self.create(parsed_url_dict) + + return repo.id if repo else False + + def _parse_url(self, url, raise_if_invalid=True): + """Parse URL to get name, host and owner. + + Args: + url (Char): URL to parse + + Raises: + ValidationError: If the URL is not valid + + Returns: + Dict: Dictionary with name, host and owner + or empty dict if the URL is not valid and raise_if_invalid is False + """ + + # Validate URL + if not giturlparse.validate(url): + if raise_if_invalid: + raise ValidationError(_("Not a valid repository URL!")) + return {} + + # Parse URL + parsed_url = giturlparse.parse(url) + + # Get or create owner + owner_id = self.env["cx.tower.git.repo.owner"]._get_owner_id_by_name( + name=parsed_url.owner, + create=True, + ) + + # Get provider based on host + provider = self._get_provider(parsed_url) + + return { + "repo": parsed_url.repo, + "host": parsed_url.host, + "owner_id": owner_id, + "provider": provider, + } + + def _get_provider(self, parsed_url): + """Get provider. + + Args: + parsed_url (GitUrlParsed): Parsed URL object + + Returns: + str: Provider name + """ + provider = "other" + if parsed_url.assembla: + provider = "assembla" + elif parsed_url.bitbucket or "bitbucket" in parsed_url.host: + provider = "bitbucket" + elif parsed_url.gitlab: + provider = "gitlab" + elif parsed_url.github: + provider = "github" + + return provider + + # ------------------------------ + # YAML mixin methods + # ------------------------------ + def _get_fields_for_yaml(self): + res = super()._get_fields_for_yaml() + res += [ + "url", + "is_private", + "secret_id", + ] + return res diff --git a/addons/cetmix_tower_git/models/cx_tower_git_repo_owner.py b/addons/cetmix_tower_git/models/cx_tower_git_repo_owner.py new file mode 100644 index 0000000..3e0545a --- /dev/null +++ b/addons/cetmix_tower_git/models/cx_tower_git_repo_owner.py @@ -0,0 +1,107 @@ +# Copyright (C) 2024 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models +from odoo.tools import ormcache + + +class CxTowerGitRepoOwner(models.Model): + """ + Git Repository Owner. + Represents an organization or user that owns repositories. + Examples: "cetmix", "OCA", etc. + """ + + _name = "cx.tower.git.repo.owner" + _inherit = ["cx.tower.reference.mixin", "cx.tower.yaml.mixin"] + _description = "Cetmix Tower Git Repository Owner" + _order = "name" + + display_name = fields.Char( + readonly=False, compute="_compute_display_name", store=True + ) + + name = fields.Char( + help="Name of the repository owner (e.g., 'cetmix', 'OCA')", + ) + reference = fields.Char( + index=True, + compute="_compute_display_name", + required=False, + store=True, + ) + repo_ids = fields.One2many( + comodel_name="cx.tower.git.repo", + inverse_name="owner_id", + string="Repositories", + copy=False, + help="Repositories owned by this organization/user", + ) + secret_id = fields.Many2one( + comodel_name="cx.tower.key", + string="Secret", + domain="[('key_type', '=', 's')]", + help="Custom secret used for this repository owner", + ) + + @api.depends("name") + def _compute_display_name(self): + """Compute display name.""" + for owner in self: + # By default, display name is the same as name + name = owner.name + owner.update( + { + "display_name": name or False, + "reference": owner._generate_or_fix_reference(name) + if name + else False, + } + ) + + @ormcache("self.env.uid", "self.env.su", "name", "create") + def _get_owner_id_by_name(self, name, create=False): + """Get owner id by name. + + Args: + name (str): Owner name + create (bool): Create owner if not found + Returns: + int: Owner ID or None if not found + """ + owner = self.search([("name", "=ilike", name)], limit=1) if name else None + if not owner and create and name: + owner = self.create({"name": name}) + return owner.id if owner else None + + @api.model_create_multi + def create(self, vals_list): + """Clear cache on create.""" + res = super().create(vals_list) + self.env.registry.clear_cache() + return res + + def write(self, vals): + """Clear cache on write.""" + res = super().write(vals) + if "name" in vals: + self.env.registry.clear_cache() + return res + + def unlink(self): + """Clear cache on unlink.""" + res = super().unlink() + self.env.registry.clear_cache() + return res + + # ------------------------------ + # YAML mixin methods + # ------------------------------ + def _get_fields_for_yaml(self): + res = super()._get_fields_for_yaml() + res += [ + "display_name", + "name", + "secret_id", + ] + return res diff --git a/addons/cetmix_tower_git/models/cx_tower_git_source.py b/addons/cetmix_tower_git/models/cx_tower_git_source.py new file mode 100644 index 0000000..41a7fa1 --- /dev/null +++ b/addons/cetmix_tower_git/models/cx_tower_git_source.py @@ -0,0 +1,195 @@ +# Copyright (C) 2024 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import api, fields, models + + +class CxTowerGitSource(models.Model): + """ + Git Source. + Implements single git source. + Each source can include multiple remotes which can be + branches or pull requests of different repositories. + """ + + _name = "cx.tower.git.source" + _description = "Cetmix Tower Git Source" + + _inherit = [ + "cx.tower.reference.mixin", + "cx.tower.yaml.mixin", + ] + _order = "sequence, name" + + active = fields.Boolean(related="git_project_id.active", store=True, readonly=True) + enabled = fields.Boolean( + default=True, help="Enable in configuration and exported to files" + ) + name = fields.Char(required=False) + sequence = fields.Integer(default=10) + git_project_id = fields.Many2one( + comodel_name="cx.tower.git.project", + string="Git Configuration", + required=True, + ondelete="cascade", + auto_join=True, + ) + + remote_ids = fields.One2many( + comodel_name="cx.tower.git.remote", + inverse_name="source_id", + auto_join=True, + copy=True, + ) + remote_count = fields.Integer( + compute="_compute_remote_count", + string="Remotes", + ) + remote_count_private = fields.Integer( + compute="_compute_remote_count", + string="Private Remotes", + ) + + @api.depends("remote_ids", "remote_ids.enabled", "remote_ids.is_private") + def _compute_remote_count(self): + for record in self: + remote_count = private_remote_count = 0 + for remote in record.remote_ids: + if not remote.enabled: + continue + if remote.is_private: + private_remote_count += 1 + remote_count += 1 + record.update( + { + "remote_count": remote_count, + "remote_count_private": private_remote_count, + } + ) + + @api.model_create_multi + def create(self, vals_list): + res = super().create(vals_list) + # Update name + no_name = res.filtered(lambda s: not s.name) + if no_name: + no_name._compose_name() + # Update related files and templates on create + res._update_related_files_and_templates() + return res + + def write(self, vals): + res = super().write(vals) + # Compose name + if "name" in vals and not vals.get("name"): + self._compose_name() + # Update related files and templates on update + self._update_related_files_and_templates() + return res + + def unlink(self): + """ + Override to update related files and templates on unlink + """ + projects = self.git_project_id + res = super().unlink() + + # Update related files and templates on unlink + if projects: + file_relations = projects.git_project_rel_ids # type: ignore + if file_relations: + file_relations._save_to_file() + template_relations = projects.git_project_file_template_rel_ids # type: ignore + if template_relations: + template_relations._save_to_file_template() + return res + + def _compose_name(self): + """Compose name if not provided explicitly""" + for source in self: + if source.name: + continue + remote = fields.first(source.remote_ids) + if not remote: + source.name = "Empty Source" + continue + + remote_repo = remote.repo_id + if not remote_repo or not remote_repo.owner_id: + source.name = "Empty Source" + continue + source.name = f"{remote_repo.owner_id.name}/{remote_repo.repo}" + + def _update_related_files_and_templates(self): + # Update related files and templates on update + related_files = self.mapped("git_project_id").mapped("git_project_rel_ids") + if related_files: + related_files._save_to_file() + related_templates = self.mapped("git_project_id").mapped( + "git_project_file_template_rel_ids" + ) + if related_templates: + related_templates._save_to_file_template() + + # ------------------------------ + # Reference mixin methods + # ------------------------------ + def _get_pre_populated_model_data(self): + res = super()._get_pre_populated_model_data() + res.update({"cx.tower.git.source": ["cx.tower.git.project", "git_project_id"]}) + return res + + # ------------------------------ + # YAML mixin methods + # ------------------------------ + def _get_fields_for_yaml(self): + res = super()._get_fields_for_yaml() + res += [ + "name", + "enabled", + "sequence", + "remote_ids", + ] + return res + + # ------------------------------ + # Git Aggregator related methods + # ------------------------------ + def _git_aggregator_prepare_record(self): + """Prepare json structure for git aggregator. + + Returns: + Dict: Json structure for git aggregator + """ + self.ensure_one() + + # Prepare remotes, merges and target + remotes = {} + merges = [] + target = None + for remote in self.remote_ids: + if remote.enabled: + remotes.update({remote.name: remote._git_aggregator_prepare_url()}) + merges.append( + { + "remote": remote.name, + "ref": remote._git_aggregator_prepare_head(), + } + ) + # Set target to first remote name + if not target: + target = remote.name + + # If no remotes, return empty dict + if not remotes: + return {} + + vals = { + "remotes": remotes, + "merges": merges, + "target": target, + } + + # Fetch only first commit if there is only one remote + if len(remotes) == 1: + vals.update({"defaults": {"depth": 1}}) + return vals diff --git a/addons/cetmix_tower_git/models/cx_tower_plan_line.py b/addons/cetmix_tower_git/models/cx_tower_plan_line.py new file mode 100644 index 0000000..0c676c0 --- /dev/null +++ b/addons/cetmix_tower_git/models/cx_tower_plan_line.py @@ -0,0 +1,32 @@ +# Copyright (C) 2025 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class CxTowerPlanLine(models.Model): + """Flight Plan Line""" + + _inherit = "cx.tower.plan.line" + + git_project_id = fields.Many2one( + comodel_name="cx.tower.git.project", + string="Git Project", + help="Select a git project to be linked to the file and server.", + ) + is_make_copy = fields.Boolean( + string="Make a Copy", + help="Create a copy of the Git Project instead of linking " + "the file to the existing one.", + ) + + # ------------------------------ + # YAML mixin methods + # ------------------------------ + def _get_fields_for_yaml(self): + res = super()._get_fields_for_yaml() + res += [ + "git_project_id", + "is_make_copy", + ] + return res diff --git a/addons/cetmix_tower_git/models/cx_tower_server.py b/addons/cetmix_tower_git/models/cx_tower_server.py new file mode 100644 index 0000000..55c9c56 --- /dev/null +++ b/addons/cetmix_tower_git/models/cx_tower_server.py @@ -0,0 +1,187 @@ +# Copyright (C) 2024 Cetmix OÜ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging + +from odoo import api, fields, models + +_logger = logging.getLogger(__name__) + + +class CxTowerServer(models.Model): + _inherit = "cx.tower.server" + + git_project_rel_ids = fields.One2many( + comodel_name="cx.tower.git.project.rel", + inverse_name="server_id", + copy=False, + depends=["git_project_ids"], + groups="cetmix_tower_server.group_manager,cetmix_tower_server.group_root", + ) + + # Helper field to get all git projects related to server + # IMPORTANT: This field may contain duplicates because of the relation nature! + git_project_ids = fields.Many2many( + comodel_name="cx.tower.git.project", + relation="cx_tower_git_project_rel", + column1="server_id", + column2="git_project_id", + readonly=True, + copy=False, + depends=["git_project_rel_ids"], + groups="cetmix_tower_server.group_manager,cetmix_tower_server.group_root", + ) + + # ------------------------------ + # YAML mixin methods + # ------------------------------ + def _get_fields_for_yaml(self): + res = super()._get_fields_for_yaml() + res += [ + "git_project_rel_ids", + ] + return res + + def _get_force_x2m_resolve_models(self): + res = super()._get_force_x2m_resolve_models() + + # Add File in order to always try to use existing one + res += ["cx.tower.file"] + return res + + def _update_or_create_related_record( + self, model, reference, values, create_immediately=False + ): + # Files must be created immediately because they are related + # to both server and git project. + # So if a file is not created immediately when it is created + # for the server, the same file will be created for the git project. + # This will lead to creation of two files with the same content + # for the same server. + + if model._name == "cx.tower.file": + create_immediately = True + return super()._update_or_create_related_record( + model, reference, values, create_immediately=create_immediately + ) + + @api.model + def get_servers_by_git_ref(self, repository_url, head=None, head_type=None): + """ + Return servers linked to a given Git repository reference. + + Parameters + ---------- + repository_url : str + Pre-normalized canonical Git URL + (e.g. ``https://host/owner/repo.git``). + head : str, optional + Branch name, commit SHA, or PR identifier. + head_type : {'branch', 'commit', 'pr'}, optional + Type of the ``head`` argument. + If only ``head`` is provided, it will match across all head types. + If only ``head_type`` is provided, it will filter by type regardless of head + + Returns + ------- + recordset of cx.tower.server + Matching servers. Empty recordset if no matches. + """ + + server_obj = self.env["cx.tower.server"] + # URL MUST be already canonical. + if not repository_url: + return server_obj + + # Get repository id by URL + repo_id = self.env["cx.tower.git.repo"]._get_repo_id_by_url( + repository_url, raise_if_invalid=False + ) + if not repo_id: + return server_obj + repo = self.env["cx.tower.git.repo"].browse(repo_id) + + # Compose domain for remotes + remote_domain = [ + ("source_id.enabled", "=", True), + ("enabled", "=", True), + ] + if head: + head = self.env["cx.tower.git.remote"]._sanitize_head(head) + remote_domain.append(("head", "=", head)) + if head_type: + remote_domain.append(("head_type", "=", head_type)) + + # Get remotes + remotes = repo.remote_ids.filtered_domain(remote_domain) + if not remotes: + return server_obj + + # Get servers from remotes + servers = remotes.mapped("git_project_id.git_project_rel_ids.server_id") + return servers + + def _command_runner_file_using_template_create_file( + self, + log_record, + server_dir, + **kwargs, + ): + """Override to create git project relation + when creating a file using a template. + """ + file = super()._command_runner_file_using_template_create_file( + log_record, server_dir, **kwargs + ) + if file: + # Get the flight plan line from log record + plan_line = log_record.plan_log_id.plan_line_executed_id + # Try to get git project from custom values + custom_values = log_record.variable_values + git_project_reference = custom_values and custom_values.get( + "__git_project__" + ) + if git_project_reference: + git_project = self.env["cx.tower.git.project"].get_by_reference( + git_project_reference + ) + if not git_project: + _logger.warning( + "Git project '%s' provided with the `__git_project__` " + "custom value not found for server '%s' " + "in flight plan line '%s' " + "of the flight plan '%s'. " + "No project was linked to the file '%s'.", + git_project_reference, + self.name, + plan_line.name, + log_record.plan_log_id.plan_id.name, + file.name, + ) + + # Try to get git project set explicitly in the flight plan line + else: + git_project = plan_line.git_project_id + if not git_project: + return file + + if plan_line.is_make_copy: + # Remove default_server_ids from context, because this relation + # will be created through git_project_rel_ids. + # default_server_ids will interfere at the moment when + # pairs of values are created through SQL query + # in the method write_real and it does not take into account + # that in this case we are creating a copy of the git project + git_project = git_project.with_context(default_server_ids=False).copy( + {"name": git_project._compose_copy_name(server=self)} + ) + + self.env["cx.tower.git.project.rel"].create( + { + "git_project_id": git_project.id, + "server_id": self.id, + "file_id": file.id, + "project_format": git_project._default_project_format(), + } + ) + return file diff --git a/addons/cetmix_tower_git/pyproject.toml b/addons/cetmix_tower_git/pyproject.toml new file mode 100644 index 0000000..4231d0c --- /dev/null +++ b/addons/cetmix_tower_git/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/addons/cetmix_tower_git/readme/CONFIGURE.md b/addons/cetmix_tower_git/readme/CONFIGURE.md new file mode 100644 index 0000000..8c717e5 --- /dev/null +++ b/addons/cetmix_tower_git/readme/CONFIGURE.md @@ -0,0 +1 @@ +Please refer to the [official documentation](https://cetmix.com/tower) for detailed configuration instructions. diff --git a/addons/cetmix_tower_git/readme/DESCRIPTION.md b/addons/cetmix_tower_git/readme/DESCRIPTION.md new file mode 100644 index 0000000..19da7e3 --- /dev/null +++ b/addons/cetmix_tower_git/readme/DESCRIPTION.md @@ -0,0 +1,3 @@ +This module implements Git Management functionality for [Cetmix Tower](https://cetmix.com/tower). + +Please refer to the [official documentation](https://cetmix.com/tower) for detailed information. diff --git a/addons/cetmix_tower_git/readme/HISTORY.md b/addons/cetmix_tower_git/readme/HISTORY.md new file mode 100644 index 0000000..c9e6096 --- /dev/null +++ b/addons/cetmix_tower_git/readme/HISTORY.md @@ -0,0 +1,10 @@ +## 18.0.1.0.2 (2026-03-10) + +- Features: Provide git project name using the `__git_project__` custom value when creating a project in flight plan. Improve the UI and UX of Git Projects. (5197) + +- Bugfixes: Link server to git project only once. (5214) + + +## 18.0.1.0.1 (2025-12-17) + +- Features: Improve search views, implement the search panel for selected views. (5139) diff --git a/addons/cetmix_tower_git/readme/USAGE.md b/addons/cetmix_tower_git/readme/USAGE.md new file mode 100644 index 0000000..901f5a6 --- /dev/null +++ b/addons/cetmix_tower_git/readme/USAGE.md @@ -0,0 +1 @@ +Please refer to the [official documentation](https://cetmix.com/tower) for detailed usage instructions. diff --git a/addons/cetmix_tower_git/readme/newsfragments/.gitkeep b/addons/cetmix_tower_git/readme/newsfragments/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/addons/cetmix_tower_git/security/cx_tower_git_project_file_template_rel_security.xml b/addons/cetmix_tower_git/security/cx_tower_git_project_file_template_rel_security.xml new file mode 100644 index 0000000..c01bc75 --- /dev/null +++ b/addons/cetmix_tower_git/security/cx_tower_git_project_file_template_rel_security.xml @@ -0,0 +1,46 @@ + + + + + Git Project File Template Relation: Manager Read Access + + ['&', + '|', + ('git_project_id.user_ids', 'in', [user.id]), + ('git_project_id.manager_ids', 'in', [user.id]), + '|', + ('file_template_id.user_ids', 'in', [user.id]), + ('file_template_id.manager_ids', 'in', [user.id])] + + + + + + + + + + Git Project File Template Relation: Manager Write Access + + [ + ('git_project_id.manager_ids', 'in', [user.id]), + ('file_template_id.manager_ids', 'in', [user.id])] + + + + + + + + + + Git Project File Template Relation: Root Full Access + + [(1, '=', 1)] + + + diff --git a/addons/cetmix_tower_git/security/cx_tower_git_project_rel_security.xml b/addons/cetmix_tower_git/security/cx_tower_git_project_rel_security.xml new file mode 100644 index 0000000..ff23797 --- /dev/null +++ b/addons/cetmix_tower_git/security/cx_tower_git_project_rel_security.xml @@ -0,0 +1,43 @@ + + + + + Git Project Relation: Manager Read Access + + ['&', + '|', + ('git_project_id.user_ids', 'in', [user.id]), + ('git_project_id.manager_ids', 'in', [user.id]), + '|', + ('server_id.user_ids', 'in', [user.id]), + ('server_id.manager_ids', 'in', [user.id])] + + + + + + + + + + Git Project Relation: Manager Create/Write/Delete Access + + [('git_project_id.manager_ids', 'in', [user.id]), + ('server_id.manager_ids', 'in', [user.id])] + + + + + + + + + + Git Project Relation: Root Full Access + + [(1, '=', 1)] + + + diff --git a/addons/cetmix_tower_git/security/cx_tower_git_project_security.xml b/addons/cetmix_tower_git/security/cx_tower_git_project_security.xml new file mode 100644 index 0000000..b5aab7b --- /dev/null +++ b/addons/cetmix_tower_git/security/cx_tower_git_project_security.xml @@ -0,0 +1,64 @@ + + + + + Git Project: Manager Read Access + + ['|', ('user_ids', 'in', [user.id]), ('manager_ids', 'in', [user.id])] + + + + + + + + + + Git Project: Manager Read Access via Server + + ['|', + ('git_project_rel_ids.server_id.user_ids', 'in', [user.id]), + ('git_project_rel_ids.server_id.manager_ids', 'in', [user.id])] + + + + + + + + + + Git Project: Manager Write Access + + [('manager_ids', 'in', [user.id])] + + + + + + + + + + Git Project: Manager Delete Access + + [('manager_ids', 'in', [user.id]), ('create_uid', '=', user.id)] + + + + + + + + + + Git Project: Root Full Access + + [(1, '=', 1)] + + + diff --git a/addons/cetmix_tower_git/security/cx_tower_git_remote_security.xml b/addons/cetmix_tower_git/security/cx_tower_git_remote_security.xml new file mode 100644 index 0000000..fc1a5dd --- /dev/null +++ b/addons/cetmix_tower_git/security/cx_tower_git_remote_security.xml @@ -0,0 +1,70 @@ + + + + + Git Remote: Manager Read Access + + ['|', ('git_project_id.user_ids', 'in', [user.id]), ('git_project_id.manager_ids', 'in', [user.id])] + + + + + + + + + + Git Remote: Manager Read Access via Server + + ['|', + ('git_project_id.git_project_rel_ids.server_id.user_ids', 'in', [user.id]), + ('git_project_id.git_project_rel_ids.server_id.manager_ids', 'in', [user.id])] + + + + + + + + + + Git Remote: Manager Write/Create Access + + [('git_project_id.manager_ids', 'in', [user.id])] + + + + + + + + + + Git Remote: Manager Delete Access + + [('git_project_id.manager_ids', 'in', [user.id]), ('create_uid', '=', user.id)] + + + + + + + + + + Git Remote: Root Full Access + + [(1, '=', 1)] + + + + + + + diff --git a/addons/cetmix_tower_git/security/cx_tower_git_repo_owner_security.xml b/addons/cetmix_tower_git/security/cx_tower_git_repo_owner_security.xml new file mode 100644 index 0000000..6106223 --- /dev/null +++ b/addons/cetmix_tower_git/security/cx_tower_git_repo_owner_security.xml @@ -0,0 +1,24 @@ + + + + + + + Git Repository Owner: Manager Write/Create Access + + [('create_uid', '=', user.id)] + + + + + + + + + + Git Repository Owner: Root Full Access + + [(1, '=', 1)] + + + diff --git a/addons/cetmix_tower_git/security/cx_tower_git_repo_security.xml b/addons/cetmix_tower_git/security/cx_tower_git_repo_security.xml new file mode 100644 index 0000000..ff59427 --- /dev/null +++ b/addons/cetmix_tower_git/security/cx_tower_git_repo_security.xml @@ -0,0 +1,24 @@ + + + + + + + Git Repository: Manager Write/Create Access + + [('create_uid', '=', user.id)] + + + + + + + + + + Git Repository: Root Full Access + + [(1, '=', 1)] + + + diff --git a/addons/cetmix_tower_git/security/cx_tower_git_source_security.xml b/addons/cetmix_tower_git/security/cx_tower_git_source_security.xml new file mode 100644 index 0000000..706645a --- /dev/null +++ b/addons/cetmix_tower_git/security/cx_tower_git_source_security.xml @@ -0,0 +1,72 @@ + + + + + + + Git Source: Manager Read Access + + ['|', ('git_project_id.user_ids', 'in', [user.id]), ('git_project_id.manager_ids', 'in', [user.id])] + + + + + + + + + + Git Source: Manager Read Access via Server + + ['|', + ('git_project_id.git_project_rel_ids.server_id.user_ids', 'in', [user.id]), + ('git_project_id.git_project_rel_ids.server_id.manager_ids', 'in', [user.id])] + + + + + + + + + + Git Source: Manager Write/Create Access + + [('git_project_id.manager_ids', 'in', [user.id])] + + + + + + + + + + Git Source: Manager Delete Access + + [('git_project_id.manager_ids', 'in', [user.id]), ('create_uid', '=', user.id)] + + + + + + + + + + Git Source: Root Full Access + + [(1, '=', 1)] + + + + + + + diff --git a/addons/cetmix_tower_git/security/ir.model.access.csv b/addons/cetmix_tower_git/security/ir.model.access.csv new file mode 100644 index 0000000..750134f --- /dev/null +++ b/addons/cetmix_tower_git/security/ir.model.access.csv @@ -0,0 +1,15 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_git_config_manager,Git Config Manager,model_cx_tower_git_project,cetmix_tower_server.group_manager,1,1,1,1 +access_git_config_root,Git Config Root,model_cx_tower_git_project,cetmix_tower_server.group_root,1,1,1,1 +access_git_source_manager,Git Source Manager,model_cx_tower_git_source,cetmix_tower_server.group_manager,1,1,1,1 +access_git_source_root,Git Source Root,model_cx_tower_git_source,cetmix_tower_server.group_root,1,1,1,1 +access_git_remote_manager,Git Remote Manager,model_cx_tower_git_remote,cetmix_tower_server.group_manager,1,1,1,1 +access_git_remote_root,Git Remote Root,model_cx_tower_git_remote,cetmix_tower_server.group_root,1,1,1,1 +access_git_repo_manager,Git Repository Manager,model_cx_tower_git_repo,cetmix_tower_server.group_manager,1,1,1,1 +access_git_repo_root,Git Repository Root,model_cx_tower_git_repo,cetmix_tower_server.group_root,1,1,1,1 +access_git_repo_owner_manager,Git Repository Owner Manager,model_cx_tower_git_repo_owner,cetmix_tower_server.group_manager,1,1,1,0 +access_git_repo_owner_root,Git Repository Owner Root,model_cx_tower_git_repo_owner,cetmix_tower_server.group_root,1,1,1,1 +access_git_project_server_file_rel,Git Project Server File Rel Manager,model_cx_tower_git_project_rel,cetmix_tower_server.group_manager,1,1,1,1 +access_git_project_server_file_rel_root,Git Project Server File Rel Root,model_cx_tower_git_project_rel,cetmix_tower_server.group_root,1,1,1,1 +access_git_project_file_template_rel,Git Project File Template Rel Manager,model_cx_tower_git_project_file_template_rel,cetmix_tower_server.group_manager,1,1,1,1 +access_git_project_file_template_rel_root,Git Project File Template Rel Root,model_cx_tower_git_project_file_template_rel,cetmix_tower_server.group_root,1,1,1,1 diff --git a/addons/cetmix_tower_git/static/description/banner.png b/addons/cetmix_tower_git/static/description/banner.png new file mode 100644 index 0000000..48d0b50 Binary files /dev/null and b/addons/cetmix_tower_git/static/description/banner.png differ diff --git a/addons/cetmix_tower_git/static/description/icon.png b/addons/cetmix_tower_git/static/description/icon.png new file mode 100644 index 0000000..2507f55 Binary files /dev/null and b/addons/cetmix_tower_git/static/description/icon.png differ diff --git a/addons/cetmix_tower_git/static/description/index.html b/addons/cetmix_tower_git/static/description/index.html new file mode 100644 index 0000000..cc0aa42 --- /dev/null +++ b/addons/cetmix_tower_git/static/description/index.html @@ -0,0 +1,450 @@ + + + + + +Cetmix Tower Git + + + +
+

Cetmix Tower Git

+ + +

Beta License: AGPL-3 cetmix/cetmix-tower

+

This module implements Git Management functionality for Cetmix +Tower.

+

Please refer to the official +documentation for detailed information.

+

Table of contents

+ +
+

Configuration

+

Please refer to the official +documentation for detailed configuration +instructions.

+
+
+

Usage

+

Please refer to the official +documentation for detailed usage +instructions.

+
+
+

Changelog

+
+

18.0.1.0.2 (2026-03-10)

+
    +
  • Features: Provide git project name using the __git_project__ +custom value when creating a project in flight plan. Improve the UI +and UX of Git Projects. (5197)
  • +
  • Bugfixes: Link server to git project only once. (5214)
  • +
+
+
+

18.0.1.0.1 (2025-12-17)

+
    +
  • Features: Improve search views, implement the search panel for +selected views. (5139)
  • +
+
+
+
+

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

+
    +
  • Cetmix
  • +
+
+
+

Maintainers

+

This module is part of the cetmix/cetmix-tower project on GitHub.

+

You are welcome to contribute.

+
+
+
+ + diff --git a/addons/cetmix_tower_git/tests/__init__.py b/addons/cetmix_tower_git/tests/__init__.py new file mode 100644 index 0000000..97c9645 --- /dev/null +++ b/addons/cetmix_tower_git/tests/__init__.py @@ -0,0 +1,7 @@ +from . import test_remote +from . import test_source +from . import test_project +from . import test_file_rel +from . import test_file_template_rel +from . import test_server +from . import test_repo diff --git a/addons/cetmix_tower_git/tests/common.py b/addons/cetmix_tower_git/tests/common.py new file mode 100644 index 0000000..b3c9643 --- /dev/null +++ b/addons/cetmix_tower_git/tests/common.py @@ -0,0 +1,136 @@ +from odoo.addons.cetmix_tower_server.tests.common import TestTowerCommon + + +class CommonTest(TestTowerCommon): + """Common test class for all tests.""" + + @classmethod + def setUpClass(cls): + super().setUpClass() + + # Models + cls.GitProject = cls.env["cx.tower.git.project"] + cls.GitProjectRel = cls.env["cx.tower.git.project.rel"] + cls.GitProjectFileTemplateRel = cls.env[ + "cx.tower.git.project.file.template.rel" + ] + cls.GitSource = cls.env["cx.tower.git.source"] + cls.GitRemote = cls.env["cx.tower.git.remote"] + + # Data + # Project + cls.git_project_1 = cls.GitProject.create({"name": "Git Project 1"}) + + # Sources + cls.git_source_1 = cls.GitSource.create( + {"name": "Git Source 1", "git_project_id": cls.git_project_1.id} + ) + cls.git_source_2 = cls.GitSource.create( + {"name": "Git Source 2", "git_project_id": cls.git_project_1.id} + ) + # Repositories + cls.Repo = cls.env["cx.tower.git.repo"] + cls.RepoOwner = cls.env["cx.tower.git.repo.owner"] + + cls.repo_cetmix_tower = cls.Repo.create( + { + "name": "Cetmix Tower", + "url": "https://github.com/cetmix-test/cetmix-tower-test.git", + } + ) + cls.repo_oca_web = cls.Repo.create( + { + "name": "OCA Web", + "url": "https://github.com/oca-test/web-test.git", + } + ) + cls.repo_odoo_enterprise = cls.Repo.create( + { + "name": "Odoo Enterprise", + "url": "https://github.com/odoo-test/enterprise-test.git", + "is_private": True, + } + ) + cls.repo_gitlab_private = cls.Repo.create( + { + "name": "GitLab Private", + "url": "git@my.gitlab.com:cetmix-test/cetmix-tower-test.git", + "is_private": True, + } + ) + cls.repo_bitbucket_private = cls.Repo.create( + { + "name": "Bitbucket Private", + "url": "https://bitbucket.com/cetmix-test/cetmix-tower-test-enterprise.git", + "is_private": True, + } + ) + + # Same urls, different protocols (intentionally aliased) + cls.repo_other_ssh = cls.Repo.create( + {"url": "git@memegit.com:cetmix-test/cetmix-tower-test.git"} + ) + cls.repo_other_https = cls.repo_other_ssh + + # Remotes + cls.remote_github_https = cls.GitRemote.create( + { + "repo_id": cls.repo_cetmix_tower.id, + "source_id": cls.git_source_1.id, + "head_type": "pr", + "head": "https://github.com/cetmix-test/cetmix-tower-test/pull/123", + "sequence": 1, + } + ) + cls.remote_gitlab_https = cls.GitRemote.create( + { + "repo_id": cls.repo_gitlab_private.id, + "source_id": cls.git_source_1.id, + "head_type": "branch", + "head": "main", + "sequence": 2, + } + ) + cls.remote_gitlab_ssh = cls.GitRemote.create( + { + "repo_id": cls.repo_gitlab_private.id, + "source_id": cls.git_source_1.id, + "head_type": "commit", + "url_protocol": "ssh", + "head": "10000000", + "sequence": 3, + } + ) + cls.remote_bitbucket_https = cls.GitRemote.create( + { + "repo_id": cls.repo_bitbucket_private.id, + "source_id": cls.git_source_2.id, + "head_type": "branch", + "head": "dev", + "sequence": 4, + } + ) + cls.remote_other_ssh = cls.GitRemote.create( + { + "repo_id": cls.repo_other_ssh.id, + "source_id": cls.git_source_2.id, + "head_type": "branch", + "url_protocol": "ssh", + "head": "old", + "sequence": 5, + } + ) + + # File + cls.server_1_file_1 = cls.File.create( + { + "name": "File 1", + "server_id": cls.server_test_1.id, + "source": "tower", + } + ) + cls.file_template_1 = cls.FileTemplate.create( + { + "name": "File Template 1", + } + ) diff --git a/addons/cetmix_tower_git/tests/test_file_rel.py b/addons/cetmix_tower_git/tests/test_file_rel.py new file mode 100644 index 0000000..a958341 --- /dev/null +++ b/addons/cetmix_tower_git/tests/test_file_rel.py @@ -0,0 +1,390 @@ +from odoo.exceptions import AccessError + +from .common import CommonTest + + +class TestFileRel(CommonTest): + """Test class for git file relation.""" + + def setUp(self): + super().setUp() + self.file_1_rel = self.GitProjectRel.create( + { + "server_id": self.server_test_1.id, + "file_id": self.server_1_file_1.id, + "git_project_id": self.git_project_1.id, + "project_format": "git_aggregator", + } + ) + + def test_file_rel_create(self): + """Test if file relation is created correctly""" + + # -- 1 -- + # Check if file content is updated + + # Get code from project + yaml_code_from_project = ( + self.file_1_rel.git_project_id._generate_code_git_aggregator( + self.file_1_rel + ) + ) + + self.assertEqual( + self.server_1_file_1.code, + yaml_code_from_project, + "File content is not updated correctly", + ) + + # Check specific if remote is present in file + self.assertIn( + self.remote_other_ssh.repo_id.url_ssh, + self.server_1_file_1.code, + "Remote is not present in file", + ) + + # -- 2 -- + # Modify remove and check if file content is updated + self.remote_other_ssh.repo_id = self.Repo.create( + { + "url": "https://github.com/cetmix/cetmix-memes.git", + } + ) + self.remote_other_ssh.url_protocol = "https" + + # Must be different from previous project code + self.assertNotEqual( + self.server_1_file_1.code, + yaml_code_from_project, + "File content is not updated correctly", + ) + # New remote must be present in file + self.assertIn( + "https://github.com/cetmix/cetmix-memes.git", + self.server_1_file_1.code, + "Remote is not present in file", + ) + + # -- 3 -- + # Disable source and check if file content is updated + self.git_source_2.active = False + self.assertNotIn( + "https://github.com/cetmix/cetmix-memes.git", + self.server_1_file_1.code, + "Remote is present in file", + ) + + def test_format_git_aggregator(self): + """Test if format git aggregator works correctly""" + + # -- 1 -- + # Check if YAML code is generated correctly + + yaml_code = """# This file is generated with Cetmix Tower https://cetmix.com/tower +# It's designed to be used with git-aggregator tool developed by Acsone. +# Documentation for git-aggregator: https://github.com/acsone/git-aggregator + +# You need to set the following variables in your environment: +# BITBUCKET_TOKEN, GITLAB_TOKEN, GITLAB_TOKEN_NAME +# and run git-aggregator with '--expand-env' parameter. + +./git_project_1_git_source_1: + remotes: + remote_1: https://github.com/cetmix-test/cetmix-tower-test.git + remote_2: https://$GITLAB_TOKEN_NAME:$GITLAB_TOKEN@my.gitlab.com/cetmix-test/cetmix-tower-test.git + remote_3: git@my.gitlab.com:cetmix-test/cetmix-tower-test.git + merges: + - remote: remote_1 + ref: refs/pull/123/head + - remote: remote_2 + ref: main + - remote: remote_3 + ref: '10000000' + target: remote_1 +./git_project_1_git_source_1_2: + remotes: + remote_1: https://x-token-auth:$BITBUCKET_TOKEN@bitbucket.com/cetmix-test/cetmix-tower-test-enterprise.git + remote_2: git@memegit.com:cetmix-test/cetmix-tower-test.git + merges: + - remote: remote_1 + ref: dev + - remote: remote_2 + ref: old + target: remote_1 +""" # noqa: E501 + + # Get code from project + yaml_code_from_project = ( + self.file_1_rel.git_project_id._generate_code_git_aggregator( + self.file_1_rel + ) + ) + self.assertEqual( + yaml_code_from_project, + yaml_code, + "YAML code is not generated correctly", + ) + + # -- 2 -- + # Unlink remote and check if file content is updated + self.remote_github_https.unlink() + yaml_code_from_project = ( + self.file_1_rel.git_project_id._generate_code_git_aggregator( + self.file_1_rel + ) + ) + yaml_code = """# This file is generated with Cetmix Tower https://cetmix.com/tower +# It's designed to be used with git-aggregator tool developed by Acsone. +# Documentation for git-aggregator: https://github.com/acsone/git-aggregator + +# You need to set the following variables in your environment: +# BITBUCKET_TOKEN, GITLAB_TOKEN, GITLAB_TOKEN_NAME +# and run git-aggregator with '--expand-env' parameter. + +./git_project_1_git_source_1: + remotes: + remote_2: https://$GITLAB_TOKEN_NAME:$GITLAB_TOKEN@my.gitlab.com/cetmix-test/cetmix-tower-test.git + remote_3: git@my.gitlab.com:cetmix-test/cetmix-tower-test.git + merges: + - remote: remote_2 + ref: main + - remote: remote_3 + ref: '10000000' + target: remote_2 +./git_project_1_git_source_1_2: + remotes: + remote_1: https://x-token-auth:$BITBUCKET_TOKEN@bitbucket.com/cetmix-test/cetmix-tower-test-enterprise.git + remote_2: git@memegit.com:cetmix-test/cetmix-tower-test.git + merges: + - remote: remote_1 + ref: dev + - remote: remote_2 + ref: old + target: remote_1 +""" # noqa: E501 + + self.assertEqual( + yaml_code_from_project, + yaml_code, + "YAML code is not generated correctly", + ) + + # -- 3 -- + # Unlink source and check if file content is updated + self.git_source_2.unlink() + yaml_code_from_project = ( + self.file_1_rel.git_project_id._generate_code_git_aggregator( + self.file_1_rel + ) + ) + yaml_code = """# This file is generated with Cetmix Tower https://cetmix.com/tower +# It's designed to be used with git-aggregator tool developed by Acsone. +# Documentation for git-aggregator: https://github.com/acsone/git-aggregator + +# You need to set the following variables in your environment: +# GITLAB_TOKEN, GITLAB_TOKEN_NAME +# and run git-aggregator with '--expand-env' parameter. + +./git_project_1_git_source_1: + remotes: + remote_2: https://$GITLAB_TOKEN_NAME:$GITLAB_TOKEN@my.gitlab.com/cetmix-test/cetmix-tower-test.git + remote_3: git@my.gitlab.com:cetmix-test/cetmix-tower-test.git + merges: + - remote: remote_2 + ref: main + - remote: remote_3 + ref: '10000000' + target: remote_2 +""" # noqa: E501 + self.assertEqual( + yaml_code_from_project, + yaml_code, + "YAML code is not generated correctly", + ) + + def test_user_access(self): + """Test that regular users have no access to git project relations""" + user_rel = self.GitProjectRel.with_user(self.user) + + # Try create - should fail + with self.assertRaises(AccessError): + user_rel.create( + { + "server_id": self.server_test_1.id, + "file_id": self.server_1_file_1.id, + "git_project_id": self.git_project_1.id, + "project_format": "git_aggregator", + } + ) + + # Try read - should fail + with self.assertRaises(AccessError): + user_rel.browse(self.file_1_rel.id).read(["name"]) + + # Try write - should fail + with self.assertRaises(AccessError): + user_rel.browse(self.file_1_rel.id).write( + {"project_format": "git_aggregator"} + ) + + # Try unlink - should fail + with self.assertRaises(AccessError): + user_rel.browse(self.file_1_rel.id).unlink() + + def test_manager_read_access(self): + """Test manager read access rules""" + manager_rel = self.GitProjectRel.with_user(self.manager) + + # Initially manager should not have access + with self.assertRaises(AccessError): + manager_rel.browse(self.file_1_rel.id).read(["name"]) + + # Add manager as project user - should have read access + self.git_project_1.write({"user_ids": [(4, self.manager.id)]}) + self.assertEqual(manager_rel.browse(self.file_1_rel.id).name, "Git Project 1") + + # Remove from project, add as server user - should have read access + self.git_project_1.write({"user_ids": [(3, self.manager.id)]}) + self.server_test_1.write({"user_ids": [(4, self.manager.id)]}) + self.assertEqual(manager_rel.browse(self.file_1_rel.id).name, "Git Project 1") + + # Remove from server users, add as project manager - should have read access + self.server_test_1.write({"user_ids": [(3, self.manager.id)]}) + self.git_project_1.write({"manager_ids": [(4, self.manager.id)]}) + self.assertEqual(manager_rel.browse(self.file_1_rel.id).name, "Git Project 1") + + # Remove from project, add as server manager - should have read access + self.git_project_1.write({"manager_ids": [(3, self.manager.id)]}) + self.server_test_1.write({"manager_ids": [(4, self.manager.id)]}) + self.assertEqual(manager_rel.browse(self.file_1_rel.id).name, "Git Project 1") + + def test_manager_write_access(self): + """Test manager write/create access rules""" + manager_rel = self.GitProjectRel.with_user(self.manager) + + # Create new file to avoid unique constraint violation + file_2 = self.File.create( + { + "name": "test_file_2", + "server_id": self.server_test_1.id, + "source": "tower", + "file_type": "text", + } + ) + + # Try create without being project and server manager - should fail + with self.assertRaises(AccessError): + manager_rel.create( + { + "server_id": self.server_test_1.id, + "file_id": file_2.id, + "git_project_id": self.git_project_1.id, + "project_format": "git_aggregator", + } + ) + + # Add as project manager only - should still fail + file_3 = self.File.create( + { + "name": "test_file_3", + "server_id": self.server_test_1.id, + "source": "tower", + "file_type": "text", + } + ) + self.git_project_1.write({"manager_ids": [(4, self.manager.id)]}) + with self.assertRaises(AccessError): + manager_rel.create( + { + "server_id": self.server_test_1.id, + "file_id": file_3.id, + "git_project_id": self.git_project_1.id, + "project_format": "git_aggregator", + } + ) + + # Add as server manager - should succeed + file_4 = self.File.create( + { + "name": "test_file_4", + "server_id": self.server_test_1.id, + "source": "tower", + "file_type": "text", + } + ) + self.server_test_1.write({"manager_ids": [(4, self.manager.id)]}) + rel = manager_rel.create( + { + "server_id": self.server_test_1.id, + "file_id": file_4.id, + "git_project_id": self.git_project_1.id, + "project_format": "git_aggregator", + } + ) + self.assertTrue(rel.exists()) + + # Test write access + rel.write({"project_format": "git_aggregator"}) + + # Remove server manager access - should fail to write + self.server_test_1.write({"manager_ids": [(3, self.manager.id)]}) + with self.assertRaises(AccessError): + rel.write({"project_format": "git_aggregator"}) + + # Remove project manager access - should fail to write + self.git_project_1.write({"manager_ids": [(3, self.manager.id)]}) + with self.assertRaises(AccessError): + rel.write({"project_format": "git_aggregator"}) + + def test_manager_unlink_access(self): + """Test manager unlink access rules""" + manager_rel = self.GitProjectRel.with_user(self.manager) + + # Try delete without being project and server manager - should fail + with self.assertRaises(AccessError): + manager_rel.browse(self.file_1_rel.id).unlink() + + # Add as project manager only - should fail + self.git_project_1.write({"manager_ids": [(4, self.manager.id)]}) + with self.assertRaises(AccessError): + manager_rel.browse(self.file_1_rel.id).unlink() + + # Add as server manager - should succeed + self.server_test_1.write({"manager_ids": [(4, self.manager.id)]}) + self.file_1_rel.with_user(self.manager).unlink() + self.assertFalse(self.file_1_rel.exists()) + + def test_root_access(self): + """Test root access rules""" + root_rel = self.GitProjectRel.with_user(self.root) + + # Create new file to avoid unique constraint violation + file_3 = self.File.create( + { + "name": "test_file_3", + "server_id": self.server_test_1.id, + "source": "tower", + "file_type": "text", + } + ) + + # Create - should succeed + rel = root_rel.create( + { + "server_id": self.server_test_1.id, + "file_id": file_3.id, + "git_project_id": self.git_project_1.id, + "project_format": "git_aggregator", + } + ) + self.assertTrue(rel.exists()) + + # Read - should succeed + self.assertEqual(root_rel.browse(rel.id).name, "Git Project 1") + + # Write - should succeed + root_rel.browse(rel.id).write({"project_format": "git_aggregator"}) + + # Delete - should succeed + rel.unlink() + self.assertFalse(rel.exists()) diff --git a/addons/cetmix_tower_git/tests/test_file_template_rel.py b/addons/cetmix_tower_git/tests/test_file_template_rel.py new file mode 100644 index 0000000..11e8849 --- /dev/null +++ b/addons/cetmix_tower_git/tests/test_file_template_rel.py @@ -0,0 +1,308 @@ +from odoo.exceptions import AccessError + +from .common import CommonTest + + +class TestFileTemplateRel(CommonTest): + """Test class for git file template relation.""" + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.file_template_1_rel = cls.GitProjectFileTemplateRel.create( + { + "git_project_id": cls.git_project_1.id, + "file_template_id": cls.file_template_1.id, + "project_format": "git_aggregator", + } + ) + + def test_file_template_rel_create(self): + """Test if file template relation is created correctly""" + + # -- 1 -- + # Check if file content is updated + + # Get code from project + yaml_code_from_project = ( + self.file_template_1_rel.git_project_id._generate_code_git_aggregator( + self.file_template_1_rel + ) + ) + + self.assertEqual( + self.file_template_1.code, + yaml_code_from_project, + "File template content is not updated correctly", + ) + + # Check specific if remote is present in file + self.assertIn( + self.remote_other_ssh.repo_id.url_ssh, + self.file_template_1.code, + "Remote is not present in file template", + ) + + # -- 2 -- + # Modify remove and check if file template content is updated + self.remote_other_ssh.repo_id = self.Repo.create( + { + "url": "https://github.com/cetmix/cetmix-memes.git", + } + ) + self.remote_other_ssh.url_protocol = "https" + + # Must be different from previous project code + self.assertNotEqual( + self.file_template_1.code, + yaml_code_from_project, + "File template content is not updated correctly", + ) + # New remote must be present in file + self.assertIn( + "https://github.com/cetmix/cetmix-memes.git", + self.file_template_1.code, + "Remote is not present in file template", + ) + + # -- 3 -- + # Disable source and check if file content is updated + self.git_source_2.active = False + self.assertNotIn( + "https://github.com/cetmix/cetmix-memes.git", + self.file_template_1.code, + "Remote is present in file template", + ) + + def test_format_git_aggregator(self): + """Test if format git aggregator works correctly""" + + # -- 1 -- + # Check if YAML code is generated correctly + + yaml_code = """# This file is generated with Cetmix Tower https://cetmix.com/tower +# It's designed to be used with git-aggregator tool developed by Acsone. +# Documentation for git-aggregator: https://github.com/acsone/git-aggregator + +# You need to set the following variables in your environment: +# BITBUCKET_TOKEN, GITLAB_TOKEN, GITLAB_TOKEN_NAME +# and run git-aggregator with '--expand-env' parameter. + +./git_project_1_git_source_1: + remotes: + remote_1: https://github.com/cetmix-test/cetmix-tower-test.git + remote_2: https://$GITLAB_TOKEN_NAME:$GITLAB_TOKEN@my.gitlab.com/cetmix-test/cetmix-tower-test.git + remote_3: git@my.gitlab.com:cetmix-test/cetmix-tower-test.git + merges: + - remote: remote_1 + ref: refs/pull/123/head + - remote: remote_2 + ref: main + - remote: remote_3 + ref: '10000000' + target: remote_1 +./git_project_1_git_source_1_2: + remotes: + remote_1: https://x-token-auth:$BITBUCKET_TOKEN@bitbucket.com/cetmix-test/cetmix-tower-test-enterprise.git + remote_2: git@memegit.com:cetmix-test/cetmix-tower-test.git + merges: + - remote: remote_1 + ref: dev + - remote: remote_2 + ref: old + target: remote_1 +""" # noqa: E501 + + # Get code from project + yaml_code_from_project = ( + self.file_template_1_rel.git_project_id._generate_code_git_aggregator( + self.file_template_1_rel + ) + ) + self.assertEqual( + yaml_code_from_project, + yaml_code, + "YAML code is not generated correctly", + ) + + def test_user_access(self): + """Test that regular users have no access to git project relations""" + user_rel = self.GitProjectFileTemplateRel.with_user(self.user) + + # Try create - should fail + with self.assertRaises(AccessError): + user_rel.create( + { + "git_project_id": self.git_project_1.id, + "file_template_id": self.file_template_1.id, + "project_format": "git_aggregator", + } + ) + + # Try read - should fail + with self.assertRaises(AccessError): + user_rel.browse(self.file_template_1_rel.id).read(["name"]) + + # Try write - should fail + with self.assertRaises(AccessError): + user_rel.browse(self.file_template_1_rel.id).write( + {"project_format": "git_aggregator"} + ) + + # Try unlink - should fail + with self.assertRaises(AccessError): + user_rel.browse(self.file_template_1_rel.id).unlink() + + def test_manager_read_access(self): + """Test manager read access rules""" + manager_rel = self.GitProjectFileTemplateRel.with_user(self.manager) + + # Initially manager should not have access + with self.assertRaises(AccessError): + manager_rel.browse(self.file_template_1_rel.id).read(["name"]) + + # Add manager as project user - should have read access + self.git_project_1.write({"user_ids": [(4, self.manager.id)]}) + self.assertEqual( + manager_rel.browse(self.file_template_1_rel.id).name, "Git Project 1" + ) + + # Remove from project, add as file template user + # should have read access + self.git_project_1.write({"user_ids": [(3, self.manager.id)]}) + self.file_template_1.write({"user_ids": [(4, self.manager.id)]}) + self.assertEqual( + manager_rel.browse(self.file_template_1_rel.id).name, "Git Project 1" + ) + + # Remove from file template users, add as project manager + # should have read access + self.file_template_1.write({"user_ids": [(3, self.manager.id)]}) + self.git_project_1.write({"manager_ids": [(4, self.manager.id)]}) + self.assertEqual( + manager_rel.browse(self.file_template_1_rel.id).name, "Git Project 1" + ) + + # Remove from project, add as file template manager + # should have read access + self.git_project_1.write({"manager_ids": [(3, self.manager.id)]}) + self.file_template_1.write({"manager_ids": [(4, self.manager.id)]}) + self.assertEqual( + manager_rel.browse(self.file_template_1_rel.id).name, "Git Project 1" + ) + + def test_manager_write_access(self): + """Test manager write/create access rules""" + manager_rel = self.GitProjectFileTemplateRel.with_user(self.manager) + + # Create new file template to avoid unique constraint violation + file_template_2 = self.FileTemplate.create( + { + "name": "test_file_template_2", + } + ) + + # Try create without being project and file template manager - should fail + with self.assertRaises(AccessError): + manager_rel.create( + { + "git_project_id": self.git_project_1.id, + "file_template_id": file_template_2.id, + "project_format": "git_aggregator", + } + ) + + # Add as project manager only - should still fail + file_template_3 = self.FileTemplate.create( + { + "name": "test_file_template_3", + } + ) + self.git_project_1.write({"manager_ids": [(4, self.manager.id)]}) + with self.assertRaises(AccessError): + manager_rel.create( + { + "git_project_id": self.git_project_1.id, + "file_template_id": file_template_3.id, + "project_format": "git_aggregator", + } + ) + + # Add as file template manager - should succeed + file_template_4 = self.FileTemplate.create( + { + "name": "test_file_template_4", + } + ) + file_template_4.write({"manager_ids": [(4, self.manager.id)]}) + rel = manager_rel.create( + { + "git_project_id": self.git_project_1.id, + "file_template_id": file_template_4.id, + "project_format": "git_aggregator", + } + ) + self.assertTrue(rel.exists()) + + # Test write access + rel.write({"project_format": "git_aggregator"}) + + # Remove file template manager access - should fail to write + file_template_4.write({"manager_ids": [(3, self.manager.id)]}) + with self.assertRaises(AccessError): + rel.write({"project_format": "git_aggregator"}) + + # Remove project manager access - should fail to write + self.git_project_1.write({"manager_ids": [(3, self.manager.id)]}) + file_template_4.write({"manager_ids": [(4, self.manager.id)]}) + with self.assertRaises(AccessError): + rel.write({"project_format": "git_aggregator"}) + + def test_manager_unlink_access(self): + """Test manager unlink access rules""" + manager_rel = self.GitProjectFileTemplateRel.with_user(self.manager) + + # Try delete without being project and server manager - should fail + with self.assertRaises(AccessError): + manager_rel.browse(self.file_template_1_rel.id).unlink() + + # Add as project manager only - should fail + self.git_project_1.write({"manager_ids": [(4, self.manager.id)]}) + with self.assertRaises(AccessError): + manager_rel.browse(self.file_template_1_rel.id).unlink() + + # Add as file template manager - should succeed + self.file_template_1.write({"manager_ids": [(4, self.manager.id)]}) + self.file_template_1_rel.unlink() + self.assertFalse(self.file_template_1_rel.exists()) + + def test_root_access(self): + """Test root access rules""" + root_rel = self.GitProjectFileTemplateRel.with_user(self.root) + + # Create new file to avoid unique constraint violation + file_template_3 = self.FileTemplate.create( + { + "name": "test_file_template_3", + } + ) + + # Create - should succeed + rel = root_rel.create( + { + "git_project_id": self.git_project_1.id, + "file_template_id": file_template_3.id, + "project_format": "git_aggregator", + } + ) + self.assertTrue(rel.exists()) + + # Read - should succeed + self.assertEqual(root_rel.browse(rel.id).name, "Git Project 1") + + # Write - should succeed + root_rel.browse(rel.id).write({"project_format": "git_aggregator"}) + + # Delete - should succeed + rel.unlink() + self.assertFalse(rel.exists()) diff --git a/addons/cetmix_tower_git/tests/test_project.py b/addons/cetmix_tower_git/tests/test_project.py new file mode 100644 index 0000000..588d520 --- /dev/null +++ b/addons/cetmix_tower_git/tests/test_project.py @@ -0,0 +1,315 @@ +from odoo.exceptions import AccessError + +from .common import CommonTest + + +class TestProject(CommonTest): + """Test class for git project.""" + + @classmethod + def setUpClass(cls): + super().setUpClass() + # Remove user bob from all groups + cls.remove_from_group( + cls.user_bob, + [ + "cetmix_tower_server.group_user", + "cetmix_tower_server.group_manager", + "cetmix_tower_server.group_root", + ], + ) + + # Create another manager for testing + cls.manager_2 = cls.Users.create( + { + "name": "Second Manager", + "login": "manager2", + "email": "manager2@test.com", + "groups_id": [(4, cls.env.ref("cetmix_tower_server.group_manager").id)], + } + ) + + # Create test project as root + cls.project = cls.GitProject.create( + { + "name": "Test Project", + } + ) + + def test_user_access(self): + """Test that regular users have no access to git projects""" + user_project = self.GitProject.with_user(self.user) + + # Test CRUD operations + with self.assertRaises(AccessError): + user_project.create({"name": "New Project"}) + with self.assertRaises(AccessError): + user_project.browse(self.project.id).read(["name"]) + with self.assertRaises(AccessError): + user_project.browse(self.project.id).write({"name": "Updated Name"}) + with self.assertRaises(AccessError): + user_project.browse(self.project.id).unlink() + + def test_manager_read_access(self): + """Test manager read access rules""" + manager_project = self.GitProject.with_user(self.manager) + + # Manager not in user_ids or manager_ids - should not read + with self.assertRaises(AccessError): + manager_project.browse(self.project.id).read(["name"]) + + # Add manager to user_ids - should read + self.project.write({"user_ids": [(4, self.manager.id)]}) + self.assertEqual(manager_project.browse(self.project.id).name, "Test Project") + + # Remove from user_ids, add to manager_ids - should read + self.project.write( + {"user_ids": [(3, self.manager.id)], "manager_ids": [(4, self.manager.id)]} + ) + self.assertEqual(manager_project.browse(self.project.id).name, "Test Project") + + def test_manager_write_access(self): + """Test manager write/create access rules""" + manager_project = self.GitProject.with_user(self.manager) + + # Create - should succeed as manager is added by default + new_project = manager_project.create({"name": "New Project"}) + self.assertTrue(new_project.exists()) + self.assertIn(self.manager, new_project.manager_ids) + + # Write - not in manager_ids, should fail + with self.assertRaises(AccessError): + manager_project.browse(self.project.id).write({"name": "Updated Name"}) + + # Add to manager_ids - should write + self.project.write({"manager_ids": [(4, self.manager.id)]}) + manager_project.browse(self.project.id).write({"name": "Updated Name"}) + self.assertEqual(self.project.name, "Updated Name") + + def test_manager_unlink_access(self): + """Test manager unlink access rules""" + # Create project as manager_2 + project = self.GitProject.with_user(self.manager_2).create( + {"name": "Project to Delete"} + ) + manager_project = self.GitProject.with_user(self.manager) + + # Try delete as different manager - should fail + with self.assertRaises(AccessError): + manager_project.browse(project.id).unlink() + + # Add to manager_ids but not creator - should fail + project.write({"manager_ids": [(4, self.manager.id)]}) + with self.assertRaises(AccessError): + manager_project.browse(project.id).unlink() + + # Create as manager and try delete - should succeed + own_project = manager_project.create({"name": "Own Project"}) + self.assertTrue(own_project.exists()) + own_project.unlink() + self.assertFalse(own_project.exists()) + + def test_root_access(self): + """Test root access rules""" + root_project = self.GitProject.with_user(self.root) + + # Create + new_project = root_project.create({"name": "Root Project"}) + self.assertTrue(new_project.exists()) + + # Read + self.assertEqual(root_project.browse(self.project.id).name, "Test Project") + + # Write + root_project.browse(self.project.id).write({"name": "Updated by Root"}) + self.assertEqual(self.project.name, "Updated by Root") + + # Delete + new_project.unlink() + self.assertFalse(new_project.exists()) + + def test_compute_user_ids(self): + """Test computation of user_ids and manager_ids for git projects""" + # Add users "Bob" and "user" to the group "cetmix_tower_server.group_manager" + self.add_to_group(self.user_bob, "cetmix_tower_server.group_manager") + self.add_to_group(self.user, "cetmix_tower_server.group_manager") + + # -- 1 -- + # Create project as manager + project_as_manager = self.GitProject.with_user(self.manager).create( + { + "name": "Project As Manager", + } + ) + # Check that manager is added to both user_ids and manager_ids by default + self.assertEqual(len(project_as_manager.user_ids), 1) + self.assertIn(self.manager, project_as_manager.user_ids) + self.assertEqual(len(project_as_manager.manager_ids), 1) + self.assertIn(self.manager, project_as_manager.manager_ids) + + # -- 2 -- + # Create servers with multiple users and managers + server_1 = self.Server.create( + { + "name": "Test Server 1", + "ip_v4_address": "localhost", + "ssh_username": "admin", + "ssh_password": "password", + "os_id": self.os_debian_10.id, + "user_ids": [(6, 0, [self.user_bob.id, self.user.id])], # Two users + "manager_ids": [ + (6, 0, [self.manager.id, self.manager_2.id]) + ], # Two managers + } + ) + + server_2 = self.Server.create( + { + "name": "Test Server 2", + "ip_v4_address": "localhost", + "ssh_username": "admin", + "ssh_password": "password", + "os_id": self.os_debian_10.id, + "user_ids": [ + (6, 0, [self.user_bob.id, self.user.id]) + ], # Same two users + "manager_ids": [ + (6, 0, [self.manager.id, self.manager_2.id]) + ], # Same two managers + } + ) + + # Create project and link servers + project = self.GitProject.create( + { + "name": "Test Project", + } + ) + + # Create files and link them to the project + for server in [server_1, server_2]: + file = self.File.create( + { + "name": f"test_file_{server.name}", + "server_id": server.id, + } + ) + self.GitProjectRel.create( + { + "server_id": server.id, + "file_id": file.id, + "git_project_id": project.id, + "project_format": "git_aggregator", + } + ) + + # Invalidate cache to ensure computed fields are updated + project.invalidate_recordset(["server_ids", "user_ids", "manager_ids"]) + + # -- 3 -- + # Test computed values with linked servers + # Each user/manager should be counted only once even if present in both servers + self.assertEqual(len(project.server_ids), 2) + self.assertEqual(len(project.user_ids), 2) # Two unique users + self.assertIn(self.user_bob, project.user_ids) + self.assertIn(self.user, project.user_ids) + self.assertEqual(len(project.manager_ids), 2) # Two unique managers + self.assertIn(self.manager, project.manager_ids) + self.assertIn(self.manager_2, project.manager_ids) + + # -- 4 -- + # Add server with different users/managers + server_3 = self.Server.create( + { + "name": "Test Server 3", + "ip_v4_address": "localhost", + "ssh_username": "admin", + "ssh_password": "password", + "os_id": self.os_debian_10.id, + "user_ids": [(6, 0, [self.user_bob.id])], # Only one user + "manager_ids": [(6, 0, [self.manager_2.id])], # Only second manager + } + ) + file_3 = self.File.create( + { + "name": "test_file_3", + "server_id": server_3.id, + } + ) + self.GitProjectRel.create( + { + "server_id": server_3.id, + "file_id": file_3.id, + "git_project_id": project.id, + "project_format": "git_aggregator", + } + ) + + # Invalidate cache to ensure computed fields are updated + project.invalidate_recordset(["server_ids", "user_ids", "manager_ids"]) + + # Test that computed values are updated correctly + # Only users/managers present in all servers should remain + self.assertEqual(len(project.server_ids), 3) + self.assertEqual(len(project.user_ids), 1) # Only bob is in all servers + self.assertIn(self.user_bob, project.user_ids) + self.assertEqual( + len(project.manager_ids), 1 + ) # Only manager_2 is in all servers + self.assertIn(self.manager_2, project.manager_ids) + + # -- 5 -- + # Verify that first manager can still access the project + project_as_manager_1 = self.GitProject.with_user(self.manager).browse( + project.id + ) + self.assertTrue(project_as_manager_1.exists()) + self.assertEqual(project_as_manager_1.name, "Test Project") + + def test_manager_server_based_access(self): + """Test manager access through server relationships""" + manager_project = self.GitProject.with_user(self.manager) + + # Create a server where manager is a user + server = self.Server.create( + { + "name": "Test Server", + "ip_v4_address": "localhost", + "ssh_username": "admin", + "ssh_password": "password", + "os_id": self.os_debian_10.id, + "user_ids": [(4, self.manager.id)], + } + ) + + # Create a file and link project to server + file = self.File.create( + { + "name": "test_file", + "server_id": server.id, + } + ) + self.GitProjectRel.create( + { + "server_id": server.id, + "file_id": file.id, + "git_project_id": self.project.id, + "project_format": "git_aggregator", + } + ) + + # Manager should be able to read project through server relationship + self.assertEqual(manager_project.browse(self.project.id).name, "Test Project") + + # Remove manager from server users + server.write({"user_ids": [(3, self.manager.id)]}) + + # Manager should not be able to read project anymore + with self.assertRaises(AccessError): + manager_project.browse(self.project.id).read(["name"]) + + # Add manager to server managers + server.write({"manager_ids": [(4, self.manager.id)]}) + + # Manager should be able to read project again + self.assertEqual(manager_project.browse(self.project.id).name, "Test Project") diff --git a/addons/cetmix_tower_git/tests/test_remote.py b/addons/cetmix_tower_git/tests/test_remote.py new file mode 100644 index 0000000..f75bffe --- /dev/null +++ b/addons/cetmix_tower_git/tests/test_remote.py @@ -0,0 +1,462 @@ +from odoo.exceptions import AccessError + +from .common import CommonTest + + +class TestRemote(CommonTest): + """Test class for git remote.""" + + @classmethod + def setUpClass(cls): + super().setUpClass() + # Create another manager for testing + cls.manager_2 = cls.Users.create( + { + "name": "Second Manager", + "login": "manager2", + "email": "manager2@test.com", + "groups_id": [(4, cls.env.ref("cetmix_tower_server.group_manager").id)], + } + ) + + # Create test project and source as root + cls.project = cls.GitProject.create( + { + "name": "Test Project", + } + ) + cls.source = cls.GitSource.create( + { + "name": "Test Source", + "git_project_id": cls.project.id, + } + ) + cls.repo_cetmix_tower = cls.Repo.create( + { + "name": "Cetmix Tower", + "url": "https://github.com/cetmix-test/cetmix-tower.git", + } + ) + cls.remote = cls.GitRemote.create( + { + "repo_id": cls.repo_cetmix_tower.id, + "source_id": cls.source.id, + "head_type": "branch", + "head": "main", + } + ) + cls.repo_test = cls.Repo.create( + { + "name": "Test Repository", + "url": "https://github.com/cetmix-test/test.git", + } + ) + + def test_user_access(self): + """Test that regular users have no access to git remotes""" + user_remote = self.GitRemote.with_user(self.user) + + # Test CRUD operations + with self.assertRaises(AccessError): + user_remote.create( + { + "repo_id": self.repo_test.id, + "url_protocol": "https", + "source_id": self.source.id, + "head": "main", + } + ) + with self.assertRaises(AccessError): + user_remote.search([("id", "=", self.remote.id)]) + with self.assertRaises(AccessError): + self.remote.with_user(self.user).write({"head": "dev"}) + with self.assertRaises(AccessError): + self.remote.with_user(self.user).unlink() + + def test_manager_read_access(self): + """Test manager read access rules""" + manager_remote = self.GitRemote.with_user(self.manager) + + # Manager not in project user_ids or manager_ids - should not read + self.assertFalse(manager_remote.search([("id", "=", self.remote.id)])) + + # Add manager to project user_ids - should read + self.project.write({"user_ids": [(4, self.manager.id)]}) + remote = manager_remote.search([("id", "=", self.remote.id)]) + self.assertTrue(remote) + self.assertEqual(remote.head, "main") + + # Remove from user_ids, add to manager_ids - should read + self.project.write( + {"user_ids": [(3, self.manager.id)], "manager_ids": [(4, self.manager.id)]} + ) + remote = manager_remote.search([("id", "=", self.remote.id)]) + self.assertTrue(remote.exists()) + + def test_manager_write_access(self): + """Test manager write/create access rules""" + manager_remote = self.GitRemote.with_user(self.manager) + + # Create project as manager - should be added to manager_ids automatically + project = self.GitProject.with_user(self.manager).create( + { + "name": "Manager Project", + } + ) + source = self.GitSource.with_user(self.manager).create( + { + "name": "Manager Source", + "git_project_id": project.id, + } + ) + + # Create remote in own project - should succeed + new_remote = manager_remote.create( + { + "repo_id": self.repo_test.id, + "url_protocol": "https", + "source_id": source.id, + "head_type": "branch", + "head": "main", + } + ) + self.assertTrue(new_remote.exists()) + + # Write to own remote - should succeed + new_remote.write({"head": "dev"}) + self.assertEqual(new_remote.head, "dev") + + # Write to other's remote - should fail + with self.assertRaises(AccessError): + self.remote.with_user(self.manager).write({"head": "dev"}) + + def test_manager_unlink_access(self): + """Test manager unlink access rules""" + # Create project and remote as manager_2 + project = self.GitProject.with_user(self.manager_2).create( + { + "name": "Manager 2 Project", + } + ) + source = self.GitSource.create( + { + "name": "Manager 2 Source", + "git_project_id": project.id, + } + ) + remote = self.GitRemote.with_user(self.manager_2).create( + { + "repo_id": self.repo_test.id, + "url_protocol": "https", + "source_id": source.id, + "head_type": "branch", + "head": "main", + } + ) + + # Try delete as different manager - should fail even if added to manager_ids + project.write({"manager_ids": [(4, self.manager.id)]}) + with self.assertRaises(AccessError): + remote.with_user(self.manager).unlink() + + # Create remote as manager and try delete - should succeed + own_remote = self.GitRemote.with_user(self.manager).create( + { + "repo_id": self.repo_test.id, + "url_protocol": "https", + "source_id": source.id, + "head_type": "branch", + "head": "main", + } + ) + self.assertTrue(own_remote.exists()) + own_remote.with_user(self.manager).unlink() + self.assertFalse(own_remote.exists()) + + def test_root_access(self): + """Test root access rules""" + root_remote = self.GitRemote.with_user(self.root) + + # Create + new_remote = root_remote.create( + { + "repo_id": self.repo_test.id, + "url_protocol": "https", + "source_id": self.source.id, + "head_type": "branch", + "head": "main", + } + ) + self.assertTrue(new_remote.exists()) + + # Read + remote = root_remote.search([("id", "=", self.remote.id)]) + self.assertTrue(remote) + self.assertEqual(remote.head, "main") + + # Write + self.remote.with_user(self.root).write({"head": "dev"}) + self.assertEqual(self.remote.head, "dev") + + # Delete + new_remote.with_user(self.root).unlink() + self.assertFalse(new_remote.exists()) + + def test_remote_provider_protocol_and_name(self): + """Test if remote provider is detected correctly""" + + # -- 1-- + # GitHub + https + # Check if remote provider is detected correctly + self.assertEqual( + self.remote_github_https.repo_provider, + "github", + "Provider is not detected correctly", + ) + self.assertEqual( + self.remote_github_https.url_protocol, + "https", + "Protocol is not detected correctly", + ) + self.assertEqual( + self.remote_github_https.name, + "remote_1", + "Name is not prepared correctly", + ) + + # -- 2 -- + # GitLab + ssh + # Check if remote provider is detected correctly + self.assertEqual( + self.remote_gitlab_ssh.repo_provider, + "gitlab", + "Provider is not detected correctly", + ) + self.assertEqual( + self.remote_gitlab_ssh.url_protocol, + "ssh", + "Protocol is not detected correctly", + ) + self.assertEqual( + self.remote_gitlab_ssh.name, + "remote_3", + "Name is not prepared correctly", + ) + + # -- 3 -- + # Bitbucket + https + # Check if remote provider is detected correctly + self.assertEqual( + self.remote_bitbucket_https.repo_provider, + "bitbucket", + "Provider is not detected correctly", + ) + self.assertEqual( + self.remote_bitbucket_https.url_protocol, + "https", + "Protocol is not detected correctly", + ) + self.assertEqual( + self.remote_bitbucket_https.name, + "remote_1", + "Name is not prepared correctly", + ) + + # -- 4 -- + # Other + ssh + # Check if remote provider is detected correctly + self.assertEqual( + self.remote_other_ssh.repo_provider, + "gitlab", # this is how giturlparse detects the provider + "Provider is not detected correctly", + ) + self.assertEqual( + self.remote_other_ssh.url_protocol, + "ssh", + "Protocol is not detected correctly", + ) + self.assertEqual( + self.remote_other_ssh.name, + "remote_2", + "Name is not prepared correctly", + ) + + def test_git_aggregator_prepare_url(self): + """Test if url is prepared correctly""" + + # -- 1 -- + # GitHub + https + self.remote_github_https.repo_id.is_private = False + self.assertEqual( + self.remote_github_https._git_aggregator_prepare_url(), + self.remote_github_https.repo_id.url, + "URL is not prepared correctly", + ) + + # -- 2 -- + # GitHub + https -> private + self.remote_github_https.repo_id.is_private = True + self.assertEqual( + self.remote_github_https._git_aggregator_prepare_url(), + "https://$GITHUB_TOKEN:x-oauth-basic@github.com/cetmix-test/cetmix-tower-test.git", + "URL is not prepared correctly", + ) + + # -- 3 -- + # Gitlab + https + self.remote_gitlab_https.repo_id.is_private = False + self.assertEqual( + self.remote_gitlab_https._git_aggregator_prepare_url(), + self.remote_gitlab_https.repo_id.url, + "URL is not prepared correctly", + ) + + # -- 4 -- + # Gitlab + https -> private + self.remote_gitlab_https.repo_id.is_private = True + self.assertEqual( + self.remote_gitlab_https._git_aggregator_prepare_url(), + "https://$GITLAB_TOKEN_NAME:$GITLAB_TOKEN@my.gitlab.com/cetmix-test/cetmix-tower-test.git", + "URL is not prepared correctly", + ) + + # -- 5 -- + # Bitbucket + https + self.remote_bitbucket_https.repo_id.is_private = False + self.assertEqual( + self.remote_bitbucket_https._git_aggregator_prepare_url(), + self.remote_bitbucket_https.repo_id.url, + "URL is not prepared correctly", + ) + + # -- 6 -- + # Bitbucket + https -> private + self.remote_bitbucket_https.repo_id.is_private = True + self.assertEqual( + self.remote_bitbucket_https._git_aggregator_prepare_url(), + "https://x-token-auth:$BITBUCKET_TOKEN@bitbucket.com/cetmix-test/cetmix-tower-test-enterprise.git", + "URL is not prepared correctly", + ) + + # -- 7 -- + # Other + ssh + self.remote_other_ssh.repo_id.is_private = False + self.assertEqual( + self.remote_other_ssh._git_aggregator_prepare_url(), + self.remote_other_ssh.repo_id.url_ssh, + "URL is not prepared correctly", + ) + + def test_git_aggregator_prepare_head(self): + """Test if head is prepared correctly""" + + # -- 1 -- + # GitHub + PR/MR as link + self.assertEqual( + self.remote_github_https._git_aggregator_prepare_head(), + "refs/pull/123/head", + "Head is not prepared correctly", + ) + + # -- 2 -- + # GitHub + PR/MR as number + self.remote_github_https.write({"head": "123", "head_type": "pr"}) + self.assertEqual( + self.remote_github_https._git_aggregator_prepare_head(), + "refs/pull/123/head", + "Head is not prepared correctly", + ) + + # -- 3 -- + # GitHub + branch as name + self.remote_github_https.write({"head": "main", "head_type": "branch"}) + self.assertEqual( + self.remote_github_https._git_aggregator_prepare_head(), + self.remote_github_https.head, + "Head is not prepared correctly", + ) + + # -- 4 -- + # GitHub + branch as link + self.remote_github_https.write( + { + "head": "https://github.com/cetmix-test/cetmix-tower/list/14.0-demo-branch", + "head_type": "branch", + } + ) + self.assertEqual( + self.remote_github_https._git_aggregator_prepare_head(), + "14.0-demo-branch", + "Head is not prepared correctly", + ) + + # -- 5 -- + # GitHub + commit as number + self.remote_github_https.write({"head": "1234567890", "head_type": "commit"}) + self.assertEqual( + self.remote_github_https._git_aggregator_prepare_head(), + "1234567890", + "Head is not prepared correctly", + ) + + # -- 6 -- + # GitHub + commit as link + self.remote_github_https.head = ( + "https://github.com/cetmix-test/cetmix-tower/commit/1234567890" + ) + self.assertEqual( + self.remote_github_https._git_aggregator_prepare_head(), + "1234567890", + "Head is not prepared correctly", + ) + + def test_manager_server_based_access(self): + """Test manager access to remotes through server relationships""" + manager_remote = self.GitRemote.with_user(self.manager) + + # Create a server where manager is a user + server = self.Server.create( + { + "name": "Test Server", + "ip_v4_address": "localhost", + "ssh_username": "admin", + "ssh_password": "password", + "os_id": self.os_debian_10.id, + "user_ids": [(4, self.manager.id)], + } + ) + + # Link project to server + file = self.File.create( + { + "name": "test_file", + "server_id": server.id, + } + ) + self.GitProjectRel.create( + { + "server_id": server.id, + "file_id": file.id, + "git_project_id": self.project.id, + "project_format": "git_aggregator", + } + ) + + # Manager should be able to read remote through server relationship + remote = manager_remote.search([("id", "=", self.remote.id)]) + self.assertTrue(remote) + self.assertEqual(remote.head, "main") + + # Remove manager from server users + server.write({"user_ids": [(3, self.manager.id)]}) + + # Manager should not be able to read remote anymore + self.assertFalse(manager_remote.search([("id", "=", self.remote.id)])) + + # Add manager to server managers + server.write({"manager_ids": [(4, self.manager.id)]}) + + # Manager should be able to read remote again + remote = manager_remote.search([("id", "=", self.remote.id)]) + self.assertTrue(remote) + self.assertEqual(remote.head, "main") diff --git a/addons/cetmix_tower_git/tests/test_repo.py b/addons/cetmix_tower_git/tests/test_repo.py new file mode 100644 index 0000000..05db569 --- /dev/null +++ b/addons/cetmix_tower_git/tests/test_repo.py @@ -0,0 +1,84 @@ +from odoo.exceptions import ValidationError + +from .common import CommonTest + + +class TestRepo(CommonTest): + """Test class for git repository.""" + + @classmethod + def setUpClass(cls): + super().setUpClass() + + def test_repo_create_from_url_https_success(self): + """Test if repository is created correctly""" + # -- 1 -- + # Valid HTTPS URL + repo = self.Repo.create( + { + "url": "https://github.com/memes-demo/doge-memes.git", + } + ) + repo.invalidate_recordset() + + self.assertEqual(repo.name, "github.com/memes-demo/doge-memes") + self.assertEqual(repo.host, "github.com") + self.assertEqual(repo.owner_id.name, "memes-demo") + self.assertEqual(repo.provider, "github") + self.assertEqual(repo.is_private, False) + self.assertEqual(repo.url_ssh, "git@github.com:memes-demo/doge-memes.git") + self.assertEqual(repo.url_git, "git://github.com/memes-demo/doge-memes.git") + + def test_repo_create_from_url_ssh_success(self): + """Test if repository is created correctly""" + # -- 1 -- + # Valid SSH URL + repo = self.Repo.create( + { + "url": "git@gitlab.com:chad-guy/chad-guy.git", + } + ) + repo.invalidate_recordset() + + self.assertEqual(repo.name, "gitlab.com/chad-guy/chad-guy") + self.assertEqual(repo.host, "gitlab.com") + self.assertEqual(repo.owner_id.name, "chad-guy") + self.assertEqual(repo.provider, "gitlab") + self.assertEqual(repo.is_private, False) + self.assertEqual(repo.url, "https://gitlab.com/chad-guy/chad-guy.git") + self.assertEqual(repo.url_git, "git://gitlab.com/chad-guy/chad-guy.git") + + def test_repo_create_from_url_git_success(self): + """Test if repository is created correctly""" + # -- 1 -- + # Valid GIT URL + repo = self.Repo.create( + { + "url": "git://bitbucket.com/much-pepe/pepe-memes.git", + } + ) + self.assertEqual(repo.name, "bitbucket.com/much-pepe/pepe-memes") + self.assertEqual(repo.host, "bitbucket.com") + self.assertEqual(repo.owner_id.name, "much-pepe") + self.assertEqual(repo.provider, "bitbucket") + self.assertEqual(repo.is_private, False) + self.assertEqual(repo.url_ssh, "git@bitbucket.com:much-pepe/pepe-memes.git") + self.assertEqual(repo.url, "https://bitbucket.com/much-pepe/pepe-memes.git") + + def test_repo_create_from_url_fails(self): + """Test if repository creation fails with invalid URLs""" + + # Invalid URL 1 + with self.assertRaises(ValidationError): + self.Repo.create( + { + "url": "something.com", + } + ) + # Invalid URL 2 + with self.assertRaises(ValidationError): + self.Repo.create( + { + "url": "random string", + } + ) diff --git a/addons/cetmix_tower_git/tests/test_server.py b/addons/cetmix_tower_git/tests/test_server.py new file mode 100644 index 0000000..473b825 --- /dev/null +++ b/addons/cetmix_tower_git/tests/test_server.py @@ -0,0 +1,416 @@ +try: + from odoo.addons.queue_job.tests.common import trap_jobs +except ImportError: + trap_jobs = None + +from .common import CommonTest + + +class TestServer(CommonTest): + """Test setting git project to server from plan line.""" + + @classmethod + def setUpClass(cls): + super().setUpClass() + + cls.GitProjectRel.create( + { + "git_project_id": cls.git_project_1.id, + "server_id": cls.server_test_1.id, + "file_id": cls.server_1_file_1.id, + } + ) + + def test_server_creation_running_flight_plan(self): + """Test that server is created with git project from plan line.""" + git_project = self.GitProject.create( + { + "name": "Test Git Project", + "manager_ids": [(4, self.manager.id)], + } + ) + + file_template = self.FileTemplate.create( + { + "name": "Git Config Template", + "file_name": "repos.yaml", + "server_dir": "/var/test", + "code": "repositories:\n test_repo:\n " + "url: https://github.com/test/repo.git\n target: main", + } + ) + + command = self.Command.create( + { + "name": "Create Git Config File", + "action": "file_using_template", + "file_template_id": file_template.id, + } + ) + + flight_plan = self.Plan.create( + { + "name": "Git Project Setup Plan", + "note": "Sets up a git project on the server", + } + ) + + self.plan_line.create( + { + "plan_id": flight_plan.id, + "command_id": command.id, + "sequence": 10, + "git_project_id": git_project.id, + } + ) + + server_template = self.ServerTemplate.create( + { + "name": "Git Server Template", + "ssh_port": 22, + "ssh_username": "admin", + "ssh_password": "password", + "ssh_auth_mode": "p", + "os_id": self.os_debian_10.id, + "flight_plan_id": flight_plan.id, + "manager_ids": [(4, self.manager.id)], + } + ) + + action = server_template.action_create_server() + + # Open the wizard and fill in the data + wizard = ( + self.env["cx.tower.server.template.create.wizard"] + .with_context(**action["context"]) + .create( + { + "name": "Git Server", + "ip_v4_address": "192.168.1.10", + "server_template_id": server_template.id, + "skip_host_key": True, + } + ) + ) + + # If cetmix_tower_server_queue module is installed, test async processing + if self.env["ir.module.module"].search_count( + [("name", "=", "cetmix_tower_server_queue"), ("state", "=", "installed")] + ): + with trap_jobs() as trap: + wizard.action_confirm() + + # Verify that jobs were created + self.assertGreater( + len(trap.enqueued_jobs), 0, "Jobs should have been enqueued" + ) + + # Execute all trapped jobs to simulate async processing + trap.perform_enqueued_jobs() + else: + wizard.action_confirm() + + # Now search for the created records after jobs have been executed + server = self.Server.search( + [ + ("name", "=", "Git Server"), + ("server_template_id", "=", server_template.id), + ] + ) + self.assertEqual(len(server), 1, "Exactly one server should have been created") + + # Verify the file was created + file = self.File.search( + [("server_id", "=", server.id), ("name", "=", "repos.yaml")] + ) + + self.assertEqual( + len(file), 1, "Exactly one git config file should have been created" + ) + + # Verify the git project relation exists + git_project_rel = self.GitProjectRel.search( + [ + ("server_id", "=", server.id), + ("git_project_id", "=", git_project.id), + ("file_id", "=", file.id), + ] + ) + + self.assertEqual( + len(git_project_rel), 1, "Exactly one git project relation should exist" + ) + self.assertEqual( + git_project_rel.file_id, + file, + "The related file should be the git config file", + ) + self.assertEqual( + git_project_rel.git_project_id, + git_project, + "The related git project should match the one in the flight plan", + ) + self.assertEqual( + git_project_rel.project_format, + git_project._default_project_format(), + "Project format should match the default format", + ) + + def test_file_creation_with_git_project_from_custom_values(self): + """Test that git project relation is created when git project + is provided from custom values in variable_values. + """ + git_project = self.GitProject.create( + { + "name": "Test Git Project From Custom Values", + "manager_ids": [(4, self.manager.id)], + } + ) + + file_template = self.FileTemplate.create( + { + "name": "Git Config Template Custom Values", + "file_name": "repos_custom.yaml", + "server_dir": "/var/test", + "code": "repositories:\n test_repo:\n " + "url: https://github.com/test/repo.git\n target: main", + } + ) + + command = self.Command.create( + { + "name": "Create Git Config File Custom Values", + "action": "file_using_template", + "file_template_id": file_template.id, + } + ) + + flight_plan = self.Plan.create( + { + "name": "Git Project Setup Plan Custom Values", + "note": "Sets up a git project on the server from custom values", + } + ) + + # Create plan line WITHOUT git_project_id set + # The git project should come from custom values instead + plan_line = self.plan_line.create( + { + "plan_id": flight_plan.id, + "command_id": command.id, + "sequence": 10, + } + ) + + # Create plan log + plan_log = self.env["cx.tower.plan.log"].create( + { + "server_id": self.server_test_1.id, + "plan_id": flight_plan.id, + "plan_line_executed_id": plan_line.id, + } + ) + + # Create command log with variable_values containing __git_project__ + command_log = self.CommandLog.create( + { + "server_id": self.server_test_1.id, + "command_id": command.id, + "plan_log_id": plan_log.id, + "variable_values": {"__git_project__": git_project.reference}, + } + ) + + # Call the method directly to test the custom values path + file = self.server_test_1._command_runner_file_using_template_create_file( + log_record=command_log, server_dir="/var/test" + ) + + # Verify the file was created + self.assertTrue(file, "File should have been created") + + # Verify the git project relation exists + git_project_rel = self.GitProjectRel.search( + [ + ("server_id", "=", self.server_test_1.id), + ("git_project_id", "=", git_project.id), + ("file_id", "=", file.id), + ] + ) + + self.assertEqual( + len(git_project_rel), 1, "Exactly one git project relation should exist" + ) + self.assertEqual( + git_project_rel.file_id, + file, + "The related file should be the git config file", + ) + self.assertEqual( + git_project_rel.git_project_id, + git_project, + "The related git project should match the one from custom values", + ) + self.assertEqual( + git_project_rel.project_format, + git_project._default_project_format(), + "Project format should match the default format", + ) + + def test_file_creation_with_git_project_from_custom_values_priority(self): + """Test that git project from custom values takes priority + over git project from plan line. + """ + git_project_custom = self.GitProject.create( + { + "name": "Test Git Project From Custom Values Priority", + "manager_ids": [(4, self.manager.id)], + } + ) + + git_project_plan_line = self.GitProject.create( + { + "name": "Test Git Project From Plan Line", + "manager_ids": [(4, self.manager.id)], + } + ) + + file_template = self.FileTemplate.create( + { + "name": "Git Config Template Priority", + "file_name": "repos_priority.yaml", + "server_dir": "/var/test", + "code": "repositories:\n test_repo:\n " + "url: https://github.com/test/repo.git\n target: main", + } + ) + + command = self.Command.create( + { + "name": "Create Git Config File Priority", + "action": "file_using_template", + "file_template_id": file_template.id, + } + ) + + flight_plan = self.Plan.create( + { + "name": "Git Project Setup Plan Priority", + "note": "Tests priority of custom values over plan line", + } + ) + + # Create plan line WITH git_project_id set + # But custom values should take priority + plan_line = self.plan_line.create( + { + "plan_id": flight_plan.id, + "command_id": command.id, + "sequence": 10, + "git_project_id": git_project_plan_line.id, + } + ) + + # Create plan log + plan_log = self.env["cx.tower.plan.log"].create( + { + "server_id": self.server_test_1.id, + "plan_id": flight_plan.id, + "plan_line_executed_id": plan_line.id, + } + ) + + # Create command log with variable_values containing __git_project__ + # This should take priority over plan_line.git_project_id + command_log = self.CommandLog.create( + { + "server_id": self.server_test_1.id, + "command_id": command.id, + "plan_log_id": plan_log.id, + "variable_values": {"__git_project__": git_project_custom.reference}, + } + ) + + # Call the method directly to test the custom values path + file = self.server_test_1._command_runner_file_using_template_create_file( + log_record=command_log, server_dir="/var/test" + ) + + # Verify the file was created + self.assertTrue(file, "File should have been created") + + # Verify the git project relation uses the git project from custom values + # (not the one from plan line) + git_project_rel = self.GitProjectRel.search( + [ + ("server_id", "=", self.server_test_1.id), + ("git_project_id", "=", git_project_custom.id), + ("file_id", "=", file.id), + ] + ) + + self.assertEqual( + len(git_project_rel), 1, "Exactly one git project relation should exist" + ) + self.assertEqual( + git_project_rel.git_project_id, + git_project_custom, + "The related git project should match the one from custom values, " + "not from plan line", + ) + + # Verify that the plan line git project was NOT used + git_project_rel_plan_line = self.GitProjectRel.search( + [ + ("server_id", "=", self.server_test_1.id), + ("git_project_id", "=", git_project_plan_line.id), + ("file_id", "=", file.id), + ] + ) + self.assertEqual( + len(git_project_rel_plan_line), + 0, + "No relation should exist for the plan line git project", + ) + + def test_server_get_servers_by_git_ref_success(self): + """Check the success case of server.get_servers_by_git_ref""" + + # 1. URL only + servers = self.Server.get_servers_by_git_ref( + self.remote_github_https.repo_id.url + ) + self.assertEqual(servers, self.server_test_1) + + # 2. Specific URL with specific head + servers = self.Server.get_servers_by_git_ref( + self.remote_github_https.repo_id.url, "123" + ) + self.assertEqual(servers, self.server_test_1) + + # 2. Specific URL with specific head and head type + servers = self.Server.get_servers_by_git_ref( + self.remote_github_https.repo_id.url, "123", "pr" + ) + self.assertEqual(servers, self.server_test_1) + + def test_server_get_servers_by_git_ref_no_match(self): + """Check the no match case of server.get_servers_by_git_ref""" + + # 1. Repo link does not exist + servers = self.Server.get_servers_by_git_ref( + "https://github.com/other-org/other-repo.git", "main", "branch" + ) + self.assertFalse(servers) + + # 2. Repo link exists, but remote does not exist + servers = self.Server.get_servers_by_git_ref( + self.repo_cetmix_tower.url, "3311", "pr" + ) + self.assertFalse(servers) + + # 3. Repo link exists, but remote type does not exist + servers = self.Server.get_servers_by_git_ref( + self.repo_cetmix_tower.url, "main", "commit" + ) + self.assertFalse(servers) diff --git a/addons/cetmix_tower_git/tests/test_source.py b/addons/cetmix_tower_git/tests/test_source.py new file mode 100644 index 0000000..de67eef --- /dev/null +++ b/addons/cetmix_tower_git/tests/test_source.py @@ -0,0 +1,226 @@ +from odoo.exceptions import AccessError + +from .common import CommonTest + + +class TestSource(CommonTest): + """Test class for git source.""" + + @classmethod + def setUpClass(cls): + super().setUpClass() + # Create another manager for testing + cls.manager_2 = cls.Users.create( + { + "name": "Second Manager", + "login": "manager2", + "email": "manager2@test.com", + "groups_id": [(4, cls.env.ref("cetmix_tower_server.group_manager").id)], + } + ) + + # Create test project and source as root + cls.project = cls.GitProject.create( + { + "name": "Test Project", + } + ) + cls.source = cls.GitSource.create( + { + "name": "Test Source", + "git_project_id": cls.project.id, + } + ) + + def test_user_access(self): + """Test that regular users have no access to git sources""" + user_source = self.GitSource.with_user(self.user) + + # Test CRUD operations + with self.assertRaises(AccessError): + user_source.create( + { + "name": "New Source", + "git_project_id": self.project.id, + } + ) + with self.assertRaises(AccessError): + user_source.browse(self.source.id).read(["name"]) + with self.assertRaises(AccessError): + user_source.browse(self.source.id).write({"name": "Updated Name"}) + with self.assertRaises(AccessError): + user_source.browse(self.source.id).unlink() + + def test_manager_read_access(self): + """Test manager read access rules""" + manager_source = self.GitSource.with_user(self.manager) + + # Manager not in project user_ids or manager_ids - should not read + with self.assertRaises(AccessError): + manager_source.browse(self.source.id).read(["name"]) + + # Add manager to project user_ids - should read + self.project.write({"user_ids": [(4, self.manager.id)]}) + self.assertEqual(manager_source.browse(self.source.id).name, "Test Source") + + # Remove from user_ids, add to manager_ids - should read + self.project.write( + {"user_ids": [(3, self.manager.id)], "manager_ids": [(4, self.manager.id)]} + ) + self.assertEqual(manager_source.browse(self.source.id).name, "Test Source") + + def test_manager_write_access(self): + """Test manager write/create access rules""" + manager_source = self.GitSource.with_user(self.manager) + + # Create project as manager - should be added to manager_ids automatically + project = self.GitProject.with_user(self.manager).create( + { + "name": "Manager Project", + } + ) + self.assertIn(self.manager, project.manager_ids) + + # Create source in own project - should succeed + new_source = manager_source.create( + { + "name": "New Source", + "git_project_id": project.id, + } + ) + self.assertTrue(new_source.exists()) + + # Write to own source - should succeed + new_source.write({"name": "Updated Name"}) + self.assertEqual(new_source.name, "Updated Name") + + # Write to other's source - should fail + with self.assertRaises(AccessError): + manager_source.browse(self.source.id).write({"name": "Updated Name"}) + + def test_manager_unlink_access(self): + """Test manager unlink access rules""" + # Create project and source as manager_2 + project = self.GitProject.with_user(self.manager_2).create( + { + "name": "Manager 2 Project", + } + ) + source = self.GitSource.with_user(self.manager_2).create( + { + "name": "Source to Delete", + "git_project_id": project.id, + } + ) + manager_source = self.GitSource.with_user(self.manager) + + # Try delete as different manager - should fail even if added to manager_ids + project.write({"manager_ids": [(4, self.manager.id)]}) + with self.assertRaises(AccessError): + manager_source.browse(source.id).unlink() + + # Create source as manager and try delete - should succeed + own_source = manager_source.create( + { + "name": "Own Source", + "git_project_id": project.id, + } + ) + self.assertTrue(own_source.exists()) + own_source.unlink() + self.assertFalse(own_source.exists()) + + def test_root_access(self): + """Test root access rules""" + root_source = self.GitSource.with_user(self.root) + + # Create + new_source = root_source.create( + { + "name": "Root Source", + "git_project_id": self.project.id, + } + ) + self.assertTrue(new_source.exists()) + + # Read + self.assertEqual(root_source.browse(self.source.id).name, "Test Source") + + # Write + root_source.browse(self.source.id).write({"name": "Updated by Root"}) + self.assertEqual(self.source.name, "Updated by Root") + + # Delete + new_source.unlink() + self.assertFalse(new_source.exists()) + + def test_source_git_aggregator_prepare_record(self): + """Test if source prepare record method works correctly.""" + + # -- 1 -- + # Source 1 + expected_result = { + "remotes": { + "remote_1": "https://github.com/cetmix-test/cetmix-tower-test.git", + "remote_2": "https://$GITLAB_TOKEN_NAME:$GITLAB_TOKEN@my.gitlab.com/cetmix-test/cetmix-tower-test.git", + "remote_3": "git@my.gitlab.com:cetmix-test/cetmix-tower-test.git", + }, + "merges": [ + {"remote": "remote_1", "ref": "refs/pull/123/head"}, + {"remote": "remote_2", "ref": "main"}, + {"remote": "remote_3", "ref": "10000000"}, + ], + "target": "remote_1", + } + prepared_result = self.git_source_1._git_aggregator_prepare_record() + self.assertEqual( + prepared_result, expected_result, "Prepared result is not correct" + ) + + def test_manager_server_based_access(self): + """Test manager access to sources through server relationships""" + manager_source = self.GitSource.with_user(self.manager) + + # Create a server where manager is a user + server = self.Server.create( + { + "name": "Test Server", + "ip_v4_address": "localhost", + "ssh_username": "admin", + "ssh_password": "password", + "os_id": self.os_debian_10.id, + "user_ids": [(4, self.manager.id)], + } + ) + + # Link project to server + file = self.File.create( + { + "name": "test_file", + "server_id": server.id, + } + ) + self.GitProjectRel.create( + { + "server_id": server.id, + "file_id": file.id, + "git_project_id": self.project.id, + "project_format": "git_aggregator", + } + ) + + # Manager should be able to read source through server relationship + self.assertEqual(manager_source.browse(self.source.id).name, "Test Source") + + # Remove manager from server users + server.write({"user_ids": [(3, self.manager.id)]}) + + # Manager should not be able to read source anymore + with self.assertRaises(AccessError): + manager_source.browse(self.source.id).read(["name"]) + + # Add manager to server managers + server.write({"manager_ids": [(4, self.manager.id)]}) + + # Manager should be able to read source again + self.assertEqual(manager_source.browse(self.source.id).name, "Test Source") diff --git a/addons/cetmix_tower_git/tools/git_aggregator.py b/addons/cetmix_tower_git/tools/git_aggregator.py new file mode 100644 index 0000000..e69de29 diff --git a/addons/cetmix_tower_git/views/cx_tower_file_template_views.xml b/addons/cetmix_tower_git/views/cx_tower_file_template_views.xml new file mode 100644 index 0000000..d2dff20 --- /dev/null +++ b/addons/cetmix_tower_git/views/cx_tower_file_template_views.xml @@ -0,0 +1,16 @@ + + + + cx.tower.file.template.view.form + cx.tower.file.template + + + + + + + + diff --git a/addons/cetmix_tower_git/views/cx_tower_file_views.xml b/addons/cetmix_tower_git/views/cx_tower_file_views.xml new file mode 100644 index 0000000..bbf23f4 --- /dev/null +++ b/addons/cetmix_tower_git/views/cx_tower_file_views.xml @@ -0,0 +1,13 @@ + + + + cx.tower.file.view.form + cx.tower.file + + + + + + + + diff --git a/addons/cetmix_tower_git/views/cx_tower_git_project_views.xml b/addons/cetmix_tower_git/views/cx_tower_git_project_views.xml new file mode 100644 index 0000000..62d66ed --- /dev/null +++ b/addons/cetmix_tower_git/views/cx_tower_git_project_views.xml @@ -0,0 +1,291 @@ + + + + + cx.tower.git.project.list + cx.tower.git.project + + +
+
+ + + +
+
+
+ + + + cx.tower.git.project.form + cx.tower.git.project + +
+
+
+ + +
+

+ +

+

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+

+ * Sources where all remotes are private +

+
+
+

+ * Sources where some remotes are private +

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + + + cx.tower.git.repo.search + cx.tower.git.repo + + + + + + + + + + + + + + + + + + + + + + + + + + Repositories + cetmix_tower_git_repositories + cx.tower.git.repo + list,form + +

+ Create your first repository! +

+

+ Repositories represent git repositories with their metadata and configuration. + They can be linked to remotes to automatically populate URL information. +

+
+
+
diff --git a/addons/cetmix_tower_git/views/cx_tower_git_source_views.xml b/addons/cetmix_tower_git/views/cx_tower_git_source_views.xml new file mode 100644 index 0000000..b6dcd79 --- /dev/null +++ b/addons/cetmix_tower_git/views/cx_tower_git_source_views.xml @@ -0,0 +1,93 @@ + + + + + cx.tower.git.source.list + cx.tower.git.source + + + + + + + + + + + cx.tower.git.source.form + cx.tower.git.source + +
+ + +
+

+ +

+

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
diff --git a/addons/cetmix_tower_git/views/cx_tower_plan_line_view.xml b/addons/cetmix_tower_git/views/cx_tower_plan_line_view.xml new file mode 100644 index 0000000..851ac2a --- /dev/null +++ b/addons/cetmix_tower_git/views/cx_tower_plan_line_view.xml @@ -0,0 +1,36 @@ + + + + cx.tower.plan.line.view.form + cx.tower.plan.line + + + + + + + + + + + + diff --git a/addons/cetmix_tower_git/views/cx_tower_server_view.xml b/addons/cetmix_tower_git/views/cx_tower_server_view.xml new file mode 100644 index 0000000..9caf64e --- /dev/null +++ b/addons/cetmix_tower_git/views/cx_tower_server_view.xml @@ -0,0 +1,43 @@ + + + + cx.tower.server.view.form.shortcuts + cx.tower.server + + + + + + + + + + + +