Skip to content

Commit eef17e7

Browse files
Merge pull request #247 from GlodoUK/cloc-19.0
[19.0][FEAT] cloc Implementation
2 parents c63eb73 + ddd24cf commit eef17e7

9 files changed

Lines changed: 181 additions & 20 deletions

File tree

glodo_client/__manifest__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"version": "19.0.1.0.0",
88
"depends": ["base"],
99
"external_dependencies": {
10-
"python": ["cryptography"],
10+
"python": ["cryptography", "manifestoo_core"],
1111
},
1212
"license": "Other proprietary",
1313
}

glodo_client/controllers/main.py

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@
1717
from odoo.http import Controller, request, route
1818
from odoo.modules.registry import Registry
1919
from odoo.service.db import list_dbs
20-
from odoo.tools import cloc
2120

21+
from ..utils.cloc import CustomCloc
2222
from ..utils.crypto import (
2323
get_client_config,
2424
glodo_authenticated,
@@ -78,7 +78,7 @@ def info(self, **kwargs):
7878
7979
Response includes:
8080
- Instance-level information (Odoo version, etc.)
81-
- List of databases with CLOC and module info
81+
- List of databases with module info and CLOC breakdown
8282
"""
8383
# payload available via request.glodo_payload if needed
8484

@@ -139,27 +139,22 @@ def _get_database_info(self, db_name: str) -> dict:
139139
for m in modules
140140
]
141141

142+
try:
143+
cl = CustomCloc()
144+
cl.count_env(env)
145+
cloc_data = cl.summary()
146+
except Exception as e:
147+
cloc_data = {"error": str(e)}
148+
142149
db_info = {
143150
"name": db_name,
144151
"user_count": user_count,
145152
"expiration_date": expiration_date or None,
146153
"expiration_reason": expiration_reason or None,
147154
"installed_modules": module_list,
148-
"cloc": {},
155+
"cloc": cloc_data,
149156
}
150157

151-
# Try to get CLOC
152-
try:
153-
cl = cloc.Cloc()
154-
cl.count_customization(env)
155-
156-
db_info["cloc"] = {
157-
"output": cl.report(),
158-
"returncode": cl.code,
159-
}
160-
except Exception as e:
161-
db_info["cloc"] = {"error": str(e)}
162-
163158
return db_info
164159

165160
except Exception as e:

glodo_client/utils/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
from . import cloc
12
from . import crypto

glodo_client/utils/cloc.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import ast
2+
import os
3+
import pathlib
4+
5+
from manifestoo_core.core_addons import is_core_addon
6+
from manifestoo_core.odoo_series import OdooSeries, UnsupportedOdooSeries
7+
8+
from odoo import release
9+
from odoo.modules import Manifest
10+
from odoo.modules.module import MANIFEST_NAMES
11+
from odoo.tools.cloc import DEFAULT_EXCLUDE, MAX_FILE_SIZE, VALID_EXTENSION, Cloc
12+
13+
DEFAULT_EXCLUDE_INCLUDE_TESTS = [
14+
p for p in DEFAULT_EXCLUDE if not p.startswith(("tests/", "static/tests/"))
15+
]
16+
17+
18+
class CustomCloc(Cloc):
19+
def summary(self):
20+
return {
21+
"code": dict(self.code),
22+
"errors": {m: dict(files) for m, files in self.errors.items()},
23+
}
24+
25+
def count_modules(self, env):
26+
try:
27+
major, minor = release.version_info[:2]
28+
series = OdooSeries(f"{major}.{minor}")
29+
except (ValueError, UnsupportedOdooSeries):
30+
series = None
31+
32+
domain = [("state", "=", "installed")]
33+
if env["ir.module.module"]._fields.get("imported"):
34+
domain.append(("imported", "=", False))
35+
module_list = env["ir.module.module"].search(domain).mapped("name")
36+
37+
for module_name in module_list:
38+
if series and is_core_addon(module_name, series):
39+
continue
40+
manifest = Manifest.for_addon(module_name)
41+
if not manifest:
42+
continue
43+
self.count_path(manifest.path)
44+
45+
# Exact copy of odoo.tools.cloc.Cloc.count_path with DEFAULT_EXCLUDE → DEFAULT_EXCLUDE_INCLUDE_TESTS
46+
47+
# fmt: off
48+
# pylint: disable=broad-except,except-pass
49+
# ruff: noqa: E501
50+
def count_path(self, path, exclude=None):
51+
path = path.rstrip('/')
52+
exclude_list = []
53+
for i in MANIFEST_NAMES:
54+
manifest_path = os.path.join(path, i)
55+
try:
56+
with open(manifest_path, 'rb') as manifest:
57+
exclude_list.extend(DEFAULT_EXCLUDE_INCLUDE_TESTS)
58+
d = ast.literal_eval(manifest.read().decode('latin1'))
59+
for j in ['cloc_exclude', 'demo', 'demo_xml']:
60+
exclude_list.extend(d.get(j, []))
61+
break
62+
except Exception:
63+
pass
64+
if not exclude:
65+
exclude = set()
66+
for i in filter(None, exclude_list):
67+
assert '..' not in i, (
68+
f"Invalid exclusion path '{i}': '..' is not allowed. Use a normalized path."
69+
)
70+
exclude.update(str(p) for p in pathlib.Path(path).glob(i))
71+
72+
module_name = os.path.basename(path)
73+
self.book(module_name)
74+
for root, _dirs, files in os.walk(path):
75+
for file_name in files:
76+
file_path = os.path.join(root, file_name)
77+
78+
if file_path in exclude:
79+
continue
80+
81+
ext = os.path.splitext(file_path)[1].lower()
82+
if ext not in VALID_EXTENSION:
83+
continue
84+
85+
if os.path.getsize(file_path) > MAX_FILE_SIZE:
86+
self.book(module_name, file_path, (-1, "Max file size exceeded"))
87+
continue
88+
89+
with open(file_path, 'rb') as f:
90+
# Decode using latin1 to avoid error that may raise by decoding with utf8
91+
# The chars not correctly decoded in latin1 have no impact on how many lines will be counted
92+
content = f.read().decode('latin1')
93+
self.book(module_name, file_path, self.parse(content, ext))
94+
# fmt: on

glodo_server/models/glodo_instance.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,22 @@ class GlodoInstance(models.Model):
105105
readonly=True,
106106
)
107107

108+
cloc_excluded_modules = fields.Char(
109+
string="CLOC Excluded Modules",
110+
help="Comma-separated list of module names (or the literal "
111+
"'odoo/studio') to exclude from CLOC totals for this instance. "
112+
"Applies to every database under the instance.",
113+
)
114+
108115
notes = fields.Text()
109116

117+
def _parse_excluded_modules(self):
118+
self.ensure_one()
119+
raw = self.cloc_excluded_modules
120+
if not raw:
121+
return set()
122+
return {part.strip() for part in raw.split(",") if part.strip()}
123+
110124
@api.depends("database_ids")
111125
def _compute_database_count(self):
112126
for instance in self:
@@ -325,8 +339,8 @@ def action_sync_info(self):
325339
"installed_modules_json": json.dumps(
326340
db_data.get("installed_modules", [])
327341
),
328-
"cloc_output": db_data.get("cloc", {}).get("output", ""),
329342
}
343+
db_vals.update(Database._cloc_vals_from_payload(db_data.get("cloc") or {}))
330344

331345
if existing:
332346
existing.write(db_vals)

glodo_server/models/glodo_instance_database.py

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,9 +109,22 @@ class GlodoInstanceDatabase(models.Model):
109109
compute="_compute_installed_modules_html",
110110
)
111111

112-
cloc_output = fields.Text(
113-
string="CLOC Output",
112+
cloc_data_json = fields.Text(
113+
string="CLOC (JSON)",
114114
readonly=True,
115+
help="Raw CLOC payload as reported by the remote.",
116+
)
117+
118+
cloc_total = fields.Integer(
119+
string="CLOC Total",
120+
compute="_compute_cloc_totals",
121+
store=True,
122+
readonly=True,
123+
help="Lines of code counted in custom-module source trees (including "
124+
"tests/ and static/tests/) plus studio actions, manual compute fields, "
125+
"and imported-module artifacts stored in the database. Excludes core, "
126+
"enterprise, and any modules listed in the instance's CLOC Excluded "
127+
"Modules.",
115128
)
116129

117130
last_user_sync = fields.Datetime(
@@ -196,6 +209,37 @@ def _search_expiration_state(self, operator, value):
196209
)
197210
return Domain.OR(domains)
198211

212+
@api.model
213+
def _cloc_vals_from_payload(self, cloc_payload):
214+
"""Return ``{cloc_data_json: ...}`` from a ``cloc`` payload.
215+
216+
The payload follows the shape emitted by
217+
``glodo_client.utils.cloc.CustomCloc.summary``: a dict with ``code``
218+
(per-module LOC, source + customization fused) and ``errors``. The
219+
total is derived in ``_compute_cloc_totals`` so it picks up changes
220+
to the instance's excluded-modules list without re-syncing.
221+
"""
222+
if not isinstance(cloc_payload, dict) or not cloc_payload:
223+
return {"cloc_data_json": False}
224+
return {"cloc_data_json": json.dumps(cloc_payload, sort_keys=True)}
225+
226+
@api.depends("cloc_data_json", "instance_id.cloc_excluded_modules")
227+
def _compute_cloc_totals(self):
228+
for db in self:
229+
total = 0
230+
if db.cloc_data_json:
231+
try:
232+
payload = json.loads(db.cloc_data_json)
233+
except Exception:
234+
payload = {}
235+
excluded = db.instance_id._parse_excluded_modules()
236+
total = sum(
237+
v
238+
for k, v in (payload.get("code") or {}).items()
239+
if k not in excluded and isinstance(v, int)
240+
)
241+
db.cloc_total = total
242+
199243
@api.depends("installed_modules_json")
200244
def _compute_installed_modules_html(self):
201245
for db in self:

glodo_server/views/glodo_instance_database_views.xml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,10 @@
9696
/>
9797
</page>
9898
<page string="CLOC">
99-
<field name="cloc_output" widget="text" readonly="1" />
99+
<group>
100+
<field name="cloc_total" />
101+
</group>
102+
<field name="cloc_data_json" readonly="1" />
100103
</page>
101104
<page string="Notes">
102105
<field
@@ -129,6 +132,7 @@
129132
optional="hide"
130133
sum="Inactive Users"
131134
/>
135+
<field name="cloc_total" optional="hide" sum="CLOC Total" />
132136
<field name="last_user_sync" />
133137
</list>
134138
</field>

glodo_server/views/glodo_instance_views.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,14 @@
134134
</list>
135135
</field>
136136
</page>
137+
<page string="CLOC">
138+
<group>
139+
<field
140+
name="cloc_excluded_modules"
141+
placeholder="e.g. connector_edi, web_cmd_search"
142+
/>
143+
</group>
144+
</page>
137145
<page string="Notes">
138146
<field
139147
name="notes"

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
# generated from manifests external_dependencies
22
cryptography
3+
manifestoo_core

0 commit comments

Comments
 (0)