Wipe addons/: full reset for clean re-upload
This commit is contained in:
@@ -1,15 +0,0 @@
|
||||
# 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
|
||||
@@ -1,35 +0,0 @@
|
||||
# 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
|
||||
)
|
||||
@@ -1,37 +0,0 @@
|
||||
# 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
|
||||
@@ -1,47 +0,0 @@
|
||||
# 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,
|
||||
}
|
||||
)
|
||||
@@ -1,32 +0,0 @@
|
||||
# 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
|
||||
)
|
||||
@@ -1,370 +0,0 @@
|
||||
# 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_server_rel",
|
||||
readonly=True,
|
||||
copy=False,
|
||||
compute="_compute_server_ids",
|
||||
store=True,
|
||||
context={"active_test": False},
|
||||
help="Servers are added automatically based on the files"
|
||||
" linked to the project.",
|
||||
)
|
||||
source_ids = fields.One2many(
|
||||
comodel_name="cx.tower.git.source",
|
||||
inverse_name="git_project_id",
|
||||
string="Sources",
|
||||
auto_join=True,
|
||||
copy=True,
|
||||
)
|
||||
git_project_rel_ids = fields.One2many(
|
||||
comodel_name="cx.tower.git.project.rel",
|
||||
inverse_name="git_project_id",
|
||||
string="Git Project Server File Relations",
|
||||
copy=False,
|
||||
)
|
||||
# Helper field to get all files related to git project
|
||||
file_ids = fields.Many2many(
|
||||
comodel_name="cx.tower.file",
|
||||
relation="cx_tower_git_project_rel",
|
||||
column1="git_project_id",
|
||||
column2="file_id",
|
||||
string="Files",
|
||||
readonly=True,
|
||||
depends=["git_project_rel_ids"],
|
||||
copy=False,
|
||||
)
|
||||
git_project_file_template_rel_ids = fields.One2many(
|
||||
comodel_name="cx.tower.git.project.file.template.rel",
|
||||
inverse_name="git_project_id",
|
||||
string="Git Project File Template Relations",
|
||||
copy=False,
|
||||
)
|
||||
# Helper field to get all file templates related to git project
|
||||
file_template_ids = fields.Many2many(
|
||||
comodel_name="cx.tower.file.template",
|
||||
relation="cx_tower_git_project_file_template_rel",
|
||||
column1="git_project_id",
|
||||
column2="file_template_id",
|
||||
string="File Templates",
|
||||
readonly=True,
|
||||
depends=["git_project_file_template_rel_ids"],
|
||||
copy=False,
|
||||
)
|
||||
# Helper field to get all repositories used in this project
|
||||
repo_ids = fields.Many2many(
|
||||
comodel_name="cx.tower.git.repo",
|
||||
relation="cx_tower_git_repo_project_rel",
|
||||
column1="project_id",
|
||||
column2="repo_id",
|
||||
string="Repositories",
|
||||
readonly=True,
|
||||
copy=False,
|
||||
help="Repositories used in this project through its sources and remotes",
|
||||
)
|
||||
note = fields.Text()
|
||||
|
||||
# ---- Access. Add relation for mixin fields
|
||||
user_ids = fields.Many2many(
|
||||
relation="cx_tower_git_project_user_rel",
|
||||
compute="_compute_user_ids",
|
||||
readonly=False,
|
||||
store=True,
|
||||
precompute=True,
|
||||
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", "git_project_rel_ids.server_id")
|
||||
def _compute_server_ids(self):
|
||||
"""Compute server ids for git projects.
|
||||
|
||||
Why? Because a git project can be linked to multiple files
|
||||
on the same server.
|
||||
So we need to use a set to avoid duplicates so every server
|
||||
is listed only once.
|
||||
"""
|
||||
for project in self:
|
||||
project.server_ids = (
|
||||
list(set(project.git_project_rel_ids.server_id.ids))
|
||||
if project.git_project_rel_ids
|
||||
else False
|
||||
)
|
||||
|
||||
@api.depends(
|
||||
"git_project_rel_ids.server_id",
|
||||
"git_project_rel_ids.server_id.user_ids",
|
||||
"git_project_rel_ids.server_id.manager_ids",
|
||||
)
|
||||
def _compute_user_ids(self):
|
||||
"""
|
||||
Users. All users who have "Manager" group and are either set in "Users"
|
||||
or in "Managers" in all related servers.
|
||||
Managers. All users who have "Manager" group and are set as "Managers"
|
||||
in all related servers.
|
||||
|
||||
This is done to avoid unpredictable consequences when some of the servers
|
||||
are not updated due to access restrictions when a project is updated.
|
||||
"""
|
||||
for project in self:
|
||||
# Do not compute if no servers are related
|
||||
server_ids = project.git_project_rel_ids.server_id
|
||||
if not server_ids:
|
||||
continue
|
||||
|
||||
# Get all user and manager ids from related servers
|
||||
all_user_ids = server_ids.user_ids.filtered(
|
||||
lambda u: u.has_group("cetmix_tower_server.group_manager")
|
||||
).ids
|
||||
all_manager_ids = server_ids.manager_ids.ids
|
||||
|
||||
# Create a final list of user and manager ids
|
||||
user_ids = []
|
||||
manager_ids = []
|
||||
# Check if user is present in all servers
|
||||
for user_id in all_user_ids:
|
||||
if all(
|
||||
user_id in server.user_ids.ids or user_id in server.manager_ids.ids
|
||||
for server in server_ids
|
||||
):
|
||||
user_ids.append(user_id)
|
||||
# Check if manager is present in all servers
|
||||
for manager_id in all_manager_ids:
|
||||
if all(manager_id in server.manager_ids.ids for server in server_ids):
|
||||
manager_ids.append(manager_id)
|
||||
|
||||
# Set the final lists
|
||||
project.update(
|
||||
{
|
||||
"user_ids": [(6, 0, user_ids)],
|
||||
"manager_ids": [(6, 0, manager_ids)],
|
||||
}
|
||||
)
|
||||
|
||||
@api.depends(
|
||||
"source_ids", "source_ids.remote_ids", "source_ids.remote_ids.is_private"
|
||||
)
|
||||
def _compute_has_private_remotes(self):
|
||||
for project in self:
|
||||
project.has_private_remotes = any(
|
||||
source.remote_count > 0
|
||||
and source.remote_count_private == source.remote_count
|
||||
for source in project.source_ids
|
||||
)
|
||||
project.has_partially_private_remotes = any(
|
||||
source.remote_count_private > 0
|
||||
and source.remote_count_private != source.remote_count
|
||||
for source in project.source_ids
|
||||
)
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
res = super().create(vals_list)
|
||||
# Update related files and templates on create
|
||||
res._update_related_files_and_templates()
|
||||
return res
|
||||
|
||||
def write(self, vals):
|
||||
res = super().write(vals)
|
||||
# Update related files and templates on update
|
||||
self._update_related_files_and_templates()
|
||||
return res
|
||||
|
||||
# ------------------------------
|
||||
# Helper methods
|
||||
# ------------------------------
|
||||
def _update_related_files_and_templates(self):
|
||||
# Update related files and templates
|
||||
if self.git_project_rel_ids:
|
||||
self.git_project_rel_ids._save_to_file()
|
||||
if self.git_project_file_template_rel_ids:
|
||||
self.git_project_file_template_rel_ids._save_to_file_template()
|
||||
|
||||
def _extract_variables_from_text(self, text):
|
||||
"""Extract environment variables from text.
|
||||
Helper method for file content generation.
|
||||
|
||||
Args:
|
||||
text (str): Text to extract variables from
|
||||
Returns:
|
||||
List: List of variables
|
||||
"""
|
||||
variables = re.findall(r"\$([A-Z0-9_]+)", text)
|
||||
return sorted(list(set(variables)))
|
||||
|
||||
def _compose_copy_name(self, server=False):
|
||||
"""
|
||||
Compose copy name of a git project copy.
|
||||
Helper method used when creating a copy of a git project.
|
||||
|
||||
Args:
|
||||
server (cx.tower.server): Server to get the copy name for.
|
||||
|
||||
Returns:
|
||||
Char: Copy name
|
||||
"""
|
||||
self.ensure_one()
|
||||
if server:
|
||||
return server.name
|
||||
return _("%(name)s (copy)", name=self.name)
|
||||
|
||||
# ------------------------------
|
||||
# YAML mixin methods
|
||||
# ------------------------------
|
||||
def _get_fields_for_yaml(self):
|
||||
res = super()._get_fields_for_yaml()
|
||||
res += [
|
||||
"name",
|
||||
"note",
|
||||
"source_ids",
|
||||
"git_aggregator_root_dir",
|
||||
]
|
||||
return res
|
||||
|
||||
# -------------------------------
|
||||
# Git Aggregator related methods
|
||||
# -------------------------------
|
||||
def _git_aggregator_prepare_record(self):
|
||||
"""Prepare json structure for git aggregator.
|
||||
|
||||
Returns:
|
||||
Dict: Json structure for git aggregator
|
||||
"""
|
||||
self.ensure_one()
|
||||
values = {}
|
||||
for source in self.source_ids:
|
||||
if source.enabled and source.remote_count:
|
||||
root_dir = self.git_aggregator_root_dir or "."
|
||||
values.update(
|
||||
{
|
||||
f"/{source.reference}"
|
||||
if root_dir == "/"
|
||||
else f"{root_dir}/{source.reference}": source._git_aggregator_prepare_record() # noqa: E501
|
||||
}
|
||||
)
|
||||
return values
|
||||
|
||||
def _git_aggregator_prepare_yaml_comment(self, yaml_code):
|
||||
"""Generate commentary for yaml file.
|
||||
It includes brief instructions for git aggregator
|
||||
and lists environment variables that are required.
|
||||
|
||||
Args:
|
||||
yaml_code (str): Yaml code
|
||||
|
||||
Returns:
|
||||
Char: comment text or None
|
||||
"""
|
||||
|
||||
comment_text = _(
|
||||
"# This file is generated with Cetmix Tower https://cetmix.com/tower\n"
|
||||
"# It's designed to be used with git-aggregator tool developed by Acsone.\n"
|
||||
"# Documentation for git-aggregator: https://github.com/acsone/git-aggregator\n"
|
||||
)
|
||||
variable_list = self._extract_variables_from_text(yaml_code)
|
||||
if variable_list:
|
||||
comment_text += _(
|
||||
"\n# You need to set the following variables in your environment:\n# %(vars)s\n" # noqa: E501
|
||||
"# and run git-aggregator with '--expand-env' parameter.\n", # noqa: E501
|
||||
vars=(", ".join(variable_list)),
|
||||
)
|
||||
return comment_text
|
||||
|
||||
def _generate_code_git_aggregator(self, record):
|
||||
"""Generate code in git-aggregator format.
|
||||
|
||||
Args:
|
||||
record (recordset()): Model record to generate code for.
|
||||
must be a single record and have git_project_id field.
|
||||
|
||||
Returns:
|
||||
Text: Yaml code
|
||||
"""
|
||||
yaml_mixin = self.env["cx.tower.yaml.mixin"]
|
||||
|
||||
# Do not generate code if record values are empty
|
||||
record_values = record.git_project_id._git_aggregator_prepare_record()
|
||||
if record_values:
|
||||
yaml_code = yaml_mixin._convert_dict_to_yaml(record_values)
|
||||
# Prepend comment to yaml code
|
||||
comment = record.git_project_id._git_aggregator_prepare_yaml_comment(
|
||||
yaml_code
|
||||
)
|
||||
return f"{comment}\n{yaml_code}"
|
||||
return ""
|
||||
@@ -1,113 +0,0 @@
|
||||
# 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})
|
||||
@@ -1,177 +0,0 @@
|
||||
# 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
|
||||
@@ -1,415 +0,0 @@
|
||||
# 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
|
||||
@@ -1,409 +0,0 @@
|
||||
# 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
|
||||
@@ -1,107 +0,0 @@
|
||||
# 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
|
||||
@@ -1,189 +0,0 @@
|
||||
# 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
|
||||
@@ -1,32 +0,0 @@
|
||||
# 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
|
||||
@@ -1,187 +0,0 @@
|
||||
# Copyright (C) 2024 Cetmix OÜ
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
import logging
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CxTowerServer(models.Model):
|
||||
_inherit = "cx.tower.server"
|
||||
|
||||
git_project_rel_ids = fields.One2many(
|
||||
comodel_name="cx.tower.git.project.rel",
|
||||
inverse_name="server_id",
|
||||
copy=False,
|
||||
depends=["git_project_ids"],
|
||||
groups="cetmix_tower_server.group_manager,cetmix_tower_server.group_root",
|
||||
)
|
||||
|
||||
# Helper field to get all git projects related to server
|
||||
# IMPORTANT: This field may contain duplicates because of the relation nature!
|
||||
git_project_ids = fields.Many2many(
|
||||
comodel_name="cx.tower.git.project",
|
||||
relation="cx_tower_git_project_rel",
|
||||
column1="server_id",
|
||||
column2="git_project_id",
|
||||
readonly=True,
|
||||
copy=False,
|
||||
depends=["git_project_rel_ids"],
|
||||
groups="cetmix_tower_server.group_manager,cetmix_tower_server.group_root",
|
||||
)
|
||||
|
||||
# ------------------------------
|
||||
# YAML mixin methods
|
||||
# ------------------------------
|
||||
def _get_fields_for_yaml(self):
|
||||
res = super()._get_fields_for_yaml()
|
||||
res += [
|
||||
"git_project_rel_ids",
|
||||
]
|
||||
return res
|
||||
|
||||
def _get_force_x2m_resolve_models(self):
|
||||
res = super()._get_force_x2m_resolve_models()
|
||||
|
||||
# Add File in order to always try to use existing one
|
||||
res += ["cx.tower.file"]
|
||||
return res
|
||||
|
||||
def _update_or_create_related_record(
|
||||
self, model, reference, values, create_immediately=False
|
||||
):
|
||||
# Files must be created immediately because they are related
|
||||
# to both server and git project.
|
||||
# So if a file is not created immediately when it is created
|
||||
# for the server, the same file will be created for the git project.
|
||||
# This will lead to creation of two files with the same content
|
||||
# for the same server.
|
||||
|
||||
if model._name == "cx.tower.file":
|
||||
create_immediately = True
|
||||
return super()._update_or_create_related_record(
|
||||
model, reference, values, create_immediately=create_immediately
|
||||
)
|
||||
|
||||
@api.model
|
||||
def get_servers_by_git_ref(self, repository_url, head=None, head_type=None):
|
||||
"""
|
||||
Return servers linked to a given Git repository reference.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
repository_url : str
|
||||
Pre-normalized canonical Git URL
|
||||
(e.g. ``https://host/owner/repo.git``).
|
||||
head : str, optional
|
||||
Branch name, commit SHA, or PR identifier.
|
||||
head_type : {'branch', 'commit', 'pr'}, optional
|
||||
Type of the ``head`` argument.
|
||||
If only ``head`` is provided, it will match across all head types.
|
||||
If only ``head_type`` is provided, it will filter by type regardless of head
|
||||
|
||||
Returns
|
||||
-------
|
||||
recordset of cx.tower.server
|
||||
Matching servers. Empty recordset if no matches.
|
||||
"""
|
||||
|
||||
server_obj = self.env["cx.tower.server"]
|
||||
# URL MUST be already canonical.
|
||||
if not repository_url:
|
||||
return server_obj
|
||||
|
||||
# Get repository id by URL
|
||||
repo_id = self.env["cx.tower.git.repo"]._get_repo_id_by_url(
|
||||
repository_url, raise_if_invalid=False
|
||||
)
|
||||
if not repo_id:
|
||||
return server_obj
|
||||
repo = self.env["cx.tower.git.repo"].browse(repo_id)
|
||||
|
||||
# Compose domain for remotes
|
||||
remote_domain = [
|
||||
("source_id.enabled", "=", True),
|
||||
("enabled", "=", True),
|
||||
]
|
||||
if head:
|
||||
head = self.env["cx.tower.git.remote"]._sanitize_head(head)
|
||||
remote_domain.append(("head", "=", head))
|
||||
if head_type:
|
||||
remote_domain.append(("head_type", "=", head_type))
|
||||
|
||||
# Get remotes
|
||||
remotes = repo.remote_ids.filtered_domain(remote_domain)
|
||||
if not remotes:
|
||||
return server_obj
|
||||
|
||||
# Get servers from remotes
|
||||
servers = remotes.mapped("git_project_id.git_project_rel_ids.server_id")
|
||||
return servers
|
||||
|
||||
def _command_runner_file_using_template_create_file(
|
||||
self,
|
||||
log_record,
|
||||
server_dir,
|
||||
**kwargs,
|
||||
):
|
||||
"""Override to create git project relation
|
||||
when creating a file using a template.
|
||||
"""
|
||||
file = super()._command_runner_file_using_template_create_file(
|
||||
log_record, server_dir, **kwargs
|
||||
)
|
||||
if file:
|
||||
# Get the flight plan line from log record
|
||||
plan_line = log_record.plan_log_id.plan_line_executed_id
|
||||
# Try to get git project from custom values
|
||||
custom_values = log_record.variable_values
|
||||
git_project_reference = custom_values and custom_values.get(
|
||||
"__git_project__"
|
||||
)
|
||||
if git_project_reference:
|
||||
git_project = self.env["cx.tower.git.project"].get_by_reference(
|
||||
git_project_reference
|
||||
)
|
||||
if not git_project:
|
||||
_logger.warning(
|
||||
"Git project '%s' provided with the `__git_project__` "
|
||||
"custom value not found for server '%s' "
|
||||
"in flight plan line '%s' "
|
||||
"of the flight plan '%s'. "
|
||||
"No project was linked to the file '%s'.",
|
||||
git_project_reference,
|
||||
self.name,
|
||||
plan_line.name,
|
||||
log_record.plan_log_id.plan_id.name,
|
||||
file.name,
|
||||
)
|
||||
|
||||
# Try to get git project set explicitly in the flight plan line
|
||||
else:
|
||||
git_project = plan_line.git_project_id
|
||||
if not git_project:
|
||||
return file
|
||||
|
||||
if plan_line.is_make_copy:
|
||||
# Remove default_server_ids from context, because this relation
|
||||
# will be created through git_project_rel_ids.
|
||||
# default_server_ids will interfere at the moment when
|
||||
# pairs of values are created through SQL query
|
||||
# in the method write_real and it does not take into account
|
||||
# that in this case we are creating a copy of the git project
|
||||
git_project = git_project.with_context(default_server_ids=False).copy(
|
||||
{"name": git_project._compose_copy_name(server=self)}
|
||||
)
|
||||
|
||||
self.env["cx.tower.git.project.rel"].create(
|
||||
{
|
||||
"git_project_id": git_project.id,
|
||||
"server_id": self.id,
|
||||
"file_id": file.id,
|
||||
"project_format": git_project._default_project_format(),
|
||||
}
|
||||
)
|
||||
return file
|
||||
Reference in New Issue
Block a user