odoo-addon-odoo-repository 16.0.1.3.0.13__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- odoo/addons/odoo_repository/README.rst +81 -0
- odoo/addons/odoo_repository/__init__.py +2 -0
- odoo/addons/odoo_repository/__manifest__.py +58 -0
- odoo/addons/odoo_repository/controllers/__init__.py +1 -0
- odoo/addons/odoo_repository/controllers/main.py +32 -0
- odoo/addons/odoo_repository/data/ir_cron.xml +38 -0
- odoo/addons/odoo_repository/data/odoo.repository.csv +216 -0
- odoo/addons/odoo_repository/data/odoo_branch.xml +82 -0
- odoo/addons/odoo_repository/data/odoo_module.xml +16 -0
- odoo/addons/odoo_repository/data/odoo_repository.xml +71 -0
- odoo/addons/odoo_repository/data/odoo_repository_addons_path.xml +59 -0
- odoo/addons/odoo_repository/data/odoo_repository_org.xml +14 -0
- odoo/addons/odoo_repository/data/queue_job.xml +56 -0
- odoo/addons/odoo_repository/lib/__init__.py +0 -0
- odoo/addons/odoo_repository/lib/scanner.py +1302 -0
- odoo/addons/odoo_repository/migrations/16.0.1.1.0/post-migration.py +26 -0
- odoo/addons/odoo_repository/migrations/16.0.1.2.0/pre-migration.py +43 -0
- odoo/addons/odoo_repository/migrations/16.0.1.3.0/post-migration.py +19 -0
- odoo/addons/odoo_repository/models/__init__.py +18 -0
- odoo/addons/odoo_repository/models/authentication_token.py +12 -0
- odoo/addons/odoo_repository/models/odoo_author.py +16 -0
- odoo/addons/odoo_repository/models/odoo_branch.py +111 -0
- odoo/addons/odoo_repository/models/odoo_license.py +16 -0
- odoo/addons/odoo_repository/models/odoo_maintainer.py +31 -0
- odoo/addons/odoo_repository/models/odoo_module.py +24 -0
- odoo/addons/odoo_repository/models/odoo_module_branch.py +873 -0
- odoo/addons/odoo_repository/models/odoo_module_branch_version.py +123 -0
- odoo/addons/odoo_repository/models/odoo_module_category.py +15 -0
- odoo/addons/odoo_repository/models/odoo_module_dev_status.py +15 -0
- odoo/addons/odoo_repository/models/odoo_python_dependency.py +16 -0
- odoo/addons/odoo_repository/models/odoo_repository.py +664 -0
- odoo/addons/odoo_repository/models/odoo_repository_addons_path.py +40 -0
- odoo/addons/odoo_repository/models/odoo_repository_branch.py +98 -0
- odoo/addons/odoo_repository/models/odoo_repository_org.py +23 -0
- odoo/addons/odoo_repository/models/res_company.py +23 -0
- odoo/addons/odoo_repository/models/res_config_settings.py +23 -0
- odoo/addons/odoo_repository/models/ssh_key.py +12 -0
- odoo/addons/odoo_repository/readme/CONTRIBUTORS.rst +2 -0
- odoo/addons/odoo_repository/readme/DESCRIPTION.rst +1 -0
- odoo/addons/odoo_repository/security/ir.model.access.csv +27 -0
- odoo/addons/odoo_repository/security/res_groups.xml +25 -0
- odoo/addons/odoo_repository/static/description/README +4 -0
- odoo/addons/odoo_repository/static/description/icon.png +0 -0
- odoo/addons/odoo_repository/static/description/index.html +430 -0
- odoo/addons/odoo_repository/tests/__init__.py +6 -0
- odoo/addons/odoo_repository/tests/common.py +162 -0
- odoo/addons/odoo_repository/tests/test_base_scanner.py +214 -0
- odoo/addons/odoo_repository/tests/test_odoo_module_branch.py +97 -0
- odoo/addons/odoo_repository/tests/test_odoo_repository_scan.py +242 -0
- odoo/addons/odoo_repository/tests/test_repository_scanner.py +215 -0
- odoo/addons/odoo_repository/tests/test_sync_node.py +55 -0
- odoo/addons/odoo_repository/tests/test_utils.py +25 -0
- odoo/addons/odoo_repository/utils/__init__.py +0 -0
- odoo/addons/odoo_repository/utils/github.py +30 -0
- odoo/addons/odoo_repository/utils/module.py +25 -0
- odoo/addons/odoo_repository/utils/scanner.py +90 -0
- odoo/addons/odoo_repository/views/authentication_token.xml +63 -0
- odoo/addons/odoo_repository/views/menu.xml +38 -0
- odoo/addons/odoo_repository/views/odoo_author.xml +54 -0
- odoo/addons/odoo_repository/views/odoo_branch.xml +84 -0
- odoo/addons/odoo_repository/views/odoo_license.xml +40 -0
- odoo/addons/odoo_repository/views/odoo_maintainer.xml +69 -0
- odoo/addons/odoo_repository/views/odoo_module.xml +90 -0
- odoo/addons/odoo_repository/views/odoo_module_branch.xml +353 -0
- odoo/addons/odoo_repository/views/odoo_module_category.xml +40 -0
- odoo/addons/odoo_repository/views/odoo_module_dev_status.xml +40 -0
- odoo/addons/odoo_repository/views/odoo_python_dependency.xml +40 -0
- odoo/addons/odoo_repository/views/odoo_repository.xml +165 -0
- odoo/addons/odoo_repository/views/odoo_repository_addons_path.xml +49 -0
- odoo/addons/odoo_repository/views/odoo_repository_branch.xml +60 -0
- odoo/addons/odoo_repository/views/odoo_repository_org.xml +54 -0
- odoo/addons/odoo_repository/views/res_config_settings.xml +123 -0
- odoo/addons/odoo_repository/views/ssh_key.xml +63 -0
- odoo_addon_odoo_repository-16.0.1.3.0.13.dist-info/METADATA +100 -0
- odoo_addon_odoo_repository-16.0.1.3.0.13.dist-info/RECORD +77 -0
- odoo_addon_odoo_repository-16.0.1.3.0.13.dist-info/WHEEL +5 -0
- odoo_addon_odoo_repository-16.0.1.3.0.13.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,664 @@
|
|
|
1
|
+
# Copyright 2023 Camptocamp SA
|
|
2
|
+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import pathlib
|
|
8
|
+
from urllib.parse import urljoin
|
|
9
|
+
|
|
10
|
+
import requests
|
|
11
|
+
|
|
12
|
+
from odoo import _, api, fields, models, tools
|
|
13
|
+
from odoo.exceptions import UserError
|
|
14
|
+
from odoo.osv.expression import AND, OR
|
|
15
|
+
|
|
16
|
+
from odoo.addons.queue_job.delay import chain
|
|
17
|
+
from odoo.addons.queue_job.exception import RetryableJobError
|
|
18
|
+
from odoo.addons.queue_job.job import identity_exact
|
|
19
|
+
|
|
20
|
+
from ..utils.scanner import RepositoryScannerOdooEnv
|
|
21
|
+
|
|
22
|
+
_logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class OdooRepository(models.Model):
|
|
26
|
+
_name = "odoo.repository"
|
|
27
|
+
_description = "Odoo Modules Repository"
|
|
28
|
+
_order = "sequence, display_name"
|
|
29
|
+
|
|
30
|
+
_repositories_path_key = "odoo_repository_storage_path"
|
|
31
|
+
|
|
32
|
+
display_name = fields.Char(compute="_compute_display_name", store=True)
|
|
33
|
+
active = fields.Boolean(default=True)
|
|
34
|
+
sequence = fields.Integer(default=100)
|
|
35
|
+
org_id = fields.Many2one(
|
|
36
|
+
comodel_name="odoo.repository.org",
|
|
37
|
+
ondelete="cascade",
|
|
38
|
+
string="Organization",
|
|
39
|
+
required=True,
|
|
40
|
+
index=True,
|
|
41
|
+
)
|
|
42
|
+
name = fields.Char(required=True, index=True)
|
|
43
|
+
repo_url = fields.Char(
|
|
44
|
+
string="Web URL",
|
|
45
|
+
help="Web access to this repository.",
|
|
46
|
+
required=True,
|
|
47
|
+
)
|
|
48
|
+
to_scan = fields.Boolean(
|
|
49
|
+
default=True,
|
|
50
|
+
help="Scan this repository to collect data.",
|
|
51
|
+
)
|
|
52
|
+
clone_url = fields.Char(
|
|
53
|
+
string="Clone URL",
|
|
54
|
+
help="Used to clone the repository.",
|
|
55
|
+
)
|
|
56
|
+
clone_name = fields.Char(
|
|
57
|
+
help=(
|
|
58
|
+
"Force the name of the cloned repository (folder on disk). "
|
|
59
|
+
"If not set, the name of the repository will be used."
|
|
60
|
+
),
|
|
61
|
+
)
|
|
62
|
+
repo_type = fields.Selection(
|
|
63
|
+
selection=[
|
|
64
|
+
("github", "GitHub"),
|
|
65
|
+
("gitlab", "GitLab"),
|
|
66
|
+
],
|
|
67
|
+
)
|
|
68
|
+
ssh_key_id = fields.Many2one(
|
|
69
|
+
comodel_name="ssh.key",
|
|
70
|
+
ondelete="restrict",
|
|
71
|
+
string="SSH Key",
|
|
72
|
+
help="SSH key used to clone/fetch this repository.",
|
|
73
|
+
)
|
|
74
|
+
token_id = fields.Many2one(
|
|
75
|
+
comodel_name="authentication.token",
|
|
76
|
+
ondelete="restrict",
|
|
77
|
+
string="Token",
|
|
78
|
+
help="Token used to clone/fetch this repository.",
|
|
79
|
+
)
|
|
80
|
+
active = fields.Boolean(default=True)
|
|
81
|
+
addons_path_ids = fields.Many2many(
|
|
82
|
+
comodel_name="odoo.repository.addons_path",
|
|
83
|
+
string="Addons Path",
|
|
84
|
+
help="Relative path of folders in this repository hosting Odoo modules",
|
|
85
|
+
)
|
|
86
|
+
branch_ids = fields.One2many(
|
|
87
|
+
comodel_name="odoo.repository.branch",
|
|
88
|
+
inverse_name="repository_id",
|
|
89
|
+
string="Branches",
|
|
90
|
+
)
|
|
91
|
+
scan_weekday_ids = fields.Many2many(
|
|
92
|
+
comodel_name="time.weekday",
|
|
93
|
+
string="Scanning days",
|
|
94
|
+
help=(
|
|
95
|
+
"Limit scanning of this repository by the scheduled action to "
|
|
96
|
+
"certain days only. If not defined, the scan will happen every day."
|
|
97
|
+
),
|
|
98
|
+
)
|
|
99
|
+
manual_branches = fields.Boolean(
|
|
100
|
+
string="Configure branches manually",
|
|
101
|
+
help=(
|
|
102
|
+
"By default repository branches follows the configured Odoo versions "
|
|
103
|
+
"(e.g: 17.0, 18.0...). Enable this option to configure your own branches."
|
|
104
|
+
),
|
|
105
|
+
)
|
|
106
|
+
specific = fields.Boolean(
|
|
107
|
+
help=(
|
|
108
|
+
"Host specific modules (that are not generic). "
|
|
109
|
+
"Used for project repositories."
|
|
110
|
+
),
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
@api.model
|
|
114
|
+
def default_get(self, fields_list):
|
|
115
|
+
"""'default_get' method overloaded."""
|
|
116
|
+
res = super().default_get(fields_list)
|
|
117
|
+
if "addons_path_ids" not in res:
|
|
118
|
+
res["addons_path_ids"] = [
|
|
119
|
+
(
|
|
120
|
+
4,
|
|
121
|
+
self.env.ref(
|
|
122
|
+
"odoo_repository.odoo_repository_addons_path_community"
|
|
123
|
+
).id,
|
|
124
|
+
)
|
|
125
|
+
]
|
|
126
|
+
return res
|
|
127
|
+
|
|
128
|
+
@api.depends("org_id", "name")
|
|
129
|
+
def _compute_github_url(self):
|
|
130
|
+
for rec in self:
|
|
131
|
+
rec.github_url = f"{rec.org_id.github_url}/{rec.name}"
|
|
132
|
+
|
|
133
|
+
_sql_constraints = [
|
|
134
|
+
(
|
|
135
|
+
"org_id_name_uniq",
|
|
136
|
+
"UNIQUE (org_id, name)",
|
|
137
|
+
"This repository already exists.",
|
|
138
|
+
),
|
|
139
|
+
]
|
|
140
|
+
|
|
141
|
+
@api.depends("org_id.name", "name")
|
|
142
|
+
def _compute_display_name(self):
|
|
143
|
+
for rec in self:
|
|
144
|
+
rec.display_name = f"{rec.org_id.name}/{rec.name}"
|
|
145
|
+
|
|
146
|
+
@api.onchange("repo_url", "to_scan", "clone_url")
|
|
147
|
+
def _onchange_repo_url(self):
|
|
148
|
+
if not self.repo_url:
|
|
149
|
+
return
|
|
150
|
+
for type_, __ in self._fields["repo_type"].selection:
|
|
151
|
+
if type_ not in self.repo_url:
|
|
152
|
+
continue
|
|
153
|
+
self.repo_type = type_
|
|
154
|
+
if not self.clone_url and self.to_scan:
|
|
155
|
+
self.clone_url = self.repo_url
|
|
156
|
+
break
|
|
157
|
+
|
|
158
|
+
def _get_odoo_branches_to_scan(self):
|
|
159
|
+
self.ensure_one()
|
|
160
|
+
if self.manual_branches:
|
|
161
|
+
return self.branch_ids.branch_id
|
|
162
|
+
return self.env["odoo.branch"]._get_all_odoo_versions(active_test=True)
|
|
163
|
+
|
|
164
|
+
def _cron_scanner_domain(self):
|
|
165
|
+
today = fields.Date.today()
|
|
166
|
+
weekday = today.weekday()
|
|
167
|
+
return AND(
|
|
168
|
+
[
|
|
169
|
+
[("to_scan", "=", True)],
|
|
170
|
+
OR(
|
|
171
|
+
[
|
|
172
|
+
[("scan_weekday_ids.name", "=", weekday)],
|
|
173
|
+
[("scan_weekday_ids", "=", False)],
|
|
174
|
+
]
|
|
175
|
+
),
|
|
176
|
+
]
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
@api.model
|
|
180
|
+
def cron_scanner(self, branches=None, force=False):
|
|
181
|
+
"""Scan and collect Odoo repositories data.
|
|
182
|
+
|
|
183
|
+
As the scanner is run on the same server than Odoo, a special class
|
|
184
|
+
`RepositoryScannerOdooEnv` is used so the scanner can request Odoo
|
|
185
|
+
through an environment (api.Environment).
|
|
186
|
+
|
|
187
|
+
`branches` parameter allows to filter the `odoo.branch` to take into
|
|
188
|
+
account for the scan, e.g. `branches=["16.0", "18.0"]`.
|
|
189
|
+
"""
|
|
190
|
+
repositories = self.search(self._cron_scanner_domain())
|
|
191
|
+
branches_ = self.env["odoo.branch"]._get_all_odoo_versions(active_test=True)
|
|
192
|
+
if branches:
|
|
193
|
+
branches_ = branches_.filtered(lambda br: br.name in branches)
|
|
194
|
+
for repo in repositories:
|
|
195
|
+
repo.action_scan(branch_ids=branches_.ids, force=force, raise_exc=False)
|
|
196
|
+
|
|
197
|
+
def _check_config(self):
|
|
198
|
+
# Check the configuration of repositories folder
|
|
199
|
+
key = self._repositories_path_key
|
|
200
|
+
repositories_path = self.env["ir.config_parameter"].sudo().get_param(key, "")
|
|
201
|
+
if not repositories_path:
|
|
202
|
+
raise UserError(
|
|
203
|
+
_(
|
|
204
|
+
"Please define the '{key}' system parameter to "
|
|
205
|
+
"clone repositories in the folder of your choice.".format(key=key)
|
|
206
|
+
)
|
|
207
|
+
)
|
|
208
|
+
# Ensure the folder exists
|
|
209
|
+
pathlib.Path(repositories_path).mkdir(parents=True, exist_ok=True)
|
|
210
|
+
|
|
211
|
+
def _check_existing_jobs(self, raise_exc=True):
|
|
212
|
+
"""Check if a scan is already triggered for this repository."""
|
|
213
|
+
self.ensure_one()
|
|
214
|
+
existing_job = (
|
|
215
|
+
self.env["queue.job"]
|
|
216
|
+
.sudo()
|
|
217
|
+
.search(
|
|
218
|
+
[
|
|
219
|
+
("model_name", "=", self._name),
|
|
220
|
+
("records", "ilike", f'%"ids": [{self.id}]%'),
|
|
221
|
+
(
|
|
222
|
+
"state",
|
|
223
|
+
"in",
|
|
224
|
+
[
|
|
225
|
+
"wait_dependencies",
|
|
226
|
+
"pending",
|
|
227
|
+
"enqueued",
|
|
228
|
+
"started",
|
|
229
|
+
],
|
|
230
|
+
),
|
|
231
|
+
],
|
|
232
|
+
limit=1,
|
|
233
|
+
)
|
|
234
|
+
)
|
|
235
|
+
if existing_job:
|
|
236
|
+
msg = _("A scan is already ongoing for repository %s") % self.display_name
|
|
237
|
+
if raise_exc:
|
|
238
|
+
raise UserError(msg)
|
|
239
|
+
_logger.warning(msg)
|
|
240
|
+
return True
|
|
241
|
+
return False
|
|
242
|
+
|
|
243
|
+
def action_scan(self, branch_ids=None, force=False, raise_exc=True):
|
|
244
|
+
"""Scan the whole repository."""
|
|
245
|
+
self._check_config()
|
|
246
|
+
for rec in self:
|
|
247
|
+
if not rec.to_scan:
|
|
248
|
+
continue
|
|
249
|
+
if rec._check_existing_jobs(raise_exc=raise_exc):
|
|
250
|
+
continue
|
|
251
|
+
# Get branch records to scan
|
|
252
|
+
branches = rec._get_odoo_branches_to_scan()
|
|
253
|
+
if branch_ids:
|
|
254
|
+
branches = branches & self.env["odoo.branch"].search(
|
|
255
|
+
[("id", "in", branch_ids)]
|
|
256
|
+
)
|
|
257
|
+
if not branches:
|
|
258
|
+
continue
|
|
259
|
+
# Create a list of tuples ({odoo_version}, {branch_name})
|
|
260
|
+
versions_branches = [(branch.name, branch.name) for branch in branches]
|
|
261
|
+
if rec.manual_branches:
|
|
262
|
+
versions_branches = [
|
|
263
|
+
(rb.branch_id.name, rb.cloned_branch or rb.branch_id.name)
|
|
264
|
+
for rb in rec.branch_ids
|
|
265
|
+
if rb.branch_id in branches
|
|
266
|
+
]
|
|
267
|
+
if force:
|
|
268
|
+
rec._reset_scanned_commits(branch_ids=branch_ids)
|
|
269
|
+
# Scan repository branches sequentially as they need to be checked out
|
|
270
|
+
# to perform the analysis
|
|
271
|
+
# Here the launched job is responsible to:
|
|
272
|
+
# 1) detect modules updated on the first branch
|
|
273
|
+
# 2) spawn jobs to scan each module on that branch
|
|
274
|
+
# 3) spawn a job to update the last scanned commit of the repo/branch
|
|
275
|
+
# 4) spawn the next job responsible to detect modules updated
|
|
276
|
+
# on the next branch
|
|
277
|
+
version_branch = versions_branches[0]
|
|
278
|
+
next_versions_branches = versions_branches[1:]
|
|
279
|
+
job = rec._create_job_detect_modules_to_scan_on_branch(
|
|
280
|
+
version_branch, next_versions_branches, versions_branches
|
|
281
|
+
)
|
|
282
|
+
job.delay()
|
|
283
|
+
return True
|
|
284
|
+
|
|
285
|
+
def _create_job_detect_modules_to_scan_on_branch(
|
|
286
|
+
self, version_branch, next_versions_branches, all_versions_branches
|
|
287
|
+
):
|
|
288
|
+
self.ensure_one()
|
|
289
|
+
version, branch = version_branch
|
|
290
|
+
branch_str = branch
|
|
291
|
+
if version != branch:
|
|
292
|
+
branch_str = f"{branch} ({version})"
|
|
293
|
+
delayable = self.delayable(
|
|
294
|
+
description=f"Detect modules to scan in {self.display_name}#{branch_str}",
|
|
295
|
+
identity_key=identity_exact,
|
|
296
|
+
)
|
|
297
|
+
return delayable._detect_modules_to_scan_on_branch(
|
|
298
|
+
version_branch, next_versions_branches, all_versions_branches
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
def _detect_modules_to_scan_on_branch(
|
|
302
|
+
self, version_branch, next_versions_branches, all_versions_branches
|
|
303
|
+
):
|
|
304
|
+
"""Detect the modules to scan on `branch`.
|
|
305
|
+
|
|
306
|
+
It will spawn a job for each module to scan, and two other jobs to:
|
|
307
|
+
- update the last scanned commit on the repo/branch
|
|
308
|
+
- scan the next branch (so each branch is scanned in cascade)
|
|
309
|
+
|
|
310
|
+
This ensure to scan different branches sequentially for a given repository.
|
|
311
|
+
"""
|
|
312
|
+
version, branch = version_branch
|
|
313
|
+
try:
|
|
314
|
+
# Get the list of modules updated since last scan
|
|
315
|
+
params = self._prepare_scanner_parameters(version, branch)
|
|
316
|
+
scanner = RepositoryScannerOdooEnv(**params)
|
|
317
|
+
data = scanner.detect_modules_to_scan()
|
|
318
|
+
# Prepare all subsequent jobs based on modules to scan
|
|
319
|
+
jobs = self._create_subsequent_jobs(
|
|
320
|
+
version_branch, next_versions_branches, all_versions_branches, data
|
|
321
|
+
)
|
|
322
|
+
# Chain them altogether
|
|
323
|
+
if jobs:
|
|
324
|
+
chain(*jobs).delay()
|
|
325
|
+
except Exception as exc:
|
|
326
|
+
raise RetryableJobError("Scanner error") from exc
|
|
327
|
+
|
|
328
|
+
def _create_subsequent_jobs(
|
|
329
|
+
self, version_branch, next_versions_branches, all_versions_branches, data
|
|
330
|
+
):
|
|
331
|
+
jobs = []
|
|
332
|
+
version, branch = version_branch
|
|
333
|
+
# Spawn one job per module to scan
|
|
334
|
+
for data_ in data.get("addons_paths", {}).values():
|
|
335
|
+
for module_path in data_["modules_to_scan"]:
|
|
336
|
+
job = self._create_job_scan_module_on_branch(
|
|
337
|
+
version, branch, module_path, data_["specs"]
|
|
338
|
+
)
|
|
339
|
+
jobs.append(job)
|
|
340
|
+
# + another one to update the last scanned commit of the repository
|
|
341
|
+
if data.get("repo_branch_id"):
|
|
342
|
+
job = self._create_job_update_last_scanned_commit(
|
|
343
|
+
data["repo_branch_id"],
|
|
344
|
+
data["last_fetched_commit"],
|
|
345
|
+
)
|
|
346
|
+
jobs.append(job)
|
|
347
|
+
# + another one to detect modules to scan on the next branch
|
|
348
|
+
version_branch = next_versions_branches and next_versions_branches[0]
|
|
349
|
+
next_versions_branches = next_versions_branches[1:]
|
|
350
|
+
if version_branch:
|
|
351
|
+
jobs.append(
|
|
352
|
+
self._create_job_detect_modules_to_scan_on_branch(
|
|
353
|
+
version_branch, next_versions_branches, all_versions_branches
|
|
354
|
+
)
|
|
355
|
+
)
|
|
356
|
+
return jobs
|
|
357
|
+
|
|
358
|
+
def _create_job_scan_module_on_branch(self, version, branch, module_path, specs):
|
|
359
|
+
self.ensure_one()
|
|
360
|
+
branch_str = branch
|
|
361
|
+
if version != branch:
|
|
362
|
+
branch_str = f"{branch} ({version})"
|
|
363
|
+
delayable = self.delayable(
|
|
364
|
+
description=f"Scan {self.display_name}#{branch_str} - {module_path}",
|
|
365
|
+
identity_key=identity_exact,
|
|
366
|
+
)
|
|
367
|
+
return delayable._scan_module_on_branch(version, branch, module_path, specs)
|
|
368
|
+
|
|
369
|
+
def _scan_module_on_branch(self, version, branch, module_path, specs):
|
|
370
|
+
"""Scan `module_path` from `branch`."""
|
|
371
|
+
try:
|
|
372
|
+
params = self._prepare_scanner_parameters(version, branch)
|
|
373
|
+
scanner = RepositoryScannerOdooEnv(**params)
|
|
374
|
+
return scanner.scan_module(module_path, specs)
|
|
375
|
+
except Exception as exc:
|
|
376
|
+
raise RetryableJobError("Scanner error") from exc
|
|
377
|
+
|
|
378
|
+
def _create_job_update_last_scanned_commit(
|
|
379
|
+
self, repo_branch_id, last_scanned_commit, last_scan=False
|
|
380
|
+
):
|
|
381
|
+
self.ensure_one()
|
|
382
|
+
repo_branch_model = self.env["odoo.repository.branch"]
|
|
383
|
+
repo_branch = repo_branch_model.browse(repo_branch_id).exists()
|
|
384
|
+
delayable = repo_branch.delayable(
|
|
385
|
+
description=f"Update last scanned commit of {repo_branch.display_name}",
|
|
386
|
+
identity_key=identity_exact,
|
|
387
|
+
)
|
|
388
|
+
return delayable._update_last_scanned_commit(last_scanned_commit)
|
|
389
|
+
|
|
390
|
+
def _reset_scanned_commits(self, branch_ids=None):
|
|
391
|
+
"""Reset the scanned commits.
|
|
392
|
+
|
|
393
|
+
This will make the next repository scan restarting from the beginning,
|
|
394
|
+
and thus making it slower.
|
|
395
|
+
"""
|
|
396
|
+
self.ensure_one()
|
|
397
|
+
if branch_ids is None:
|
|
398
|
+
branch_ids = self.branch_ids.branch_id.ids
|
|
399
|
+
repo_branches = self.branch_ids.filtered(
|
|
400
|
+
lambda rb: rb.branch_id.id in branch_ids
|
|
401
|
+
)
|
|
402
|
+
repo_branches.write({"last_scanned_commit": False})
|
|
403
|
+
repo_branches.module_ids.sudo().write({"last_scanned_commit": False})
|
|
404
|
+
|
|
405
|
+
def _get_token(self):
|
|
406
|
+
"""Return the first available token found for this repository.
|
|
407
|
+
|
|
408
|
+
It will check the available tokens in this order:
|
|
409
|
+
- specific token linked to this repository
|
|
410
|
+
- default token defined in the global settings
|
|
411
|
+
- token defined through an environment variable
|
|
412
|
+
"""
|
|
413
|
+
self.ensure_one()
|
|
414
|
+
return (
|
|
415
|
+
self.token_id.token
|
|
416
|
+
or self.env.company.config_odoo_repository_default_token_id.token
|
|
417
|
+
or os.environ.get("GITHUB_TOKEN")
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
def _prepare_scanner_parameters(self, version, branch):
|
|
421
|
+
ir_config = self.env["ir.config_parameter"]
|
|
422
|
+
repositories_path = ir_config.sudo().get_param(self._repositories_path_key)
|
|
423
|
+
return {
|
|
424
|
+
"org": self.org_id.name,
|
|
425
|
+
"name": self.name,
|
|
426
|
+
"clone_url": self.clone_url,
|
|
427
|
+
"version": version,
|
|
428
|
+
"branch": branch,
|
|
429
|
+
"addons_paths_data": self.addons_path_ids.read(
|
|
430
|
+
[
|
|
431
|
+
"relative_path",
|
|
432
|
+
"is_standard",
|
|
433
|
+
"is_enterprise",
|
|
434
|
+
"is_community",
|
|
435
|
+
]
|
|
436
|
+
),
|
|
437
|
+
"repositories_path": repositories_path,
|
|
438
|
+
"repo_type": self.repo_type,
|
|
439
|
+
"ssh_key": self.ssh_key_id.private_key,
|
|
440
|
+
"token": self._get_token(),
|
|
441
|
+
"workaround_fs_errors": (
|
|
442
|
+
self.env.company.config_odoo_repository_workaround_fs_errors
|
|
443
|
+
),
|
|
444
|
+
"clone_name": self.clone_name,
|
|
445
|
+
"env": self.env,
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
def action_force_scan(self, branch_ids=None, raise_exc=True):
|
|
449
|
+
"""Force the scan of the repositories.
|
|
450
|
+
|
|
451
|
+
It will restart the scan without considering the last scanned commit,
|
|
452
|
+
overriding already collected module data if any.
|
|
453
|
+
"""
|
|
454
|
+
self.ensure_one()
|
|
455
|
+
return self.action_scan(branch_ids=branch_ids, force=True, raise_exc=raise_exc)
|
|
456
|
+
|
|
457
|
+
@api.model
|
|
458
|
+
def cron_fetch_data(self, branches=None, force=False):
|
|
459
|
+
"""Fetch Odoo repositories data from the main node (if any)."""
|
|
460
|
+
main_node_url = (
|
|
461
|
+
self.env["ir.config_parameter"]
|
|
462
|
+
.sudo()
|
|
463
|
+
.get_param("odoo_repository_main_node_url")
|
|
464
|
+
)
|
|
465
|
+
if not main_node_url:
|
|
466
|
+
return False
|
|
467
|
+
branch_domain = []
|
|
468
|
+
if branches:
|
|
469
|
+
branch_domain.append(("name", "in", branches))
|
|
470
|
+
branches = self.env["odoo.branch"].search(branch_domain)
|
|
471
|
+
branch_names = ",".join(branches.mapped("name"))
|
|
472
|
+
url = f"{main_node_url}?branches=%s" % branch_names
|
|
473
|
+
try:
|
|
474
|
+
response = requests.get(url, timeout=60)
|
|
475
|
+
except Exception as exc:
|
|
476
|
+
raise UserError(_("Unable to fetch data from %s") % main_node_url) from exc
|
|
477
|
+
else:
|
|
478
|
+
if response.status_code == 200:
|
|
479
|
+
try:
|
|
480
|
+
data = json.loads(response.text)
|
|
481
|
+
except json.decoder.JSONDecodeError as exc:
|
|
482
|
+
raise UserError(
|
|
483
|
+
_("Unable to decode data received from %s") % main_node_url
|
|
484
|
+
) from exc
|
|
485
|
+
else:
|
|
486
|
+
self._import_data(data)
|
|
487
|
+
|
|
488
|
+
def _import_data(self, data):
|
|
489
|
+
for module_data in data:
|
|
490
|
+
# TODO Move these methods to 'odoo.module.branch'?
|
|
491
|
+
values = self._prepare_module_branch_values(module_data)
|
|
492
|
+
self._create_or_update_module_branch(values, module_data)
|
|
493
|
+
|
|
494
|
+
def _prepare_module_branch_values(self, data):
|
|
495
|
+
# Get branch, repository and technical module
|
|
496
|
+
branch = self.env["odoo.branch"].search([("name", "=", data["branch"])])
|
|
497
|
+
org = self._get_repository_org(data["repository"]["org"])
|
|
498
|
+
repository = self._get_repository(
|
|
499
|
+
org.id, data["repository"]["name"], data["repository"]
|
|
500
|
+
)
|
|
501
|
+
repository_branch = self._get_repository_branch(
|
|
502
|
+
org.id, repository.id, branch.id, data["repository"]
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
mb_model = self.env["odoo.module.branch"]
|
|
506
|
+
module = mb_model._get_module(data["module"])
|
|
507
|
+
# Prepare values
|
|
508
|
+
category_id = mb_model._get_module_category_id(data["category"])
|
|
509
|
+
author_ids = mb_model._get_author_ids(tuple(data["authors"]))
|
|
510
|
+
maintainer_ids = mb_model._get_maintainer_ids(tuple(data["maintainers"]))
|
|
511
|
+
dev_status_id = mb_model._get_dev_status_id(data["development_status"])
|
|
512
|
+
dependency_ids = mb_model._get_dependency_ids(
|
|
513
|
+
repository_branch, data["depends"]
|
|
514
|
+
)
|
|
515
|
+
external_dependencies = data["external_dependencies"]
|
|
516
|
+
python_dependency_ids = mb_model._get_python_dependency_ids(
|
|
517
|
+
tuple(external_dependencies.get("python", []))
|
|
518
|
+
)
|
|
519
|
+
license_id = mb_model._get_license_id(data["license"])
|
|
520
|
+
versions_values = self._prepare_version_ids_values(
|
|
521
|
+
repository_branch, module, data["versions"]
|
|
522
|
+
)
|
|
523
|
+
values = {
|
|
524
|
+
"repository_branch_id": repository_branch.id,
|
|
525
|
+
"branch_id": repository_branch.branch_id.id,
|
|
526
|
+
"module_id": module.id,
|
|
527
|
+
"title": data["title"],
|
|
528
|
+
"summary": data["summary"],
|
|
529
|
+
"category_id": category_id,
|
|
530
|
+
"author_ids": [(6, 0, author_ids)],
|
|
531
|
+
"maintainer_ids": [(6, 0, maintainer_ids)],
|
|
532
|
+
"dependency_ids": [(6, 0, dependency_ids)],
|
|
533
|
+
"external_dependencies": external_dependencies,
|
|
534
|
+
"python_dependency_ids": [(6, 0, python_dependency_ids)],
|
|
535
|
+
"license_id": license_id,
|
|
536
|
+
"version": data["version"],
|
|
537
|
+
"version_ids": versions_values,
|
|
538
|
+
"development_status_id": dev_status_id,
|
|
539
|
+
"installable": data["installable"],
|
|
540
|
+
"auto_install": data["auto_install"],
|
|
541
|
+
"application": data["application"],
|
|
542
|
+
"is_standard": data["is_standard"],
|
|
543
|
+
"is_enterprise": data["is_enterprise"],
|
|
544
|
+
"is_community": data["is_community"],
|
|
545
|
+
"sloc_python": data["sloc_python"],
|
|
546
|
+
"sloc_xml": data["sloc_xml"],
|
|
547
|
+
"sloc_js": data["sloc_js"],
|
|
548
|
+
"sloc_css": data["sloc_css"],
|
|
549
|
+
"last_scanned_commit": data["last_scanned_commit"],
|
|
550
|
+
"pr_url": data["pr_url"],
|
|
551
|
+
}
|
|
552
|
+
return values
|
|
553
|
+
|
|
554
|
+
def _prepare_version_ids_values(self, repo_branch, module, versions: list[dict]):
|
|
555
|
+
version_ids = []
|
|
556
|
+
for version in versions:
|
|
557
|
+
version_model = self.env["odoo.module.branch.version"]
|
|
558
|
+
rec = version_model.search(
|
|
559
|
+
[
|
|
560
|
+
("module_branch_id.branch_id", "=", repo_branch.branch_id.id),
|
|
561
|
+
("module_branch_id.module_id", "=", module.id),
|
|
562
|
+
("name", "=", version["name"]),
|
|
563
|
+
],
|
|
564
|
+
limit=1,
|
|
565
|
+
)
|
|
566
|
+
if rec:
|
|
567
|
+
version_ids.append(fields.Command.update(rec.id, version))
|
|
568
|
+
else:
|
|
569
|
+
version_ids.append(fields.Command.create(version))
|
|
570
|
+
return version_ids
|
|
571
|
+
|
|
572
|
+
def _create_or_update_module_branch(self, values, raw_data):
|
|
573
|
+
mb_model = self.env["odoo.module.branch"]
|
|
574
|
+
rec = mb_model.search(
|
|
575
|
+
[
|
|
576
|
+
("module_id", "=", values["module_id"]),
|
|
577
|
+
# Module could have been already created to satisfy dependencies
|
|
578
|
+
# (without 'repository_branch_id' set)
|
|
579
|
+
"|",
|
|
580
|
+
("repository_branch_id", "=", values["repository_branch_id"]),
|
|
581
|
+
("branch_id", "=", values["branch_id"]),
|
|
582
|
+
],
|
|
583
|
+
limit=1,
|
|
584
|
+
)
|
|
585
|
+
values = self._pre_create_or_update_module_branch(rec, values, raw_data)
|
|
586
|
+
if rec:
|
|
587
|
+
rec.sudo().write(values)
|
|
588
|
+
else:
|
|
589
|
+
rec = mb_model.sudo().create(values)
|
|
590
|
+
self._post_create_or_update_module_branch(rec, values, raw_data)
|
|
591
|
+
return rec
|
|
592
|
+
|
|
593
|
+
def _pre_create_or_update_module_branch(self, rec, values, raw_data):
|
|
594
|
+
"""Hook executed before the creation or update of `rec`. Return values."""
|
|
595
|
+
return values
|
|
596
|
+
|
|
597
|
+
def _post_create_or_update_module_branch(self, rec, values, raw_data):
|
|
598
|
+
"""Hook executed after the creation or update of `rec`."""
|
|
599
|
+
|
|
600
|
+
@tools.ormcache("name")
|
|
601
|
+
def _get_repository_org(self, name):
|
|
602
|
+
rec = self.env["odoo.repository.org"].search([("name", "=", name)], limit=1)
|
|
603
|
+
if not rec:
|
|
604
|
+
rec = self.env["odoo.repository.org"].sudo().create({"name": name})
|
|
605
|
+
return rec
|
|
606
|
+
|
|
607
|
+
@tools.ormcache("org_id", "name")
|
|
608
|
+
def _get_repository(self, org_id, name, data):
|
|
609
|
+
rec = self.env["odoo.repository"].search(
|
|
610
|
+
[
|
|
611
|
+
("org_id", "=", org_id),
|
|
612
|
+
("name", "=", name),
|
|
613
|
+
],
|
|
614
|
+
limit=1,
|
|
615
|
+
)
|
|
616
|
+
values = {
|
|
617
|
+
"org_id": org_id,
|
|
618
|
+
"name": name,
|
|
619
|
+
"repo_url": data["repo_url"],
|
|
620
|
+
"repo_type": data["repo_type"],
|
|
621
|
+
"active": data["active"],
|
|
622
|
+
}
|
|
623
|
+
if rec:
|
|
624
|
+
rec.sudo().write(values)
|
|
625
|
+
else:
|
|
626
|
+
rec = self.env["odoo.repository"].sudo().create(values)
|
|
627
|
+
return rec
|
|
628
|
+
|
|
629
|
+
@tools.ormcache("org_id", "repository_id", "branch_id")
|
|
630
|
+
def _get_repository_branch(self, org_id, repository_id, branch_id, data):
|
|
631
|
+
rec = self.env["odoo.repository.branch"].search(
|
|
632
|
+
[
|
|
633
|
+
("repository_id", "=", repository_id),
|
|
634
|
+
("branch_id", "=", branch_id),
|
|
635
|
+
],
|
|
636
|
+
limit=1,
|
|
637
|
+
)
|
|
638
|
+
values = {
|
|
639
|
+
"repository_id": repository_id,
|
|
640
|
+
"branch_id": branch_id,
|
|
641
|
+
"last_scanned_commit": data["last_scanned_commit"],
|
|
642
|
+
}
|
|
643
|
+
if rec:
|
|
644
|
+
rec.sudo().write(values)
|
|
645
|
+
else:
|
|
646
|
+
rec = self.env["odoo.repository.branch"].sudo().create(values)
|
|
647
|
+
return rec
|
|
648
|
+
|
|
649
|
+
def _get_resource_url(self, branch, path):
|
|
650
|
+
self.ensure_one()
|
|
651
|
+
# NOTE: GitHub and GitLab supports the same URL pattern
|
|
652
|
+
url = "/".join(["tree", branch, path])
|
|
653
|
+
return urljoin(self.repo_url + "/", url)
|
|
654
|
+
|
|
655
|
+
def unlink(self):
|
|
656
|
+
# There is no deletion on cascade policy by default, but for specific
|
|
657
|
+
# repositories we want to remove specific modules anyway.
|
|
658
|
+
# This will also avoid to raise UNIQUE constraint
|
|
659
|
+
# 'odoo_module_branch_uniq_null(module_id, branch_id)' if module names
|
|
660
|
+
# are shared between repositories.
|
|
661
|
+
for rec in self:
|
|
662
|
+
if rec.specific:
|
|
663
|
+
rec.branch_ids.module_ids.sudo().unlink()
|
|
664
|
+
return super().unlink()
|