Compare commits
60 Commits
cetmix_tow
...
cetmix_tow
| Author | SHA1 | Date | |
|---|---|---|---|
| 21576ec28f | |||
| a5b60a5d3b | |||
| abcb71d469 | |||
| 4fdf6333f2 | |||
| c18aba668b | |||
| 0a8333e1e2 | |||
| 3d19db5049 | |||
| 62e7767925 | |||
| 070c89e75e | |||
| cf0d897dfa | |||
| 9fc1c6bd65 | |||
| 342e616963 | |||
| a4c6f5c561 | |||
| 811d32c5be | |||
| 135074c040 | |||
| af55099d83 | |||
| 2d9f32fc2f | |||
| d361711043 | |||
| 426c0e0792 | |||
| ae451e5911 | |||
| 31bcb48704 | |||
| 25703173fb | |||
| eab2080115 | |||
| 5f99227e6c | |||
| 4a547632ac | |||
| 8cd9bae8ea | |||
| ddadefa9a6 | |||
| 7276688114 | |||
| 66450d4d02 | |||
| 2d0bda98b1 | |||
| 89943c26eb | |||
| 0ac25c7405 | |||
| b54c955847 | |||
| 857ec4fceb | |||
| 83ff1a0ec5 | |||
| ef85be3808 | |||
| 25b80d98ce | |||
| 1871e1ffe9 | |||
| 4440daa0a4 | |||
| 6e018447b2 | |||
| 5c4949bf5b | |||
| 90cb176847 | |||
| bdf8278b7f | |||
| da1f2fd426 | |||
| 4b1cbbc86b | |||
| 0957e4d55b | |||
| 6509c2136f | |||
| 52877f9b2c | |||
| 6176d27861 | |||
| 37a160148d | |||
| ee1501034b | |||
| 92e62ae21b | |||
| 7a5d6aa254 | |||
| 42a4abb176 | |||
| 440324c078 | |||
| 7e9e92a179 | |||
| ad62d49f3d | |||
| a562808d99 | |||
| 7f6a00a8f7 | |||
| f04db1b076 |
125
addons/cetmix_tower_git/README.rst
Normal file
125
addons/cetmix_tower_git/README.rst
Normal file
@@ -0,0 +1,125 @@
|
||||
================
|
||||
Cetmix Tower Git
|
||||
================
|
||||
|
||||
..
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! This file is generated by oca-gen-addon-readme !!
|
||||
!! changes will be overwritten. !!
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! source digest: sha256:583744e8956f294682a551fc082f086b174b8d2b72652c21b1dd68f3933e7211
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
|
||||
.. |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/16.0/cetmix_tower_git
|
||||
:alt: cetmix/cetmix-tower
|
||||
|
||||
|badge1| |badge2| |badge3|
|
||||
|
||||
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.
|
||||
|
||||
**Table of contents**
|
||||
|
||||
.. contents::
|
||||
:local:
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
Please refer to the `official
|
||||
documentation <https://cetmix.com/tower>`__ for detailed configuration
|
||||
instructions.
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
Please refer to the `official
|
||||
documentation <https://cetmix.com/tower>`__ for detailed usage
|
||||
instructions.
|
||||
|
||||
Changelog
|
||||
=========
|
||||
|
||||
16.0.2.0.1 (2025-12-11)
|
||||
-----------------------
|
||||
|
||||
- Features: Improve search views, implement the search panel for
|
||||
selected views. (5139)
|
||||
|
||||
16.0.2.0.0 (2025-10-27)
|
||||
-----------------------
|
||||
|
||||
- Features: Major refactoring: implement Git repository entity. (4914)
|
||||
|
||||
16.0.1.0.6 (2025-08-18)
|
||||
-----------------------
|
||||
|
||||
- Features: Link or copy a git project when uploading the linked file
|
||||
using command (4759)
|
||||
|
||||
16.0.1.0.5 (2025-08-17)
|
||||
-----------------------
|
||||
|
||||
- Features: Search servers by git reference (4838)
|
||||
|
||||
16.0.1.0.4 (2025-07-29)
|
||||
-----------------------
|
||||
|
||||
- Features: Export related commands and flight plans together with
|
||||
server (4849)
|
||||
|
||||
16.0.1.0.3 (2025-05-23)
|
||||
-----------------------
|
||||
|
||||
- Bugfixes: Duplicated file is created when importing a YAML file with a
|
||||
git project. (4715)
|
||||
|
||||
16.0.1.0.2 (2025-05-16)
|
||||
-----------------------
|
||||
|
||||
- Features: Record references for git relations. (4670)
|
||||
|
||||
16.0.1.0.1 (2025-05-09)
|
||||
-----------------------
|
||||
|
||||
- Bugfixes: Non-critical issues and performance improvements. (4663)
|
||||
|
||||
16.0.1.0.0
|
||||
----------
|
||||
|
||||
Release for Odoo 16.0
|
||||
|
||||
Bug Tracker
|
||||
===========
|
||||
|
||||
Bugs are tracked on `GitHub Issues <https://github.com/cetmix/cetmix-tower/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 <https://github.com/cetmix/cetmix-tower/issues/new?body=module:%20cetmix_tower_git%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
|
||||
|
||||
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 <https://github.com/cetmix/cetmix-tower/tree/16.0/cetmix_tower_git>`_ project on GitHub.
|
||||
|
||||
You are welcome to contribute.
|
||||
1
addons/cetmix_tower_git/__init__.py
Normal file
1
addons/cetmix_tower_git/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import models
|
||||
40
addons/cetmix_tower_git/__manifest__.py
Normal file
40
addons/cetmix_tower_git/__manifest__.py
Normal file
@@ -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": "16.0.2.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",
|
||||
],
|
||||
}
|
||||
166
addons/cetmix_tower_git/demo/demo_data.xml
Normal file
166
addons/cetmix_tower_git/demo/demo_data.xml
Normal file
@@ -0,0 +1,166 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo noupdate="1">
|
||||
|
||||
<!-- Git Project -->
|
||||
<record id="git_project_demo" model="cx.tower.git.project">
|
||||
<field name="name">Demo Git Project</field>
|
||||
<field name="reference">demo_git_project</field>
|
||||
<field name="note">This is a demo git project.</field>
|
||||
</record>
|
||||
<!-- Repositories -->
|
||||
<record id="repo_demo_cetmix_tower" model="cx.tower.git.repo">
|
||||
<field name="url">https://github.com/cetmix-demo/cetmix-tower-demo.git</field>
|
||||
</record>
|
||||
<record id="repo_demo_oca_web" model="cx.tower.git.repo">
|
||||
<field name="url">https://github.com/oca-demo/web-demo.git</field>
|
||||
</record>
|
||||
<record id="repo_demo_odoo_enterprise" model="cx.tower.git.repo">
|
||||
<field name="url">https://github.com/odoo-demo/enterprise-demo.git</field>
|
||||
<field name="is_private" eval="True" />
|
||||
</record>
|
||||
<record id="repo_demo_gitlab_private" model="cx.tower.git.repo">
|
||||
<field name="url">https://gitlab.com/cetmix-demo/cetmix-tower-demo.git</field>
|
||||
<field name="is_private" eval="True" />
|
||||
</record>
|
||||
<record id="repo_demo_bitbucket_private" model="cx.tower.git.repo">
|
||||
<field
|
||||
name="url"
|
||||
>https://bitbucket.com/cetmix-demo/cetmix-tower-demo-enterprise.git</field>
|
||||
<field name="is_private" eval="True" />
|
||||
</record>
|
||||
<!-- Sources -->
|
||||
<!-- Cetmix Tower -->
|
||||
<record id="source_demo_cetmix_tower" model="cx.tower.git.source">
|
||||
<field name="name">Cetmix Tower</field>
|
||||
<field name="reference">cetmix_tower</field>
|
||||
<field name="git_project_id" ref="git_project_demo" />
|
||||
</record>
|
||||
<!-- Remotes-->
|
||||
<record id="remote_demo_cetmix_tower_14_0_dev" model="cx.tower.git.remote">
|
||||
<field name="source_id" ref="source_demo_cetmix_tower" />
|
||||
<field name="repo_id" ref="repo_demo_cetmix_tower" />
|
||||
<field name="head_type">branch</field>
|
||||
<field name="head">14.0</field>
|
||||
</record>
|
||||
<record id="remote_demo_cetmix_tower_pr_176" model="cx.tower.git.remote">
|
||||
<field name="source_id" ref="source_demo_cetmix_tower" />
|
||||
<field name="repo_id" ref="repo_demo_cetmix_tower" />
|
||||
<field name="head_type">pr</field>
|
||||
<field name="head">176</field>
|
||||
</record>
|
||||
<!-- OCA Web -->
|
||||
<record id="source_demo_oca_web" model="cx.tower.git.source">
|
||||
<field name="name">OCA Web</field>
|
||||
<field name="reference">oca_web</field>
|
||||
<field name="git_project_id" ref="git_project_demo" />
|
||||
</record>
|
||||
<!-- Remotes -->
|
||||
<record id="remote_demo_oca_web_14_0" model="cx.tower.git.remote">
|
||||
<field name="source_id" ref="source_demo_oca_web" />
|
||||
<field name="repo_id" ref="repo_demo_oca_web" />
|
||||
<field name="head_type">branch</field>
|
||||
<field name="head">14.0</field>
|
||||
</record>
|
||||
<!-- Odoo Enterprise -->
|
||||
<record id="source_demo_odoo_enterprise" model="cx.tower.git.source">
|
||||
<field name="name">Odoo Enterprise (Private)</field>
|
||||
<field name="reference">odoo_enterprise</field>
|
||||
<field name="git_project_id" ref="git_project_demo" />
|
||||
</record>
|
||||
<!-- Remotes -->
|
||||
<record id="remote_demo_odoo_enterprise" model="cx.tower.git.remote">
|
||||
<field name="source_id" ref="source_demo_odoo_enterprise" />
|
||||
<field name="repo_id" ref="repo_demo_odoo_enterprise" />
|
||||
<field name="head_type">branch</field>
|
||||
<field name="head">19.0</field>
|
||||
<field name="is_private" eval="True" />
|
||||
</record>
|
||||
<!-- Sample Private Gitlab -->
|
||||
<record id="source_demo_gitlab_private" model="cx.tower.git.source">
|
||||
<field name="name">Sample Semi Private Gitlab</field>
|
||||
<field name="reference">gitlab_private</field>
|
||||
<field name="git_project_id" ref="git_project_demo" />
|
||||
</record>
|
||||
<!-- Remotes -->
|
||||
<record id="remote_demo_gitlab_private_main" model="cx.tower.git.remote">
|
||||
<field name="source_id" ref="source_demo_gitlab_private" />
|
||||
<field name="repo_id" ref="repo_demo_gitlab_private" />
|
||||
<field name="head_type">branch</field>
|
||||
<field name="head">main</field>
|
||||
</record>
|
||||
<record id="remote_demo_gitlab_private_mr_1234" model="cx.tower.git.remote">
|
||||
<field name="source_id" ref="source_demo_gitlab_private" />
|
||||
<field name="repo_id" ref="repo_demo_gitlab_private" />
|
||||
<field name="head_type">pr</field>
|
||||
<field name="head">1234</field>
|
||||
</record>
|
||||
<!-- Sample Private Bitbucket -->
|
||||
<record id="source_demo_bitbucket_private" model="cx.tower.git.source">
|
||||
<field name="name">Sample Private Bitbucket</field>
|
||||
<field name="reference">bitbucket_private</field>
|
||||
<field name="git_project_id" ref="git_project_demo" />
|
||||
</record>
|
||||
<!-- Remotes -->
|
||||
<record id="remote_demo_bitbucket_private_main" model="cx.tower.git.remote">
|
||||
<field name="source_id" ref="source_demo_bitbucket_private" />
|
||||
<field name="repo_id" ref="repo_demo_bitbucket_private" />
|
||||
<field name="head_type">branch</field>
|
||||
<field name="head">dev</field>
|
||||
</record>
|
||||
<record id="remote_demo_bitbucket_private_feature" model="cx.tower.git.remote">
|
||||
<field name="source_id" ref="source_demo_bitbucket_private" />
|
||||
<field name="repo_id" ref="repo_demo_bitbucket_private" />
|
||||
<field name="head_type">commit</field>
|
||||
<field name="head">1234567890</field>
|
||||
</record>
|
||||
|
||||
<!-- Files -->
|
||||
<record id="file_demo_cetmix_tower_14_0_dev" model="cx.tower.file">
|
||||
<field name="name">repos.yaml</field>
|
||||
<field name="server_id" ref="cetmix_tower_server.server_demo_1" />
|
||||
<field name="source">tower</field>
|
||||
<field name="file_type">text</field>
|
||||
<field name="server_dir">{{ instance_name }}/config</field>
|
||||
</record>
|
||||
|
||||
<!-- Link file to git project -->
|
||||
<record
|
||||
id="git_project_rel_demo_cetmix_tower_14_0_dev"
|
||||
model="cx.tower.git.project.rel"
|
||||
>
|
||||
<field name="git_project_id" ref="git_project_demo" />
|
||||
<field name="server_id" ref="cetmix_tower_server.server_demo_1" />
|
||||
<field name="file_id" ref="file_demo_cetmix_tower_14_0_dev" />
|
||||
<field name="project_format">git_aggregator</field>
|
||||
</record>
|
||||
|
||||
<!-- Demo variable for testing giturlparse -->
|
||||
<record id="variable_demo_git_url" model="cx.tower.variable">
|
||||
<field name="name">Demo Git URL</field>
|
||||
<field name="reference">demo_git_url</field>
|
||||
</record>
|
||||
|
||||
<!-- Demo command to test giturlparse -->
|
||||
<record id="command_demo_git_url" model="cx.tower.command">
|
||||
<field name="name">Parse Git URL</field>
|
||||
<field name="action">python_code</field>
|
||||
<field name="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!"}
|
||||
</field>
|
||||
<field name="access_level">1</field>
|
||||
<field
|
||||
name="tag_ids"
|
||||
eval="[(6, 0, [ref('cetmix_tower_server.tag_custom')])]"
|
||||
/>
|
||||
<field name="note">Run Python Code: Check Branch</field>
|
||||
</record>
|
||||
</odoo>
|
||||
1029
addons/cetmix_tower_git/i18n/cetmix_tower_git.pot
Normal file
1029
addons/cetmix_tower_git/i18n/cetmix_tower_git.pot
Normal file
File diff suppressed because it is too large
Load Diff
595
addons/cetmix_tower_git/i18n/fi.po
Normal file
595
addons/cetmix_tower_git/i18n/fi.po
Normal file
@@ -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 16.0\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 ""
|
||||
"<b>Managers.</b> All users who have \"Manager\" group and are set as \"Managers\" in <b><u>all</u></b> 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 ""
|
||||
"<b>Users.</b> All users who have \"Manager\" group and are either set in "
|
||||
"\"Users\" or in \"Managers\" in <b><u>all</u></b> 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 ""
|
||||
635
addons/cetmix_tower_git/i18n/hr.po
Normal file
635
addons/cetmix_tower_git/i18n/hr.po
Normal file
@@ -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 16.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2025-03-06 09:11+0000\n"
|
||||
"Last-Translator: Bole <bole@dajmi5.com>\n"
|
||||
"Language-Team: Croatian <https://hosted.weblate.org/projects/"
|
||||
"tower-server-14-0-dev/cetmix_tower_git/hr/>\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 ""
|
||||
"<b>Managers.</b> All users who have \"Manager\" group and are set as \"Managers\" in <b><u>all</u></b> 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 ""
|
||||
"<b>Manageri.</b> Svi korisnici koji imaju \"Manager\" grupu i postavljeni su "
|
||||
"kao \"Manageri\" u <b><u>svim</u></b> 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 ""
|
||||
"<b>Users.</b> All users who have \"Manager\" group and are either set in "
|
||||
"\"Users\" or in \"Managers\" in <b><u>all</u></b> related servers."
|
||||
msgstr ""
|
||||
"<b>Korisnici.</b> Svi korisnici koji imaju \"Manager\" grupu i postavljeni "
|
||||
"su ili kao \"Korisnik\" ili kao \"Manager\" u <b><u>svim</u></b> 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 ""
|
||||
1085
addons/cetmix_tower_git/i18n/it.po
Normal file
1085
addons/cetmix_tower_git/i18n/it.po
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,82 @@
|
||||
import logging
|
||||
|
||||
from odoo import SUPERUSER_ID, api
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def migrate(cr, version):
|
||||
"""
|
||||
Convert URLs in remotes to repositories.
|
||||
Add repo_id to remotes.
|
||||
"""
|
||||
|
||||
_logger.info(
|
||||
"Converting URLs in remotes to repositories and adding repo_id to remotes."
|
||||
)
|
||||
env = api.Environment(cr, SUPERUSER_ID, {})
|
||||
|
||||
# Fetch all remotes using SQL query Group them {"url": [remote_id, remote_id, ...]}
|
||||
cr.execute(
|
||||
"""
|
||||
SELECT url, array_agg(id) as remote_ids
|
||||
FROM cx_tower_git_remote
|
||||
GROUP BY url
|
||||
"""
|
||||
)
|
||||
remote_urls = cr.fetchall()
|
||||
remote_urls_dict = {url: remote_ids for url, remote_ids in remote_urls}
|
||||
|
||||
# Create repo for each url and add this repo to all remotes
|
||||
url_count = 0
|
||||
remote_obj = env["cx.tower.git.remote"]
|
||||
repo_obj = env["cx.tower.git.repo"]
|
||||
for url, remote_ids in remote_urls_dict.items():
|
||||
repo_id = repo_obj.name_create(url)[0]
|
||||
# Check if any of the remotes is private
|
||||
remotes = remote_obj.browse(remote_ids)
|
||||
is_private = bool(remotes.filtered(lambda r: r.is_private))
|
||||
|
||||
# Add repo to remotes
|
||||
# We are using SQL to avoid post-write triggers
|
||||
cr.execute(
|
||||
"""
|
||||
UPDATE cx_tower_git_remote
|
||||
SET repo_id = %s
|
||||
WHERE id = ANY(%s)
|
||||
""",
|
||||
(repo_id, remote_ids),
|
||||
)
|
||||
|
||||
# Update repo.is_private
|
||||
# We are using SQL to avoid post-write triggers
|
||||
if is_private:
|
||||
cr.execute(
|
||||
"""
|
||||
UPDATE cx_tower_git_repo
|
||||
SET is_private = true
|
||||
WHERE id = %s
|
||||
""",
|
||||
(repo_id,),
|
||||
)
|
||||
|
||||
url_count += 1
|
||||
|
||||
# Compute project_ids for repositories
|
||||
_logger.info("Computing project_ids for repositories.")
|
||||
remote_obj.invalidate_model()
|
||||
repo_obj.invalidate_model()
|
||||
repo_obj.search([])._compute_git_project_ids()
|
||||
|
||||
# Sanitize all remote heads that contain a slash
|
||||
# Use the SQL query to avoid post-write triggers
|
||||
_logger.info("Sanitizing remote heads that contain a slash.")
|
||||
cr.execute(
|
||||
"""
|
||||
UPDATE cx_tower_git_remote
|
||||
SET head = (regexp_match(head, '[^/]+$'))[1]
|
||||
WHERE head LIKE '%/%'
|
||||
"""
|
||||
)
|
||||
|
||||
_logger.info("Migration completed. %s unique urls processed", url_count)
|
||||
15
addons/cetmix_tower_git/models/__init__.py
Normal file
15
addons/cetmix_tower_git/models/__init__.py
Normal file
@@ -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
|
||||
35
addons/cetmix_tower_git/models/cetmix_tower.py
Normal file
35
addons/cetmix_tower_git/models/cetmix_tower.py
Normal file
@@ -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
|
||||
)
|
||||
37
addons/cetmix_tower_git/models/cx_tower_command.py
Normal file
37
addons/cetmix_tower_git/models/cx_tower_command.py
Normal file
@@ -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'. "
|
||||
" <a "
|
||||
"href='https://github.com/nephila/giturlparse/'"
|
||||
" target='_blank'>Documentation on GitHub</a>."
|
||||
),
|
||||
},
|
||||
}
|
||||
}
|
||||
)
|
||||
return custom_python_libraries
|
||||
47
addons/cetmix_tower_git/models/cx_tower_file.py
Normal file
47
addons/cetmix_tower_git/models/cx_tower_file.py
Normal file
@@ -0,0 +1,47 @@
|
||||
# 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,
|
||||
}
|
||||
)
|
||||
32
addons/cetmix_tower_git/models/cx_tower_file_template.py
Normal file
32
addons/cetmix_tower_git/models/cx_tower_file_template.py
Normal file
@@ -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
|
||||
)
|
||||
334
addons/cetmix_tower_git/models/cx_tower_git_project.py
Normal file
334
addons/cetmix_tower_git/models/cx_tower_git_project.py
Normal file
@@ -0,0 +1,334 @@
|
||||
# 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)
|
||||
# IMPORTANT: This field may contain duplicates because of the relation nature!
|
||||
server_ids = fields.Many2many(
|
||||
comodel_name="cx.tower.server",
|
||||
relation="cx_tower_git_project_rel",
|
||||
column1="git_project_id",
|
||||
column2="server_id",
|
||||
string="Servers",
|
||||
readonly=True,
|
||||
copy=False,
|
||||
help="Servers are added automatically based on the files linked to the project."
|
||||
"\nIMPORTANT: This field may contain duplicates"
|
||||
" because of the relation nature!",
|
||||
)
|
||||
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,
|
||||
domain=lambda self: [
|
||||
("groups_id", "in", self.env.ref("cetmix_tower_server.group_manager").ids)
|
||||
],
|
||||
)
|
||||
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.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
|
||||
|
||||
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.
|
||||
|
||||
Returns:
|
||||
List: List of variables
|
||||
"""
|
||||
variables = re.findall(r"\$([A-Z0-9_]+)", text)
|
||||
return sorted(list(set(variables)))
|
||||
|
||||
# ------------------------------
|
||||
# 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 ""
|
||||
@@ -0,0 +1,113 @@
|
||||
# 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_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__<format> 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(
|
||||
self.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})
|
||||
177
addons/cetmix_tower_git/models/cx_tower_git_project_rel.py
Normal file
177
addons/cetmix_tower_git/models/cx_tower_git_project_rel.py
Normal file
@@ -0,0 +1,177 @@
|
||||
# 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_<format> 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(
|
||||
self.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
|
||||
415
addons/cetmix_tower_git/models/cx_tower_git_remote.py
Normal file
415
addons/cetmix_tower_git/models/cx_tower_git_remote.py
Normal file
@@ -0,0 +1,415 @@
|
||||
# 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"
|
||||
|
||||
# Used to detect git ssh urls
|
||||
GIT_SSH_URL_PATTERN = r"^[\w\.-]+@[\w\.-]+:.*\.git$"
|
||||
GIT_HTTPS_URL_PATTERN = r"^https://.*\.git$"
|
||||
GIT_GIT_URL_PATTERN = r"^git://.*\.git$"
|
||||
|
||||
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_<position>`
|
||||
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
|
||||
"""
|
||||
related_files = self.mapped("git_project_id").mapped("git_project_rel_ids")
|
||||
related_templates = self.mapped("git_project_id").mapped(
|
||||
"git_project_file_template_rel_ids"
|
||||
)
|
||||
res = super().unlink()
|
||||
|
||||
# Update related files and templates on unlink
|
||||
if related_files:
|
||||
related_files._save_to_file()
|
||||
if related_templates:
|
||||
related_templates._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
|
||||
|
||||
@api.model
|
||||
def get_head_data(self):
|
||||
"""
|
||||
This method is used to get values for the dropdown dynamic widget.
|
||||
It is designed for integrations with repo providers using APIs.
|
||||
|
||||
Returns:
|
||||
List: List of tuples(selection, name)
|
||||
eg [('18.0', '18.0'), ('main', 'main'), ('develop', 'develop')]
|
||||
"""
|
||||
values = [
|
||||
("18.0", "18.0"),
|
||||
("main", "Main"),
|
||||
("develop", "Develop"),
|
||||
("17.0", "17.0"),
|
||||
]
|
||||
return values
|
||||
|
||||
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_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()
|
||||
|
||||
# This is how final url will look like
|
||||
# https://$GITHUB_TOKEN:x-oauth-basic@github.com/soem_org/some_private_repo.git
|
||||
url_without_protocol = url.replace("https://", "")
|
||||
url = f"https://$GITHUB_TOKEN:x-oauth-basic@{url_without_protocol}"
|
||||
return url
|
||||
|
||||
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()
|
||||
|
||||
# This is how final url will look like
|
||||
# https://<token-name>:<token-value>@<gitlaburl-repository>.git
|
||||
url_without_protocol = url.replace("https://", "")
|
||||
url = f"https://$GITLAB_TOKEN_NAME:$GITLAB_TOKEN@{url_without_protocol}"
|
||||
return url
|
||||
|
||||
def _git_aggregator_prepare_url_bitbucket(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()
|
||||
|
||||
# This is how final url will look like
|
||||
# https://x-token-auth:{access_token}@bitbucket.org/user/repo.git
|
||||
# From https://support.atlassian.com/bitbucket-cloud/docs/use-oauth-on-bitbucket-cloud/
|
||||
url_without_protocol = url.replace("https://", "")
|
||||
url = f"https://x-token-auth:$BITBUCKET_TOKEN@{url_without_protocol}"
|
||||
return url
|
||||
|
||||
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 _git_aggregator_prepare_head_github(self):
|
||||
"""Prepare head for git aggregator for Github.
|
||||
|
||||
Returns:
|
||||
Char: Prepared head for git aggregator
|
||||
"""
|
||||
|
||||
# Extract branch name, PR/MR or commit number from head
|
||||
head_number = self.head.split("/")[-1]
|
||||
if not head_number:
|
||||
raise ValidationError(
|
||||
_("Git Aggregator: " "Head number is empty in %(head)s", head=self.head)
|
||||
)
|
||||
|
||||
# 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
|
||||
"""
|
||||
# Extract branch name, PR/MR or commit number from head
|
||||
head_number = self.head.split("/")[-1]
|
||||
if not head_number:
|
||||
raise ValidationError(
|
||||
_("Git Aggregator: " "Head number is empty in %(head)s", head=self.head)
|
||||
)
|
||||
|
||||
# PR/MR
|
||||
if self.head_type == "pr":
|
||||
return f"merge-requests/{head_number}/head"
|
||||
|
||||
# Commit
|
||||
# https://gitlab.com/cetmix/test/-/tree/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
|
||||
"""
|
||||
# Extract branch name, PR/MR or commit number from head
|
||||
head_number = self.head.split("/")[-1]
|
||||
if not head_number:
|
||||
raise ValidationError(
|
||||
_("Git Aggregator: " "Head number is empty in %(head)s", head=self.head)
|
||||
)
|
||||
# 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,
|
||||
)
|
||||
)
|
||||
|
||||
# Commit
|
||||
if self.head_type in ["commit", "branch"]:
|
||||
return f"{head_number}"
|
||||
|
||||
# Fallback to original head
|
||||
return self.head
|
||||
409
addons/cetmix_tower_git/models/cx_tower_git_repo.py
Normal file
409
addons/cetmix_tower_git/models/cx_tower_git_repo.py
Normal file
@@ -0,0 +1,409 @@
|
||||
# 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
|
||||
from odoo.tools import ormcache
|
||||
|
||||
|
||||
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", "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:
|
||||
projects = repo.remote_ids.mapped("git_project_id")
|
||||
repo.git_project_ids = [(6, 0, projects.ids)]
|
||||
|
||||
@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
|
||||
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)
|
||||
# Otherwise, add to create list
|
||||
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.clear_caches()
|
||||
return res
|
||||
|
||||
def write(self, vals):
|
||||
"""Write repositories."""
|
||||
res = super().write(vals)
|
||||
self.clear_caches()
|
||||
return res
|
||||
|
||||
def unlink(self):
|
||||
"""Unlink repositories."""
|
||||
res = super().unlink()
|
||||
self.clear_caches()
|
||||
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")
|
||||
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
|
||||
107
addons/cetmix_tower_git/models/cx_tower_git_repo_owner.py
Normal file
107
addons/cetmix_tower_git/models/cx_tower_git_repo_owner.py
Normal file
@@ -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")
|
||||
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.clear_caches()
|
||||
return res
|
||||
|
||||
def write(self, vals):
|
||||
"""Clear cache on write."""
|
||||
res = super().write(vals)
|
||||
if "name" in vals:
|
||||
self.clear_caches()
|
||||
return res
|
||||
|
||||
def unlink(self):
|
||||
"""Clear cache on unlink."""
|
||||
res = super().unlink()
|
||||
self.clear_caches()
|
||||
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
|
||||
189
addons/cetmix_tower_git/models/cx_tower_git_source.py
Normal file
189
addons/cetmix_tower_git/models/cx_tower_git_source.py
Normal file
@@ -0,0 +1,189 @@
|
||||
# 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
|
||||
res._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
|
||||
"""
|
||||
related_files = self.mapped("git_project_id").mapped("git_project_rel_ids")
|
||||
related_templates = self.mapped("git_project_id").mapped(
|
||||
"git_project_file_template_rel_ids"
|
||||
)
|
||||
res = super().unlink()
|
||||
# Update related files and templates on unlink
|
||||
if related_files:
|
||||
related_files._save_to_file()
|
||||
if related_templates:
|
||||
related_templates._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
|
||||
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
|
||||
32
addons/cetmix_tower_git/models/cx_tower_plan_line.py
Normal file
32
addons/cetmix_tower_git/models/cx_tower_plan_line.py
Normal file
@@ -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
|
||||
156
addons/cetmix_tower_git/models/cx_tower_server.py
Normal file
156
addons/cetmix_tower_git/models/cx_tower_server.py
Normal file
@@ -0,0 +1,156 @@
|
||||
# Copyright (C) 2024 Cetmix OÜ
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
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,
|
||||
file_template_id,
|
||||
server_dir,
|
||||
plan_line,
|
||||
if_file_exists,
|
||||
**kwargs,
|
||||
):
|
||||
"""Override to create git project relation
|
||||
when creating a file using a template.
|
||||
"""
|
||||
file = super()._command_runner_file_using_template_create_file(
|
||||
file_template_id, server_dir, plan_line, if_file_exists, **kwargs
|
||||
)
|
||||
if file and plan_line:
|
||||
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()
|
||||
|
||||
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
|
||||
3
addons/cetmix_tower_git/pyproject.toml
Normal file
3
addons/cetmix_tower_git/pyproject.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
[build-system]
|
||||
requires = ["whool"]
|
||||
build-backend = "whool.buildapi"
|
||||
1
addons/cetmix_tower_git/readme/CONFIGURE.md
Normal file
1
addons/cetmix_tower_git/readme/CONFIGURE.md
Normal file
@@ -0,0 +1 @@
|
||||
Please refer to the [official documentation](https://cetmix.com/tower) for detailed configuration instructions.
|
||||
3
addons/cetmix_tower_git/readme/DESCRIPTION.md
Normal file
3
addons/cetmix_tower_git/readme/DESCRIPTION.md
Normal file
@@ -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.
|
||||
43
addons/cetmix_tower_git/readme/HISTORY.md
Normal file
43
addons/cetmix_tower_git/readme/HISTORY.md
Normal file
@@ -0,0 +1,43 @@
|
||||
## 16.0.2.0.1 (2025-12-11)
|
||||
|
||||
- Features: Improve search views, implement the search panel for selected views. (5139)
|
||||
|
||||
|
||||
## 16.0.2.0.0 (2025-10-27)
|
||||
|
||||
- Features: Major refactoring: implement Git repository entity. (4914)
|
||||
|
||||
|
||||
## 16.0.1.0.6 (2025-08-18)
|
||||
|
||||
- Features: Link or copy a git project when uploading the linked file using command (4759)
|
||||
|
||||
|
||||
## 16.0.1.0.5 (2025-08-17)
|
||||
|
||||
- Features: Search servers by git reference (4838)
|
||||
|
||||
|
||||
## 16.0.1.0.4 (2025-07-29)
|
||||
|
||||
- Features: Export related commands and flight plans together with server (4849)
|
||||
|
||||
|
||||
## 16.0.1.0.3 (2025-05-23)
|
||||
|
||||
- Bugfixes: Duplicated file is created when importing a YAML file with a git project. (4715)
|
||||
|
||||
|
||||
## 16.0.1.0.2 (2025-05-16)
|
||||
|
||||
- Features: Record references for git relations. (4670)
|
||||
|
||||
|
||||
## 16.0.1.0.1 (2025-05-09)
|
||||
|
||||
- Bugfixes: Non-critical issues and performance improvements. (4663)
|
||||
|
||||
|
||||
## 16.0.1.0.0
|
||||
|
||||
Release for Odoo 16.0
|
||||
1
addons/cetmix_tower_git/readme/USAGE.md
Normal file
1
addons/cetmix_tower_git/readme/USAGE.md
Normal file
@@ -0,0 +1 @@
|
||||
Please refer to the [official documentation](https://cetmix.com/tower) for detailed usage instructions.
|
||||
@@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
|
||||
<!-- Manager Read Rule -->
|
||||
<record id="rule_git_project_file_template_rel_manager_read" model="ir.rule">
|
||||
<field
|
||||
name="name"
|
||||
>Git Project File Template Relation: Manager Read Access</field>
|
||||
<field name="model_id" ref="model_cx_tower_git_project_file_template_rel" />
|
||||
<field name="domain_force">['&',
|
||||
'|',
|
||||
('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])]</field>
|
||||
<field name="groups" eval="[(4, ref('cetmix_tower_server.group_manager'))]" />
|
||||
<field name="perm_read" eval="1" />
|
||||
<field name="perm_write" eval="0" />
|
||||
<field name="perm_create" eval="0" />
|
||||
<field name="perm_unlink" eval="0" />
|
||||
</record>
|
||||
|
||||
<!-- Manager Write/Create/Delete Rule -->
|
||||
<record id="rule_git_project_file_template_rel_manager_write" model="ir.rule">
|
||||
<field
|
||||
name="name"
|
||||
>Git Project File Template Relation: Manager Write Access</field>
|
||||
<field name="model_id" ref="model_cx_tower_git_project_file_template_rel" />
|
||||
<field name="domain_force">[
|
||||
('git_project_id.manager_ids', 'in', [user.id]),
|
||||
('file_template_id.manager_ids', 'in', [user.id])]</field>
|
||||
<field name="groups" eval="[(4, ref('cetmix_tower_server.group_manager'))]" />
|
||||
<field name="perm_read" eval="0" />
|
||||
<field name="perm_write" eval="1" />
|
||||
<field name="perm_create" eval="1" />
|
||||
<field name="perm_unlink" eval="1" />
|
||||
</record>
|
||||
|
||||
|
||||
<!-- Root Access Rule -->
|
||||
<record id="rule_git_project_file_template_rel_root" model="ir.rule">
|
||||
<field name="name">Git Project File Template Relation: Root Full Access</field>
|
||||
<field name="model_id" ref="model_cx_tower_git_project_file_template_rel" />
|
||||
<field name="domain_force">[(1, '=', 1)]</field>
|
||||
<field name="groups" eval="[(4, ref('cetmix_tower_server.group_root'))]" />
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,45 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
|
||||
<!-- Manager Read Rule -->
|
||||
<record id="rule_git_project_rel_manager_read" model="ir.rule">
|
||||
<field name="name">Git Project Relation: Manager Read Access</field>
|
||||
<field name="model_id" ref="model_cx_tower_git_project_rel" />
|
||||
<field name="domain_force">['&',
|
||||
'|',
|
||||
('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])]</field>
|
||||
<field name="groups" eval="[(4, ref('cetmix_tower_server.group_manager'))]" />
|
||||
<field name="perm_read" eval="1" />
|
||||
<field name="perm_write" eval="0" />
|
||||
<field name="perm_create" eval="0" />
|
||||
<field name="perm_unlink" eval="0" />
|
||||
</record>
|
||||
|
||||
<!-- Manager Write/Create/Delete Rule -->
|
||||
<record id="rule_git_project_rel_manager_create_write_unlink" model="ir.rule">
|
||||
<field
|
||||
name="name"
|
||||
>Git Project Relation: Manager Create/Write/Delete Access</field>
|
||||
<field name="model_id" ref="model_cx_tower_git_project_rel" />
|
||||
<field name="domain_force">[('git_project_id.manager_ids', 'in', [user.id]),
|
||||
('server_id.manager_ids', 'in', [user.id])]</field>
|
||||
<field name="groups" eval="[(4, ref('cetmix_tower_server.group_manager'))]" />
|
||||
<field name="perm_read" eval="0" />
|
||||
<field name="perm_write" eval="1" />
|
||||
<field name="perm_create" eval="1" />
|
||||
<field name="perm_unlink" eval="1" />
|
||||
</record>
|
||||
|
||||
<!-- Root Access Rule -->
|
||||
<record id="rule_git_project_rel_root" model="ir.rule">
|
||||
<field name="name">Git Project Relation: Root Full Access</field>
|
||||
<field name="model_id" ref="model_cx_tower_git_project_rel" />
|
||||
<field name="domain_force">[(1, '=', 1)]</field>
|
||||
<field name="groups" eval="[(4, ref('cetmix_tower_server.group_root'))]" />
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,67 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
|
||||
<!-- Manager Read Rule -->
|
||||
<record id="rule_git_project_manager_read" model="ir.rule">
|
||||
<field name="name">Git Project: Manager Read Access</field>
|
||||
<field name="model_id" ref="model_cx_tower_git_project" />
|
||||
<field
|
||||
name="domain_force"
|
||||
>['|', ('user_ids', 'in', [user.id]), ('manager_ids', 'in', [user.id])]</field>
|
||||
<field name="groups" eval="[(4, ref('cetmix_tower_server.group_manager'))]" />
|
||||
<field name="perm_read" eval="1" />
|
||||
<field name="perm_write" eval="0" />
|
||||
<field name="perm_create" eval="0" />
|
||||
<field name="perm_unlink" eval="0" />
|
||||
</record>
|
||||
|
||||
<!-- Additional Manager Read Rule with Server Access -->
|
||||
<record id="rule_git_project_manager_read_server" model="ir.rule">
|
||||
<field name="name">Git Project: Manager Read Access via Server</field>
|
||||
<field name="model_id" ref="model_cx_tower_git_project" />
|
||||
<field name="domain_force">['|',
|
||||
('server_ids.user_ids', 'in', [user.id]),
|
||||
('server_ids.manager_ids', 'in', [user.id])]</field>
|
||||
<field name="groups" eval="[(4, ref('cetmix_tower_server.group_manager'))]" />
|
||||
<field name="perm_read" eval="1" />
|
||||
<field name="perm_write" eval="0" />
|
||||
<field name="perm_create" eval="0" />
|
||||
<field name="perm_unlink" eval="0" />
|
||||
</record>
|
||||
|
||||
<!-- Manager Write/Create Rule -->
|
||||
<record id="rule_git_project_manager_write" model="ir.rule">
|
||||
<field name="name">Git Project: Manager Write Access</field>
|
||||
<field name="model_id" ref="model_cx_tower_git_project" />
|
||||
<field name="domain_force">[('manager_ids', 'in', [user.id])]</field>
|
||||
<field name="groups" eval="[(4, ref('cetmix_tower_server.group_manager'))]" />
|
||||
<field name="perm_read" eval="0" />
|
||||
<field name="perm_write" eval="1" />
|
||||
<field name="perm_create" eval="1" />
|
||||
<field name="perm_unlink" eval="0" />
|
||||
</record>
|
||||
|
||||
<!-- Manager Delete Rule -->
|
||||
<record id="rule_git_project_manager_unlink" model="ir.rule">
|
||||
<field name="name">Git Project: Manager Delete Access</field>
|
||||
<field name="model_id" ref="model_cx_tower_git_project" />
|
||||
<field
|
||||
name="domain_force"
|
||||
>[('manager_ids', 'in', [user.id]), ('create_uid', '=', user.id)]</field>
|
||||
<field name="groups" eval="[(4, ref('cetmix_tower_server.group_manager'))]" />
|
||||
<field name="perm_read" eval="0" />
|
||||
<field name="perm_write" eval="0" />
|
||||
<field name="perm_create" eval="0" />
|
||||
<field name="perm_unlink" eval="1" />
|
||||
</record>
|
||||
|
||||
<!-- Root Access Rule -->
|
||||
<record id="rule_git_project_root" model="ir.rule">
|
||||
<field name="name">Git Project: Root Full Access</field>
|
||||
<field name="model_id" ref="model_cx_tower_git_project" />
|
||||
<field name="domain_force">[(1, '=', 1)]</field>
|
||||
<field name="groups" eval="[(4, ref('cetmix_tower_server.group_root'))]" />
|
||||
</record>
|
||||
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,73 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
|
||||
<!-- Manager Read Rule -->
|
||||
<record id="rule_git_remote_manager_read" model="ir.rule">
|
||||
<field name="name">Git Remote: Manager Read Access</field>
|
||||
<field name="model_id" ref="model_cx_tower_git_remote" />
|
||||
<field
|
||||
name="domain_force"
|
||||
>['|', ('git_project_id.user_ids', 'in', [user.id]), ('git_project_id.manager_ids', 'in', [user.id])]</field>
|
||||
<field name="groups" eval="[(4, ref('cetmix_tower_server.group_manager'))]" />
|
||||
<field name="perm_read" eval="1" />
|
||||
<field name="perm_write" eval="0" />
|
||||
<field name="perm_create" eval="0" />
|
||||
<field name="perm_unlink" eval="0" />
|
||||
</record>
|
||||
|
||||
<!-- Additional Manager Read Rule with Server Access -->
|
||||
<record id="rule_git_remote_manager_read_server" model="ir.rule">
|
||||
<field name="name">Git Remote: Manager Read Access via Server</field>
|
||||
<field name="model_id" ref="model_cx_tower_git_remote" />
|
||||
<field name="domain_force">['|',
|
||||
('git_project_id.server_ids.user_ids', 'in', [user.id]),
|
||||
('git_project_id.server_ids.manager_ids', 'in', [user.id])]</field>
|
||||
<field name="groups" eval="[(4, ref('cetmix_tower_server.group_manager'))]" />
|
||||
<field name="perm_read" eval="1" />
|
||||
<field name="perm_write" eval="0" />
|
||||
<field name="perm_create" eval="0" />
|
||||
<field name="perm_unlink" eval="0" />
|
||||
</record>
|
||||
|
||||
<!-- Manager Write/Create Rule -->
|
||||
<record id="rule_git_remote_manager_write" model="ir.rule">
|
||||
<field name="name">Git Remote: Manager Write/Create Access</field>
|
||||
<field name="model_id" ref="model_cx_tower_git_remote" />
|
||||
<field
|
||||
name="domain_force"
|
||||
>[('git_project_id.manager_ids', 'in', [user.id])]</field>
|
||||
<field name="groups" eval="[(4, ref('cetmix_tower_server.group_manager'))]" />
|
||||
<field name="perm_read" eval="0" />
|
||||
<field name="perm_write" eval="1" />
|
||||
<field name="perm_create" eval="1" />
|
||||
<field name="perm_unlink" eval="0" />
|
||||
</record>
|
||||
|
||||
<!-- Manager Delete Rule -->
|
||||
<record id="rule_git_remote_manager_unlink" model="ir.rule">
|
||||
<field name="name">Git Remote: Manager Delete Access</field>
|
||||
<field name="model_id" ref="model_cx_tower_git_remote" />
|
||||
<field
|
||||
name="domain_force"
|
||||
>[('git_project_id.manager_ids', 'in', [user.id]), ('create_uid', '=', user.id)]</field>
|
||||
<field name="groups" eval="[(4, ref('cetmix_tower_server.group_manager'))]" />
|
||||
<field name="perm_read" eval="0" />
|
||||
<field name="perm_write" eval="0" />
|
||||
<field name="perm_create" eval="0" />
|
||||
<field name="perm_unlink" eval="1" />
|
||||
</record>
|
||||
|
||||
<!-- Root Access Rule -->
|
||||
<record id="rule_git_remote_root" model="ir.rule">
|
||||
<field name="name">Git Remote: Root Full Access</field>
|
||||
<field name="model_id" ref="model_cx_tower_git_remote" />
|
||||
<field name="domain_force">[(1, '=', 1)]</field>
|
||||
<field name="groups" eval="[(4, ref('cetmix_tower_server.group_root'))]" />
|
||||
<field name="perm_read" eval="1" />
|
||||
<field name="perm_write" eval="1" />
|
||||
<field name="perm_create" eval="1" />
|
||||
<field name="perm_unlink" eval="1" />
|
||||
</record>
|
||||
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
|
||||
<!-- Manager Read Rule - View All Records - nothing to add as this is default -->
|
||||
|
||||
<!-- Manager Write/Create Rule -->
|
||||
<record id="rule_git_repo_owner_manager_write" model="ir.rule">
|
||||
<field name="name">Git Repository Owner: Manager Write/Create Access</field>
|
||||
<field name="model_id" ref="model_cx_tower_git_repo_owner" />
|
||||
<field name="domain_force">[('create_uid', '=', user.id)]</field>
|
||||
<field name="groups" eval="[(4, ref('cetmix_tower_server.group_manager'))]" />
|
||||
<field name="perm_read" eval="0" />
|
||||
<field name="perm_write" eval="1" />
|
||||
<field name="perm_create" eval="1" />
|
||||
<field name="perm_unlink" eval="0" />
|
||||
</record>
|
||||
|
||||
<!-- Root Access Rule -->
|
||||
<record id="rule_git_repo_owner_root" model="ir.rule">
|
||||
<field name="name">Git Repository Owner: Root Full Access</field>
|
||||
<field name="model_id" ref="model_cx_tower_git_repo_owner" />
|
||||
<field name="domain_force">[(1, '=', 1)]</field>
|
||||
<field name="groups" eval="[(4, ref('cetmix_tower_server.group_root'))]" />
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
|
||||
<!-- Manager Read Rule - View All Records - nothing to add as this is default -->
|
||||
|
||||
<!-- Manager Write/Create Rule -->
|
||||
<record id="rule_git_repo_manager_write" model="ir.rule">
|
||||
<field name="name">Git Repository: Manager Write/Create Access</field>
|
||||
<field name="model_id" ref="model_cx_tower_git_repo" />
|
||||
<field name="domain_force">[('create_uid', '=', user.id)]</field>
|
||||
<field name="groups" eval="[(4, ref('cetmix_tower_server.group_manager'))]" />
|
||||
<field name="perm_read" eval="0" />
|
||||
<field name="perm_write" eval="1" />
|
||||
<field name="perm_create" eval="1" />
|
||||
<field name="perm_unlink" eval="0" />
|
||||
</record>
|
||||
|
||||
<!-- Root Access Rule -->
|
||||
<record id="rule_git_repo_root" model="ir.rule">
|
||||
<field name="name">Git Repository: Root Full Access</field>
|
||||
<field name="model_id" ref="model_cx_tower_git_repo" />
|
||||
<field name="domain_force">[(1, '=', 1)]</field>
|
||||
<field name="groups" eval="[(4, ref('cetmix_tower_server.group_root'))]" />
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,75 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
|
||||
<!-- Git Source Record Rules -->
|
||||
|
||||
<!-- Manager Read Rule -->
|
||||
<record id="rule_git_source_manager_read" model="ir.rule">
|
||||
<field name="name">Git Source: Manager Read Access</field>
|
||||
<field name="model_id" ref="model_cx_tower_git_source" />
|
||||
<field
|
||||
name="domain_force"
|
||||
>['|', ('git_project_id.user_ids', 'in', [user.id]), ('git_project_id.manager_ids', 'in', [user.id])]</field>
|
||||
<field name="groups" eval="[(4, ref('cetmix_tower_server.group_manager'))]" />
|
||||
<field name="perm_read" eval="1" />
|
||||
<field name="perm_write" eval="0" />
|
||||
<field name="perm_create" eval="0" />
|
||||
<field name="perm_unlink" eval="0" />
|
||||
</record>
|
||||
|
||||
<!-- Additional Manager Read Rule with Server Access -->
|
||||
<record id="rule_git_source_manager_read_server" model="ir.rule">
|
||||
<field name="name">Git Source: Manager Read Access via Server</field>
|
||||
<field name="model_id" ref="model_cx_tower_git_source" />
|
||||
<field name="domain_force">['|',
|
||||
('git_project_id.server_ids.user_ids', 'in', [user.id]),
|
||||
('git_project_id.server_ids.manager_ids', 'in', [user.id])]</field>
|
||||
<field name="groups" eval="[(4, ref('cetmix_tower_server.group_manager'))]" />
|
||||
<field name="perm_read" eval="1" />
|
||||
<field name="perm_write" eval="0" />
|
||||
<field name="perm_create" eval="0" />
|
||||
<field name="perm_unlink" eval="0" />
|
||||
</record>
|
||||
|
||||
<!-- Manager Write/Create Rule -->
|
||||
<record id="rule_git_source_manager_write" model="ir.rule">
|
||||
<field name="name">Git Source: Manager Write/Create Access</field>
|
||||
<field name="model_id" ref="model_cx_tower_git_source" />
|
||||
<field
|
||||
name="domain_force"
|
||||
>[('git_project_id.manager_ids', 'in', [user.id])]</field>
|
||||
<field name="groups" eval="[(4, ref('cetmix_tower_server.group_manager'))]" />
|
||||
<field name="perm_read" eval="0" />
|
||||
<field name="perm_write" eval="1" />
|
||||
<field name="perm_create" eval="1" />
|
||||
<field name="perm_unlink" eval="0" />
|
||||
</record>
|
||||
|
||||
<!-- Manager Delete Rule -->
|
||||
<record id="rule_git_source_manager_unlink" model="ir.rule">
|
||||
<field name="name">Git Source: Manager Delete Access</field>
|
||||
<field name="model_id" ref="model_cx_tower_git_source" />
|
||||
<field
|
||||
name="domain_force"
|
||||
>[('git_project_id.manager_ids', 'in', [user.id]), ('create_uid', '=', user.id)]</field>
|
||||
<field name="groups" eval="[(4, ref('cetmix_tower_server.group_manager'))]" />
|
||||
<field name="perm_read" eval="0" />
|
||||
<field name="perm_write" eval="0" />
|
||||
<field name="perm_create" eval="0" />
|
||||
<field name="perm_unlink" eval="1" />
|
||||
</record>
|
||||
|
||||
<!-- Root Access Rule -->
|
||||
<record id="rule_git_source_root" model="ir.rule">
|
||||
<field name="name">Git Source: Root Full Access</field>
|
||||
<field name="model_id" ref="model_cx_tower_git_source" />
|
||||
<field name="domain_force">[(1, '=', 1)]</field>
|
||||
<field name="groups" eval="[(4, ref('cetmix_tower_server.group_root'))]" />
|
||||
<field name="perm_read" eval="1" />
|
||||
<field name="perm_write" eval="1" />
|
||||
<field name="perm_create" eval="1" />
|
||||
<field name="perm_unlink" eval="1" />
|
||||
</record>
|
||||
|
||||
|
||||
</odoo>
|
||||
15
addons/cetmix_tower_git/security/ir.model.access.csv
Normal file
15
addons/cetmix_tower_git/security/ir.model.access.csv
Normal file
@@ -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
|
||||
|
BIN
addons/cetmix_tower_git/static/description/banner.png
Normal file
BIN
addons/cetmix_tower_git/static/description/banner.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 85 KiB |
BIN
addons/cetmix_tower_git/static/description/icon.png
Normal file
BIN
addons/cetmix_tower_git/static/description/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
497
addons/cetmix_tower_git/static/description/index.html
Normal file
497
addons/cetmix_tower_git/static/description/index.html
Normal file
@@ -0,0 +1,497 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
|
||||
<title>Cetmix Tower Git</title>
|
||||
<style type="text/css">
|
||||
|
||||
/*
|
||||
:Author: David Goodger (goodger@python.org)
|
||||
:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $
|
||||
:Copyright: This stylesheet has been placed in the public domain.
|
||||
|
||||
Default cascading style sheet for the HTML output of Docutils.
|
||||
Despite the name, some widely supported CSS2 features are used.
|
||||
|
||||
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
|
||||
customize this style sheet.
|
||||
*/
|
||||
|
||||
/* used to remove borders from tables and images */
|
||||
.borderless, table.borderless td, table.borderless th {
|
||||
border: 0 }
|
||||
|
||||
table.borderless td, table.borderless th {
|
||||
/* Override padding for "table.docutils td" with "! important".
|
||||
The right padding separates the table cells. */
|
||||
padding: 0 0.5em 0 0 ! important }
|
||||
|
||||
.first {
|
||||
/* Override more specific margin styles with "! important". */
|
||||
margin-top: 0 ! important }
|
||||
|
||||
.last, .with-subtitle {
|
||||
margin-bottom: 0 ! important }
|
||||
|
||||
.hidden {
|
||||
display: none }
|
||||
|
||||
.subscript {
|
||||
vertical-align: sub;
|
||||
font-size: smaller }
|
||||
|
||||
.superscript {
|
||||
vertical-align: super;
|
||||
font-size: smaller }
|
||||
|
||||
a.toc-backref {
|
||||
text-decoration: none ;
|
||||
color: black }
|
||||
|
||||
blockquote.epigraph {
|
||||
margin: 2em 5em ; }
|
||||
|
||||
dl.docutils dd {
|
||||
margin-bottom: 0.5em }
|
||||
|
||||
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Uncomment (and remove this text!) to get bold-faced definition list terms
|
||||
dl.docutils dt {
|
||||
font-weight: bold }
|
||||
*/
|
||||
|
||||
div.abstract {
|
||||
margin: 2em 5em }
|
||||
|
||||
div.abstract p.topic-title {
|
||||
font-weight: bold ;
|
||||
text-align: center }
|
||||
|
||||
div.admonition, div.attention, div.caution, div.danger, div.error,
|
||||
div.hint, div.important, div.note, div.tip, div.warning {
|
||||
margin: 2em ;
|
||||
border: medium outset ;
|
||||
padding: 1em }
|
||||
|
||||
div.admonition p.admonition-title, div.hint p.admonition-title,
|
||||
div.important p.admonition-title, div.note p.admonition-title,
|
||||
div.tip p.admonition-title {
|
||||
font-weight: bold ;
|
||||
font-family: sans-serif }
|
||||
|
||||
div.attention p.admonition-title, div.caution p.admonition-title,
|
||||
div.danger p.admonition-title, div.error p.admonition-title,
|
||||
div.warning p.admonition-title, .code .error {
|
||||
color: red ;
|
||||
font-weight: bold ;
|
||||
font-family: sans-serif }
|
||||
|
||||
/* Uncomment (and remove this text!) to get reduced vertical space in
|
||||
compound paragraphs.
|
||||
div.compound .compound-first, div.compound .compound-middle {
|
||||
margin-bottom: 0.5em }
|
||||
|
||||
div.compound .compound-last, div.compound .compound-middle {
|
||||
margin-top: 0.5em }
|
||||
*/
|
||||
|
||||
div.dedication {
|
||||
margin: 2em 5em ;
|
||||
text-align: center ;
|
||||
font-style: italic }
|
||||
|
||||
div.dedication p.topic-title {
|
||||
font-weight: bold ;
|
||||
font-style: normal }
|
||||
|
||||
div.figure {
|
||||
margin-left: 2em ;
|
||||
margin-right: 2em }
|
||||
|
||||
div.footer, div.header {
|
||||
clear: both;
|
||||
font-size: smaller }
|
||||
|
||||
div.line-block {
|
||||
display: block ;
|
||||
margin-top: 1em ;
|
||||
margin-bottom: 1em }
|
||||
|
||||
div.line-block div.line-block {
|
||||
margin-top: 0 ;
|
||||
margin-bottom: 0 ;
|
||||
margin-left: 1.5em }
|
||||
|
||||
div.sidebar {
|
||||
margin: 0 0 0.5em 1em ;
|
||||
border: medium outset ;
|
||||
padding: 1em ;
|
||||
background-color: #ffffee ;
|
||||
width: 40% ;
|
||||
float: right ;
|
||||
clear: right }
|
||||
|
||||
div.sidebar p.rubric {
|
||||
font-family: sans-serif ;
|
||||
font-size: medium }
|
||||
|
||||
div.system-messages {
|
||||
margin: 5em }
|
||||
|
||||
div.system-messages h1 {
|
||||
color: red }
|
||||
|
||||
div.system-message {
|
||||
border: medium outset ;
|
||||
padding: 1em }
|
||||
|
||||
div.system-message p.system-message-title {
|
||||
color: red ;
|
||||
font-weight: bold }
|
||||
|
||||
div.topic {
|
||||
margin: 2em }
|
||||
|
||||
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
|
||||
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
|
||||
margin-top: 0.4em }
|
||||
|
||||
h1.title {
|
||||
text-align: center }
|
||||
|
||||
h2.subtitle {
|
||||
text-align: center }
|
||||
|
||||
hr.docutils {
|
||||
width: 75% }
|
||||
|
||||
img.align-left, .figure.align-left, object.align-left, table.align-left {
|
||||
clear: left ;
|
||||
float: left ;
|
||||
margin-right: 1em }
|
||||
|
||||
img.align-right, .figure.align-right, object.align-right, table.align-right {
|
||||
clear: right ;
|
||||
float: right ;
|
||||
margin-left: 1em }
|
||||
|
||||
img.align-center, .figure.align-center, object.align-center {
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
table.align-center {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.align-left {
|
||||
text-align: left }
|
||||
|
||||
.align-center {
|
||||
clear: both ;
|
||||
text-align: center }
|
||||
|
||||
.align-right {
|
||||
text-align: right }
|
||||
|
||||
/* reset inner alignment in figures */
|
||||
div.align-right {
|
||||
text-align: inherit }
|
||||
|
||||
/* div.align-center * { */
|
||||
/* text-align: left } */
|
||||
|
||||
.align-top {
|
||||
vertical-align: top }
|
||||
|
||||
.align-middle {
|
||||
vertical-align: middle }
|
||||
|
||||
.align-bottom {
|
||||
vertical-align: bottom }
|
||||
|
||||
ol.simple, ul.simple {
|
||||
margin-bottom: 1em }
|
||||
|
||||
ol.arabic {
|
||||
list-style: decimal }
|
||||
|
||||
ol.loweralpha {
|
||||
list-style: lower-alpha }
|
||||
|
||||
ol.upperalpha {
|
||||
list-style: upper-alpha }
|
||||
|
||||
ol.lowerroman {
|
||||
list-style: lower-roman }
|
||||
|
||||
ol.upperroman {
|
||||
list-style: upper-roman }
|
||||
|
||||
p.attribution {
|
||||
text-align: right ;
|
||||
margin-left: 50% }
|
||||
|
||||
p.caption {
|
||||
font-style: italic }
|
||||
|
||||
p.credits {
|
||||
font-style: italic ;
|
||||
font-size: smaller }
|
||||
|
||||
p.label {
|
||||
white-space: nowrap }
|
||||
|
||||
p.rubric {
|
||||
font-weight: bold ;
|
||||
font-size: larger ;
|
||||
color: maroon ;
|
||||
text-align: center }
|
||||
|
||||
p.sidebar-title {
|
||||
font-family: sans-serif ;
|
||||
font-weight: bold ;
|
||||
font-size: larger }
|
||||
|
||||
p.sidebar-subtitle {
|
||||
font-family: sans-serif ;
|
||||
font-weight: bold }
|
||||
|
||||
p.topic-title {
|
||||
font-weight: bold }
|
||||
|
||||
pre.address {
|
||||
margin-bottom: 0 ;
|
||||
margin-top: 0 ;
|
||||
font: inherit }
|
||||
|
||||
pre.literal-block, pre.doctest-block, pre.math, pre.code {
|
||||
margin-left: 2em ;
|
||||
margin-right: 2em }
|
||||
|
||||
pre.code .ln { color: gray; } /* line numbers */
|
||||
pre.code, code { background-color: #eeeeee }
|
||||
pre.code .comment, code .comment { color: #5C6576 }
|
||||
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
|
||||
pre.code .literal.string, code .literal.string { color: #0C5404 }
|
||||
pre.code .name.builtin, code .name.builtin { color: #352B84 }
|
||||
pre.code .deleted, code .deleted { background-color: #DEB0A1}
|
||||
pre.code .inserted, code .inserted { background-color: #A3D289}
|
||||
|
||||
span.classifier {
|
||||
font-family: sans-serif ;
|
||||
font-style: oblique }
|
||||
|
||||
span.classifier-delimiter {
|
||||
font-family: sans-serif ;
|
||||
font-weight: bold }
|
||||
|
||||
span.interpreted {
|
||||
font-family: sans-serif }
|
||||
|
||||
span.option {
|
||||
white-space: nowrap }
|
||||
|
||||
span.pre {
|
||||
white-space: pre }
|
||||
|
||||
span.problematic, pre.problematic {
|
||||
color: red }
|
||||
|
||||
span.section-subtitle {
|
||||
/* font-size relative to parent (h1..h6 element) */
|
||||
font-size: 80% }
|
||||
|
||||
table.citation {
|
||||
border-left: solid 1px gray;
|
||||
margin-left: 1px }
|
||||
|
||||
table.docinfo {
|
||||
margin: 2em 4em }
|
||||
|
||||
table.docutils {
|
||||
margin-top: 0.5em ;
|
||||
margin-bottom: 0.5em }
|
||||
|
||||
table.footnote {
|
||||
border-left: solid 1px black;
|
||||
margin-left: 1px }
|
||||
|
||||
table.docutils td, table.docutils th,
|
||||
table.docinfo td, table.docinfo th {
|
||||
padding-left: 0.5em ;
|
||||
padding-right: 0.5em ;
|
||||
vertical-align: top }
|
||||
|
||||
table.docutils th.field-name, table.docinfo th.docinfo-name {
|
||||
font-weight: bold ;
|
||||
text-align: left ;
|
||||
white-space: nowrap ;
|
||||
padding-left: 0 }
|
||||
|
||||
/* "booktabs" style (no vertical lines) */
|
||||
table.docutils.booktabs {
|
||||
border: 0px;
|
||||
border-top: 2px solid;
|
||||
border-bottom: 2px solid;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
table.docutils.booktabs * {
|
||||
border: 0px;
|
||||
}
|
||||
table.docutils.booktabs th {
|
||||
border-bottom: thin solid;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
|
||||
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
|
||||
font-size: 100% }
|
||||
|
||||
ul.auto-toc {
|
||||
list-style-type: none }
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document" id="cetmix-tower-git">
|
||||
<h1 class="title">Cetmix Tower Git</h1>
|
||||
|
||||
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! This file is generated by oca-gen-addon-readme !!
|
||||
!! changes will be overwritten. !!
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! source digest: sha256:583744e8956f294682a551fc082f086b174b8d2b72652c21b1dd68f3933e7211
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
|
||||
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/license-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/cetmix/cetmix-tower/tree/16.0/cetmix_tower_git"><img alt="cetmix/cetmix-tower" src="https://img.shields.io/badge/github-cetmix%2Fcetmix--tower-lightgray.png?logo=github" /></a></p>
|
||||
<p>This module implements Git Management functionality for <a class="reference external" href="https://cetmix.com/tower">Cetmix
|
||||
Tower</a>.</p>
|
||||
<p>Please refer to the <a class="reference external" href="https://cetmix.com/tower">official
|
||||
documentation</a> for detailed information.</p>
|
||||
<p><strong>Table of contents</strong></p>
|
||||
<div class="contents local topic" id="contents">
|
||||
<ul class="simple">
|
||||
<li><a class="reference internal" href="#configuration" id="toc-entry-1">Configuration</a></li>
|
||||
<li><a class="reference internal" href="#usage" id="toc-entry-2">Usage</a></li>
|
||||
<li><a class="reference internal" href="#changelog" id="toc-entry-3">Changelog</a><ul>
|
||||
<li><a class="reference internal" href="#section-1" id="toc-entry-4">16.0.2.0.1 (2025-12-11)</a></li>
|
||||
<li><a class="reference internal" href="#section-2" id="toc-entry-5">16.0.2.0.0 (2025-10-27)</a></li>
|
||||
<li><a class="reference internal" href="#section-3" id="toc-entry-6">16.0.1.0.6 (2025-08-18)</a></li>
|
||||
<li><a class="reference internal" href="#section-4" id="toc-entry-7">16.0.1.0.5 (2025-08-17)</a></li>
|
||||
<li><a class="reference internal" href="#section-5" id="toc-entry-8">16.0.1.0.4 (2025-07-29)</a></li>
|
||||
<li><a class="reference internal" href="#section-6" id="toc-entry-9">16.0.1.0.3 (2025-05-23)</a></li>
|
||||
<li><a class="reference internal" href="#section-7" id="toc-entry-10">16.0.1.0.2 (2025-05-16)</a></li>
|
||||
<li><a class="reference internal" href="#section-8" id="toc-entry-11">16.0.1.0.1 (2025-05-09)</a></li>
|
||||
<li><a class="reference internal" href="#section-9" id="toc-entry-12">16.0.1.0.0</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-13">Bug Tracker</a></li>
|
||||
<li><a class="reference internal" href="#credits" id="toc-entry-14">Credits</a><ul>
|
||||
<li><a class="reference internal" href="#authors" id="toc-entry-15">Authors</a></li>
|
||||
<li><a class="reference internal" href="#maintainers" id="toc-entry-16">Maintainers</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="configuration">
|
||||
<h1><a class="toc-backref" href="#toc-entry-1">Configuration</a></h1>
|
||||
<p>Please refer to the <a class="reference external" href="https://cetmix.com/tower">official
|
||||
documentation</a> for detailed configuration
|
||||
instructions.</p>
|
||||
</div>
|
||||
<div class="section" id="usage">
|
||||
<h1><a class="toc-backref" href="#toc-entry-2">Usage</a></h1>
|
||||
<p>Please refer to the <a class="reference external" href="https://cetmix.com/tower">official
|
||||
documentation</a> for detailed usage
|
||||
instructions.</p>
|
||||
</div>
|
||||
<div class="section" id="changelog">
|
||||
<h1><a class="toc-backref" href="#toc-entry-3">Changelog</a></h1>
|
||||
<div class="section" id="section-1">
|
||||
<h2><a class="toc-backref" href="#toc-entry-4">16.0.2.0.1 (2025-12-11)</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Features: Improve search views, implement the search panel for
|
||||
selected views. (5139)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="section-2">
|
||||
<h2><a class="toc-backref" href="#toc-entry-5">16.0.2.0.0 (2025-10-27)</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Features: Major refactoring: implement Git repository entity. (4914)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="section-3">
|
||||
<h2><a class="toc-backref" href="#toc-entry-6">16.0.1.0.6 (2025-08-18)</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Features: Link or copy a git project when uploading the linked file
|
||||
using command (4759)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="section-4">
|
||||
<h2><a class="toc-backref" href="#toc-entry-7">16.0.1.0.5 (2025-08-17)</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Features: Search servers by git reference (4838)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="section-5">
|
||||
<h2><a class="toc-backref" href="#toc-entry-8">16.0.1.0.4 (2025-07-29)</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Features: Export related commands and flight plans together with
|
||||
server (4849)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="section-6">
|
||||
<h2><a class="toc-backref" href="#toc-entry-9">16.0.1.0.3 (2025-05-23)</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Bugfixes: Duplicated file is created when importing a YAML file with a
|
||||
git project. (4715)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="section-7">
|
||||
<h2><a class="toc-backref" href="#toc-entry-10">16.0.1.0.2 (2025-05-16)</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Features: Record references for git relations. (4670)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="section-8">
|
||||
<h2><a class="toc-backref" href="#toc-entry-11">16.0.1.0.1 (2025-05-09)</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Bugfixes: Non-critical issues and performance improvements. (4663)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="section-9">
|
||||
<h2><a class="toc-backref" href="#toc-entry-12">16.0.1.0.0</a></h2>
|
||||
<p>Release for Odoo 16.0</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section" id="bug-tracker">
|
||||
<h1><a class="toc-backref" href="#toc-entry-13">Bug Tracker</a></h1>
|
||||
<p>Bugs are tracked on <a class="reference external" href="https://github.com/cetmix/cetmix-tower/issues">GitHub Issues</a>.
|
||||
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
|
||||
<a class="reference external" href="https://github.com/cetmix/cetmix-tower/issues/new?body=module:%20cetmix_tower_git%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
|
||||
<p>Do not contact contributors directly about support or help with technical issues.</p>
|
||||
</div>
|
||||
<div class="section" id="credits">
|
||||
<h1><a class="toc-backref" href="#toc-entry-14">Credits</a></h1>
|
||||
<div class="section" id="authors">
|
||||
<h2><a class="toc-backref" href="#toc-entry-15">Authors</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Cetmix</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="maintainers">
|
||||
<h2><a class="toc-backref" href="#toc-entry-16">Maintainers</a></h2>
|
||||
<p>This module is part of the <a class="reference external" href="https://github.com/cetmix/cetmix-tower/tree/16.0/cetmix_tower_git">cetmix/cetmix-tower</a> project on GitHub.</p>
|
||||
<p>You are welcome to contribute.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
7
addons/cetmix_tower_git/tests/__init__.py
Normal file
7
addons/cetmix_tower_git/tests/__init__.py
Normal file
@@ -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
|
||||
136
addons/cetmix_tower_git/tests/common.py
Normal file
136
addons/cetmix_tower_git/tests/common.py
Normal file
@@ -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",
|
||||
}
|
||||
)
|
||||
390
addons/cetmix_tower_git/tests/test_file_rel.py
Normal file
390
addons/cetmix_tower_git/tests/test_file_rel.py
Normal file
@@ -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.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())
|
||||
308
addons/cetmix_tower_git/tests/test_file_template_rel.py
Normal file
308
addons/cetmix_tower_git/tests/test_file_template_rel.py
Normal file
@@ -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())
|
||||
315
addons/cetmix_tower_git/tests/test_project.py
Normal file
315
addons/cetmix_tower_git/tests/test_project.py
Normal file
@@ -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")
|
||||
462
addons/cetmix_tower_git/tests/test_remote.py
Normal file
462
addons/cetmix_tower_git/tests/test_remote.py
Normal file
@@ -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.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/tree/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")
|
||||
84
addons/cetmix_tower_git/tests/test_repo.py
Normal file
84
addons/cetmix_tower_git/tests/test_repo.py
Normal file
@@ -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",
|
||||
}
|
||||
)
|
||||
198
addons/cetmix_tower_git/tests/test_server.py
Normal file
198
addons/cetmix_tower_git/tests/test_server.py
Normal file
@@ -0,0 +1,198 @@
|
||||
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,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
# 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_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)
|
||||
226
addons/cetmix_tower_git/tests/test_source.py
Normal file
226
addons/cetmix_tower_git/tests/test_source.py
Normal file
@@ -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")
|
||||
0
addons/cetmix_tower_git/tools/git_aggregator.py
Normal file
0
addons/cetmix_tower_git/tools/git_aggregator.py
Normal file
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
|
||||
<record id="cx_tower_file_template_view_form" model="ir.ui.view">
|
||||
<field name="name">cx.tower.file.template.view.form</field>
|
||||
<field name="model">cx.tower.file.template</field>
|
||||
<field
|
||||
name="inherit_id"
|
||||
ref="cetmix_tower_server.cx_tower_file_template_view_form"
|
||||
/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="source" position="after">
|
||||
<field
|
||||
name="git_project_id"
|
||||
attrs="{'invisible': [('git_project_id', '=', False)]}"
|
||||
/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
18
addons/cetmix_tower_git/views/cx_tower_file_views.xml
Normal file
18
addons/cetmix_tower_git/views/cx_tower_file_views.xml
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
|
||||
<record id="cx_tower_file_view_form" model="ir.ui.view">
|
||||
<field name="name">cx.tower.file.view.form</field>
|
||||
<field name="model">cx.tower.file</field>
|
||||
<field name="inherit_id" ref="cetmix_tower_server.cx_tower_file_view_form" />
|
||||
<field name="arch" type="xml">
|
||||
<field name="auto_sync" position="before">
|
||||
<field
|
||||
name="git_project_id"
|
||||
attrs="{'invisible': [('git_project_id', '=', False)]}"
|
||||
/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
236
addons/cetmix_tower_git/views/cx_tower_git_project_views.xml
Normal file
236
addons/cetmix_tower_git/views/cx_tower_git_project_views.xml
Normal file
@@ -0,0 +1,236 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
|
||||
<!-- Tree View -->
|
||||
<record id="cx_tower_git_project_view_tree" model="ir.ui.view">
|
||||
<field name="name">cx.tower.git.project.tree</field>
|
||||
<field name="model">cx.tower.git.project</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree>
|
||||
<field name="name" />
|
||||
<field name="server_ids" widget="many2many_tags" />
|
||||
<field name="active" widget="boolean_toggle" />
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Form View -->
|
||||
<record id="cx_tower_git_project_view_form" model="ir.ui.view">
|
||||
<field name="name">cx.tower.git.project.form</field>
|
||||
<field name="model">cx.tower.git.project</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<sheet>
|
||||
<widget
|
||||
name="web_ribbon"
|
||||
title="Archived"
|
||||
bg_color="bg-danger"
|
||||
attrs="{'invisible': [('active', '=', True)]}"
|
||||
/>
|
||||
<div class="oe_title">
|
||||
<h1>
|
||||
<field name="name" placeholder="Name" />
|
||||
</h1>
|
||||
<h3>
|
||||
<field
|
||||
name="reference"
|
||||
placeholder="Reference. Can contain English letters, digits and '_'. Leave blank to autogenerate"
|
||||
/>
|
||||
</h3>
|
||||
</div>
|
||||
<group>
|
||||
<group string="General">
|
||||
<field name="active" invisible="1" />
|
||||
<field
|
||||
name="server_ids"
|
||||
widget="many2many_tags"
|
||||
attrs="{'invisible': [('server_ids', '=', [])]}"
|
||||
/>
|
||||
<field
|
||||
name="file_template_ids"
|
||||
widget="many2many_tags"
|
||||
attrs="{'invisible': [('file_template_ids', '=', [])]}"
|
||||
/>
|
||||
</group>
|
||||
<group string="Git Aggregator">
|
||||
<field
|
||||
name="git_aggregator_root_dir"
|
||||
string="Root Directory"
|
||||
placeholder="Git aggregator root directory where sources will be cloned. Leave blank to use '.'"
|
||||
/>
|
||||
</group>
|
||||
<field name="note" placeholder="Put your notes here..." />
|
||||
</group>
|
||||
<notebook>
|
||||
<page name="sources" string="Sources">
|
||||
<field name="source_ids">
|
||||
<tree
|
||||
decoration-muted="not enabled"
|
||||
decoration-info="remote_count_private == remote_count and remote_count > 0"
|
||||
decoration-warning="remote_count_private != remote_count and remote_count > 0 and remote_count_private > 0"
|
||||
>
|
||||
<field name="sequence" widget="handle" />
|
||||
<field
|
||||
name="name"
|
||||
placeholder="..to be autogenerated"
|
||||
/>
|
||||
<field name="remote_count" optional="show" />
|
||||
<field
|
||||
name="remote_count_private"
|
||||
optional="hide"
|
||||
/>
|
||||
<field name="enabled" widget="boolean_toggle" />
|
||||
<field name="reference" optional="hide" />
|
||||
<field name="create_uid" optional="hide" />
|
||||
</tree>
|
||||
</field>
|
||||
<field name="has_private_remotes" invisible="1" />
|
||||
<field name="has_partially_private_remotes" invisible="1" />
|
||||
<div
|
||||
class="text-info"
|
||||
attrs="{'invisible': [('has_private_remotes', '=', False)]}"
|
||||
>
|
||||
<p>
|
||||
* Sources where all remotes are private
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="text-warning"
|
||||
attrs="{'invisible': [('has_partially_private_remotes', '=', False)]}"
|
||||
>
|
||||
<p>
|
||||
* Sources where some remotes are private
|
||||
</p>
|
||||
</div>
|
||||
</page>
|
||||
<page name="files" string="Files">
|
||||
<field name="git_project_rel_ids">
|
||||
<tree editable="bottom">
|
||||
<field
|
||||
name="server_id"
|
||||
options="{'no_create': True, 'no_create_edit': True}"
|
||||
/>
|
||||
<field
|
||||
name="file_id"
|
||||
options="{'no_create': True, 'no_create_edit': True}"
|
||||
/>
|
||||
<field name="project_format" />
|
||||
<field name="auto_sync" />
|
||||
<button
|
||||
type="object"
|
||||
name="action_open_server"
|
||||
string="Open Server"
|
||||
title="Open Server"
|
||||
class="btn-secondary"
|
||||
/>
|
||||
</tree>
|
||||
<form>
|
||||
<group>
|
||||
<field name="server_id" />
|
||||
<field name="file_id" />
|
||||
<field name="project_format" />
|
||||
<field name="auto_sync" />
|
||||
</group>
|
||||
</form>
|
||||
</field>
|
||||
</page>
|
||||
<page name="file_templates" string="File Templates">
|
||||
<field name="git_project_file_template_rel_ids">
|
||||
<tree editable="bottom">
|
||||
<field
|
||||
name="file_template_id"
|
||||
options="{'no_create': True, 'no_create_edit': True}"
|
||||
/>
|
||||
<field name="project_format" />
|
||||
<button
|
||||
type="object"
|
||||
string="Open file template"
|
||||
name="action_open_file_template"
|
||||
title="Open File Template"
|
||||
class="btn-secondary"
|
||||
/>
|
||||
</tree>
|
||||
<form>
|
||||
<group>
|
||||
<field name="file_template_id" />
|
||||
<field name="project_format" />
|
||||
</group>
|
||||
</form>
|
||||
</field>
|
||||
</page>
|
||||
<page
|
||||
name="repos"
|
||||
string="Repos"
|
||||
groups="base.group_no_one"
|
||||
attrs="{'invisible': [('repo_ids', '=', [])]}"
|
||||
>
|
||||
<field name="repo_ids" />
|
||||
</page>
|
||||
<page
|
||||
name="access"
|
||||
string="Access"
|
||||
groups="cetmix_tower_server.group_manager"
|
||||
>
|
||||
<group name="access">
|
||||
<field
|
||||
name="user_ids"
|
||||
widget="many2many_tags"
|
||||
placeholder="users who can view this record"
|
||||
options="{'no_create': True}"
|
||||
/>
|
||||
<field
|
||||
name="manager_ids"
|
||||
widget="many2many_tags"
|
||||
placeholder="managers who can modify this record"
|
||||
options="{'no_create': True}"
|
||||
/>
|
||||
</group>
|
||||
<div
|
||||
class="alert alert-warning"
|
||||
role="alert"
|
||||
style="margin-bottom:0px;"
|
||||
>
|
||||
<ul>
|
||||
<li>
|
||||
<b
|
||||
>Users.</b> All users who have "Manager" group and are either set in "Users" or in "Managers" in <b
|
||||
><u>all</u></b> related servers.
|
||||
</li>
|
||||
<li>
|
||||
<b
|
||||
>Managers.</b> All users who have "Manager" group and are set as "Managers" in <b
|
||||
><u>all</u></b> 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.
|
||||
</li>
|
||||
</ul>
|
||||
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.
|
||||
</div>
|
||||
</page>
|
||||
<page name="yaml" string="YAML">
|
||||
<div groups="!cetmix_tower_yaml.group_export">
|
||||
<h3
|
||||
>You must be a member of the "YAML/Export" group to export data as YAML.</h3>
|
||||
</div>
|
||||
<button
|
||||
type="object"
|
||||
groups="cetmix_tower_yaml.group_export"
|
||||
class="oe_highlight"
|
||||
name="action_open_yaml_export_wizard"
|
||||
string="Export YAML"
|
||||
/>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Action -->
|
||||
<record id="cx_tower_git_project_action" model="ir.actions.act_window">
|
||||
<field name="name">Git Projects</field>
|
||||
<field name="res_model">cx.tower.git.project</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
</record>
|
||||
|
||||
|
||||
</odoo>
|
||||
84
addons/cetmix_tower_git/views/cx_tower_git_remote_views.xml
Normal file
84
addons/cetmix_tower_git/views/cx_tower_git_remote_views.xml
Normal file
@@ -0,0 +1,84 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
|
||||
<!-- Tree View -->
|
||||
<record id="cx_tower_git_remote_view_tree" model="ir.ui.view">
|
||||
<field name="name">cx.tower.git.remote.tree</field>
|
||||
<field name="model">cx.tower.git.remote</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree decoration-info="is_private == True">
|
||||
<field
|
||||
name="repo_id"
|
||||
placeholder="select or enter a link"
|
||||
options="{'no_create_edit': True}"
|
||||
/>
|
||||
<field name="is_private" optional="hide" />
|
||||
<field name="head_type" />
|
||||
<field name="head" />
|
||||
<field name="enabled" widget="boolean_toggle" />
|
||||
<field name="create_uid" optional="hide" />
|
||||
<field name="reference" optional="hide" />
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Form View -->
|
||||
<record id="cx_tower_git_remote_view_form" model="ir.ui.view">
|
||||
<field name="name">cx.tower.git.remote.form</field>
|
||||
<field name="model">cx.tower.git.remote</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<sheet>
|
||||
<widget
|
||||
name="web_ribbon"
|
||||
title="Disabled"
|
||||
bg_color="bg-danger"
|
||||
attrs="{'invisible': [('enabled', '=', True)]}"
|
||||
/>
|
||||
<group>
|
||||
<field name="sequence" />
|
||||
<field name="enabled" />
|
||||
<field name="active" invisible="1" />
|
||||
<field
|
||||
name="repo_id"
|
||||
placeholder="select or enter a link"
|
||||
options="{'no_create_edit': True}"
|
||||
/>
|
||||
<field name="is_private" />
|
||||
<field
|
||||
name="url_protocol"
|
||||
widget="radio"
|
||||
options="{'horizontal': true}"
|
||||
/>
|
||||
<field
|
||||
name="head_type"
|
||||
widget="radio"
|
||||
options="{'horizontal': true}"
|
||||
/>
|
||||
<field
|
||||
name="head"
|
||||
placeholder="Branch/PR/commit number or link"
|
||||
/>
|
||||
<field name="create_uid" />
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Action -->
|
||||
<record id="action_cx_tower_git_remote" model="ir.actions.act_window">
|
||||
<field name="name">Git Remotes</field>
|
||||
<field name="res_model">cx.tower.git.remote</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Create your first git remote!
|
||||
</p>
|
||||
<p>
|
||||
Git remotes represent branches, pull requests, or commits from git repositories.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,65 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
|
||||
<!-- Tree View -->
|
||||
<record id="cx_tower_git_repo_owner_view_tree" model="ir.ui.view">
|
||||
<field name="name">cx.tower.git.repo.owner.tree</field>
|
||||
<field name="model">cx.tower.git.repo.owner</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree>
|
||||
<field name="display_name" />
|
||||
<field name="name" />
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Form View -->
|
||||
<record id="cx_tower_git_repo_owner_view_form" model="ir.ui.view">
|
||||
<field name="name">cx.tower.git.repo.owner.form</field>
|
||||
<field name="model">cx.tower.git.repo.owner</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="display_name" placeholder="e.g., Cetmix, OCA" />
|
||||
<field name="name" placeholder="e.g., cetmix, oca" />
|
||||
<field
|
||||
name="reference"
|
||||
placeholder="Can contain English letters, digits and '_'. Leave blank to autogenerate"
|
||||
/>
|
||||
<field name="secret_id" />
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Repositories" name="repositories">
|
||||
<field name="repo_ids" readonly="1">
|
||||
<tree>
|
||||
<field name="name" />
|
||||
<field name="reference" optional="hide" />
|
||||
<field name="provider" />
|
||||
<field name="is_private" />
|
||||
</tree>
|
||||
</field>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Action -->
|
||||
<record id="action_cx_tower_git_repo_owner" model="ir.actions.act_window">
|
||||
<field name="name">Repository Owners</field>
|
||||
<field name="res_model">cx.tower.git.repo.owner</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Create your first repository owner!
|
||||
</p>
|
||||
<p>
|
||||
Repository owners represent organizations or users that own git repositories.
|
||||
Examples include "cetmix", "OCA", etc.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
163
addons/cetmix_tower_git/views/cx_tower_git_repo_views.xml
Normal file
163
addons/cetmix_tower_git/views/cx_tower_git_repo_views.xml
Normal file
@@ -0,0 +1,163 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
|
||||
<!-- Tree View -->
|
||||
<record id="cx_tower_git_repo_view_tree" model="ir.ui.view">
|
||||
<field name="name">cx.tower.git.repo.tree</field>
|
||||
<field name="model">cx.tower.git.repo</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree decoration-info="is_private == True">
|
||||
<field name="name" />
|
||||
<field name="reference" optional="hide" />
|
||||
<field name="provider" optional="show" />
|
||||
<field name="owner_id" optional="hide" />
|
||||
<field name="is_private" optional="hide" />
|
||||
<field name="remote_count" optional="hide" />
|
||||
<field name="git_project_count" optional="hide" />
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Form View -->
|
||||
<record id="cx_tower_git_repo_view_form" model="ir.ui.view">
|
||||
<field name="name">cx.tower.git.repo.form</field>
|
||||
<field name="model">cx.tower.git.repo</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button
|
||||
name="action_view_remotes"
|
||||
type="object"
|
||||
string="Remotes"
|
||||
class="oe_stat_button"
|
||||
icon="fa-external-link"
|
||||
attrs="{'invisible': [('remote_count', '=', 0)]}"
|
||||
>
|
||||
<field
|
||||
name="remote_count"
|
||||
widget="statinfo"
|
||||
string="Remotes"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
name="action_view_projects"
|
||||
type="object"
|
||||
string="GitProjects"
|
||||
class="oe_stat_button"
|
||||
icon="fa-folder"
|
||||
attrs="{'invisible': [('git_project_count', '=', 0)]}"
|
||||
>
|
||||
<field
|
||||
name="git_project_count"
|
||||
widget="statinfo"
|
||||
string="Projects"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<group>
|
||||
<group name="info">
|
||||
<field
|
||||
name="url"
|
||||
placeholder="https, ssh or git formats are accepted"
|
||||
/>
|
||||
<field name="url_ssh" />
|
||||
<field name="url_git" />
|
||||
<field name="repo" placeholder="e.g., cetmix-tower, odoo" />
|
||||
<field name="host" />
|
||||
</group>
|
||||
<group name="details">
|
||||
<field
|
||||
name="reference"
|
||||
placeholder="Can contain English letters, digits and '_'. Leave blank to autogenerate"
|
||||
/>
|
||||
<field name="active" />
|
||||
<field name="owner_id" />
|
||||
<field name="provider" />
|
||||
<field name="is_private" />
|
||||
<field name="secret_id" />
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Search View -->
|
||||
<record id="cx_tower_git_repo_view_search" model="ir.ui.view">
|
||||
<field name="name">cx.tower.git.repo.search</field>
|
||||
<field name="model">cx.tower.git.repo</field>
|
||||
<field name="arch" type="xml">
|
||||
<search>
|
||||
<field
|
||||
name="repo"
|
||||
string="Name/Reference"
|
||||
filter_domain="['|', ('repo', 'ilike', self), ('reference', 'ilike', self)]"
|
||||
/>
|
||||
<field name="owner_id" string="Org" />
|
||||
<field name="provider" />
|
||||
|
||||
<filter
|
||||
string="Public"
|
||||
name="public"
|
||||
domain="[('is_private', '=', False)]"
|
||||
/>
|
||||
<filter
|
||||
string="Private"
|
||||
name="private"
|
||||
domain="[('is_private', '=', True)]"
|
||||
/>
|
||||
<separator />
|
||||
<filter
|
||||
string="Provider: Other"
|
||||
name="no_provider"
|
||||
domain="[('provider', '=', 'other')]"
|
||||
/>
|
||||
|
||||
<group expand="0" string="Group By">
|
||||
<filter
|
||||
string="Provider"
|
||||
name="group_provider"
|
||||
context="{'group_by': 'provider'}"
|
||||
/>
|
||||
<filter
|
||||
string="Org"
|
||||
name="group_owner"
|
||||
context="{'group_by': 'owner_id'}"
|
||||
/>
|
||||
</group>
|
||||
<searchpanel>
|
||||
<field
|
||||
name="provider"
|
||||
string="Provider"
|
||||
icon="fa-globe"
|
||||
enable_counters="1"
|
||||
/>
|
||||
<field
|
||||
name="owner_id"
|
||||
string="Org"
|
||||
icon="fa-building"
|
||||
enable_counters="1"
|
||||
/>
|
||||
</searchpanel>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Action -->
|
||||
<record id="action_cx_tower_git_repo" model="ir.actions.act_window">
|
||||
<field name="name">Repositories</field>
|
||||
<field name="res_model">cx.tower.git.repo</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Create your first repository!
|
||||
</p>
|
||||
<p>
|
||||
Repositories represent git repositories with their metadata and configuration.
|
||||
They can be linked to remotes to automatically populate URL information.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
86
addons/cetmix_tower_git/views/cx_tower_git_source_views.xml
Normal file
86
addons/cetmix_tower_git/views/cx_tower_git_source_views.xml
Normal file
@@ -0,0 +1,86 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
|
||||
<!-- Tree View -->
|
||||
<record id="cx_tower_git_source_view_tree" model="ir.ui.view">
|
||||
<field name="name">cx.tower.git.source.tree</field>
|
||||
<field name="model">cx.tower.git.source</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree>
|
||||
<field name="name" />
|
||||
<field name="git_project_id" />
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Form View -->
|
||||
<record id="cx_tower_git_source_view_form" model="ir.ui.view">
|
||||
<field name="name">cx.tower.git.source.form</field>
|
||||
<field name="model">cx.tower.git.source</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<sheet>
|
||||
<widget
|
||||
name="web_ribbon"
|
||||
title="Disabled"
|
||||
bg_color="bg-danger"
|
||||
attrs="{'invisible': [('enabled', '=', True)]}"
|
||||
/>
|
||||
<div class="oe_title">
|
||||
<h1>
|
||||
<field
|
||||
name="name"
|
||||
placeholder="Name. Leave blank to autogenerate"
|
||||
/>
|
||||
</h1>
|
||||
<h3>
|
||||
<field
|
||||
name="reference"
|
||||
placeholder="Reference. Can contain English letters, digits and '_'. Leave blank to autogenerate"
|
||||
attrs="{'invisible': [('reference', '=', False)]}"
|
||||
/>
|
||||
</h3>
|
||||
</div>
|
||||
<group>
|
||||
<field name="sequence" />
|
||||
<field name="enabled" />
|
||||
<field name="active" invisible="1" />
|
||||
</group>
|
||||
<notebook>
|
||||
<page name="remotes" string="Remotes">
|
||||
<div
|
||||
class="alert alert-warning"
|
||||
role="alert"
|
||||
style="margin-bottom:0px;"
|
||||
attrs="{'invisible': [('remote_count', '<', 2)]}"
|
||||
>
|
||||
<p>
|
||||
The top one remote will be used as a merge target.
|
||||
You can re-arrange remotes by dragging them or changing their sequence value.
|
||||
</p>
|
||||
</div>
|
||||
<field name="remote_count" invisible="1" />
|
||||
<field name="remote_ids">
|
||||
<tree
|
||||
decoration-muted="not enabled"
|
||||
decoration-info="is_private == True"
|
||||
>
|
||||
<field name="sequence" widget="handle" />
|
||||
<field name="repo_id" />
|
||||
<field name="head_type" />
|
||||
<field name="head" />
|
||||
<field name="url_protocol" />
|
||||
<field name="enabled" widget="boolean_toggle" />
|
||||
<field name="is_private" optional="hide" />
|
||||
<field name="reference" optional="hide" />
|
||||
<field name="create_uid" optional="hide" />
|
||||
</tree>
|
||||
</field>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
19
addons/cetmix_tower_git/views/cx_tower_plan_line_view.xml
Normal file
19
addons/cetmix_tower_git/views/cx_tower_plan_line_view.xml
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
<record id="cx_tower_plan_line_view_form" model="ir.ui.view">
|
||||
<field name="name">cx.tower.plan.line.view.form</field>
|
||||
<field name="model">cx.tower.plan.line</field>
|
||||
<field
|
||||
name="inherit_id"
|
||||
ref="cetmix_tower_server.cx_tower_plan_line_view_form"
|
||||
/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='condition']" position="before">
|
||||
<group attrs="{'invisible': [('action', '!=', 'file_using_template')]}">
|
||||
<field name="git_project_id" />
|
||||
<field name="is_make_copy" />
|
||||
</group>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
45
addons/cetmix_tower_git/views/cx_tower_server_view.xml
Normal file
45
addons/cetmix_tower_git/views/cx_tower_server_view.xml
Normal file
@@ -0,0 +1,45 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
|
||||
<record id="cx_tower_server_view_form" model="ir.ui.view">
|
||||
<field name="name">cx.tower.server.view.form.shortcuts</field>
|
||||
<field name="model">cx.tower.server</field>
|
||||
<field name="inherit_id" ref="cetmix_tower_server.cx_tower_server_view_form" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//notebook" position="inside">
|
||||
<page
|
||||
name="git_projects"
|
||||
string="Git Projects"
|
||||
groups="cetmix_tower_server.group_manager,cetmix_tower_server.group_root"
|
||||
>
|
||||
<field name="git_project_rel_ids">
|
||||
<tree editable="bottom">
|
||||
<field name="server_id" invisible="1" />
|
||||
<field name="git_project_id" />
|
||||
<field name="file_id" />
|
||||
<field name="project_format" />
|
||||
<field name="auto_sync" />
|
||||
<button
|
||||
type="object"
|
||||
string="Configure"
|
||||
name="action_open_project"
|
||||
title="Open Git Project"
|
||||
class="btn-secondary"
|
||||
/>
|
||||
</tree>
|
||||
<form>
|
||||
<group>
|
||||
<field name="server_id" invisible="1" />
|
||||
<field name="git_project_id" />
|
||||
<field name="file_id" />
|
||||
<field name="project_format" />
|
||||
<field name="auto_sync" />
|
||||
</group>
|
||||
</form>
|
||||
</field>
|
||||
</page>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
32
addons/cetmix_tower_git/views/menuitems.xml
Normal file
32
addons/cetmix_tower_git/views/menuitems.xml
Normal file
@@ -0,0 +1,32 @@
|
||||
<odoo>
|
||||
<!-- Git Projects -> Tools -->
|
||||
<menuitem
|
||||
id="menu_cx_tower_git_project"
|
||||
name="Git Projects"
|
||||
action="cx_tower_git_project_action"
|
||||
sequence="30"
|
||||
parent="cetmix_tower_server.menu_tools"
|
||||
/>
|
||||
|
||||
<!-- Git Projects Settings -> Settings -->
|
||||
<menuitem
|
||||
id="menu_cx_tower_git_project_settings"
|
||||
name="Git Projects"
|
||||
parent="cetmix_tower_server.menu_settings"
|
||||
sequence="60"
|
||||
/>
|
||||
<menuitem
|
||||
id="menu_cx_tower_git_repositories"
|
||||
name="Repositories"
|
||||
parent="menu_cx_tower_git_project_settings"
|
||||
action="action_cx_tower_git_repo"
|
||||
sequence="10"
|
||||
/>
|
||||
<menuitem
|
||||
id="menu_cx_tower_git_repository_owners"
|
||||
name="Repository Owners"
|
||||
parent="menu_cx_tower_git_project_settings"
|
||||
action="action_cx_tower_git_repo_owner"
|
||||
sequence="20"
|
||||
/>
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user