You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

442 lines
15KB

  1. # Copyright (C) 2016-Today: Odoo Community Association (OCA)
  2. # Copyright 2020 Tecnativa - Víctor Martínez
  3. # @author: Sylvain LE GAL (https://twitter.com/legalsylvain)
  4. # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
  5. import logging
  6. import os
  7. import shutil
  8. from datetime import datetime
  9. from subprocess import check_output
  10. from odoo import _, addons, api, exceptions, fields, models, tools
  11. from odoo.tools.safe_eval import safe_eval
  12. from .github import _GITHUB_URL
  13. _logger = logging.getLogger(__name__)
  14. try:
  15. from git import Repo
  16. except ImportError:
  17. _logger.debug("Cannot import 'git' python library.")
  18. class GithubRepository(models.Model):
  19. _name = "github.repository.branch"
  20. _inherit = ["abstract.github.model"]
  21. _order = "repository_id, sequence_serie"
  22. _description = "Github Repository Branch"
  23. _github_type = "repository_branches"
  24. _github_login_field = False
  25. _SELECTION_STATE = [
  26. ("to_download", "To Download"),
  27. ("to_analyze", "To Analyze"),
  28. ("analyzed", "Analyzed"),
  29. ]
  30. # Column Section
  31. name = fields.Char(string="Name", readonly=True, index=True)
  32. size = fields.Integer(string="Size (Byte) ", readonly=True)
  33. mb_size = fields.Float(
  34. string="Size (Megabyte)", store=True, compute="_compute_mb_size"
  35. )
  36. complete_name = fields.Char(
  37. string="Complete Name", store=True, compute="_compute_complete_name"
  38. )
  39. repository_id = fields.Many2one(
  40. comodel_name="github.repository",
  41. string="Repository",
  42. required=True,
  43. index=True,
  44. readonly=True,
  45. ondelete="cascade",
  46. )
  47. organization_id = fields.Many2one(
  48. comodel_name="github.organization",
  49. string="Organization",
  50. related="repository_id.organization_id",
  51. store=True,
  52. readonly=True,
  53. )
  54. organization_serie_id = fields.Many2one(
  55. comodel_name="github.organization.serie",
  56. string="Organization Serie",
  57. store=True,
  58. compute="_compute_organization_serie_id",
  59. )
  60. sequence_serie = fields.Integer(
  61. string="Sequence Serie", store=True, related="organization_serie_id.sequence"
  62. )
  63. local_path = fields.Char(string="Local Path", compute="_compute_local_path")
  64. state = fields.Selection(
  65. string="State", selection=_SELECTION_STATE, default="to_download"
  66. )
  67. last_download_date = fields.Datetime(string="Last Download Date")
  68. last_analyze_date = fields.Datetime(string="Last Analyze Date")
  69. coverage_url = fields.Char(
  70. string="Coverage URL", store=True, compute="_compute_coverage"
  71. )
  72. ci_url = fields.Char(string="CI URL", store=True, compute="_compute_ci")
  73. github_url = fields.Char(
  74. string="Github URL", store=True, compute="_compute_github_url"
  75. )
  76. analysis_rule_ids = fields.Many2many(
  77. string="Analysis Rules", comodel_name="github.analysis.rule"
  78. )
  79. analysis_rule_info_ids = fields.One2many(
  80. string="Analysis Rules (info)",
  81. comodel_name="github.repository.branch.rule.info",
  82. inverse_name="repository_branch_id",
  83. )
  84. # Init Section
  85. def __init__(self, env, ids, prefetch_ids):
  86. source_path = self._get_source_path()
  87. if source_path and not os.path.exists(source_path):
  88. try:
  89. os.makedirs(source_path)
  90. except Exception as e:
  91. _logger.error(
  92. _(
  93. "Error when trying to create the main folder %s\n"
  94. " Please check Odoo Access Rights.\n %s"
  95. ),
  96. source_path,
  97. e,
  98. )
  99. if source_path and source_path not in addons.__path__:
  100. addons.__path__.append(source_path)
  101. super().__init__(env, ids, prefetch_ids)
  102. def _get_source_path(self):
  103. return tools.config.get("source_code_local_path", "") or os.environ.get(
  104. "SOURCE_CODE_LOCAL_PATH", ""
  105. )
  106. # Action Section
  107. def button_download_code(self):
  108. return self._download_code()
  109. def button_analyze_code(self):
  110. return self._analyze_code()
  111. @api.model
  112. def cron_download_all(self):
  113. branches = self.search([])
  114. branches._download_code()
  115. return True
  116. @api.model
  117. def cron_analyze_all(self):
  118. branches = self.search([("state", "=", "to_analyze")])
  119. branches._analyze_code()
  120. return True
  121. # Custom
  122. def create_or_update_from_name(self, repository_id, name):
  123. branch = self.search(
  124. [("name", "=", name), ("repository_id", "=", repository_id)]
  125. )
  126. if not branch:
  127. branch = self.create({"name": name, "repository_id": repository_id})
  128. return branch
  129. def _download_code(self):
  130. for branch in self:
  131. if not os.path.exists(branch.local_path):
  132. _logger.info("Cloning new repository into %s ..." % branch.local_path)
  133. # Cloning the repository
  134. try:
  135. os.makedirs(branch.local_path)
  136. except Exception:
  137. raise exceptions.Warning(
  138. _(
  139. "Error when trying to create the folder %s\n"
  140. " Please check Odoo Access Rights."
  141. )
  142. % (branch.local_path)
  143. )
  144. command = ("git clone %s%s/%s.git -b %s %s") % (
  145. _GITHUB_URL,
  146. branch.repository_id.organization_id.github_login,
  147. branch.repository_id.name,
  148. branch.name,
  149. branch.local_path,
  150. )
  151. os.system(command)
  152. branch.write(
  153. {"last_download_date": datetime.today(), "state": "to_analyze"}
  154. )
  155. else:
  156. # Update repository
  157. _logger.info("Pulling existing repository %s ..." % branch.local_path)
  158. try:
  159. res = check_output(
  160. ["git", "pull", "origin", branch.name], cwd=branch.local_path
  161. )
  162. if branch.state == "to_download" or b"up-to-date" not in res:
  163. branch.write(
  164. {
  165. "last_download_date": datetime.today(),
  166. "state": "to_analyze",
  167. }
  168. )
  169. else:
  170. branch.write({"last_download_date": datetime.today()})
  171. except Exception:
  172. # Trying to clean the local folder
  173. _logger.warning(
  174. _(
  175. "Error when updating the branch %s in the local folder"
  176. " %s.\n Deleting the local folder and trying"
  177. " again."
  178. ),
  179. branch.name,
  180. branch.local_path,
  181. )
  182. try:
  183. shutil.rmtree(branch.local_path)
  184. except Exception:
  185. _logger.error(
  186. "Error deleting the branch %s in the local folder "
  187. "%s. You need to check manually what is happening "
  188. "there."
  189. )
  190. else:
  191. branch._download_code()
  192. return True
  193. def _get_analyzable_files(self, existing_folder):
  194. res = []
  195. for root, _dirs, files in os.walk(existing_folder):
  196. if "/.git" not in root:
  197. for fic in files:
  198. if fic != ".gitignore":
  199. res.append(os.path.join(root, fic))
  200. return res
  201. def set_analysis_rule_info(self):
  202. rule_ids = (
  203. self.repository_id.organization_id.analysis_rule_ids
  204. + self.repository_id.analysis_rule_ids
  205. + self.analysis_rule_ids
  206. )
  207. for rule_id in rule_ids:
  208. self._delete_analysis_rule_model_info(rule_id)
  209. for vals in self._prepare_analysis_rule_info_vals(rule_id):
  210. self.env[self._prepare_analysis_rule_model_info(rule_id)].create(vals)
  211. def analyze_code_one(self):
  212. """Overload Me in custom Module that manage Source Code analysis.
  213. """
  214. self.ensure_one()
  215. path = self.local_path
  216. self.set_analysis_rule_info()
  217. # Compute Files Sizes
  218. size = 0
  219. for file_path in self._get_analyzable_files(path):
  220. try:
  221. size += os.path.getsize(file_path)
  222. except Exception:
  223. _logger.warning("Warning : unable to eval the size of '%s'.", file_path)
  224. try:
  225. Repo(path)
  226. except Exception:
  227. # If it's not a correct repository, we flag the branch
  228. # to be downloaded again
  229. self.state = "to_download"
  230. return {"size": 0}
  231. return {"size": size}
  232. def _analyze_code(self):
  233. partial_commit = safe_eval(
  234. self.sudo()
  235. .env["ir.config_parameter"]
  236. .get_param("git.partial_commit_during_analysis")
  237. )
  238. for branch in self:
  239. path = branch.local_path
  240. if not os.path.exists(path):
  241. _logger.warning("Warning Folder %s not found: Analysis skipped.", path)
  242. else:
  243. _logger.info("Analyzing Source Code in %s ...", path)
  244. try:
  245. vals = branch.analyze_code_one()
  246. vals.update(
  247. {"last_analyze_date": datetime.today(), "state": "analyzed"}
  248. )
  249. # Mark the branch as analyzed
  250. branch.write(vals)
  251. if partial_commit:
  252. self._cr.commit() # pylint: disable=invalid-commit
  253. except Exception as e:
  254. _logger.warning(
  255. "Cannot analyze branch %s so skipping it, error " "is: %s",
  256. branch.name,
  257. e,
  258. )
  259. return True
  260. def _prepare_analysis_rule_model_info(self, analysis_rule_id):
  261. """Define model data info that override with other addons"""
  262. return "github.repository.branch.rule.info"
  263. def _delete_analysis_rule_model_info(self, analysis_rule_id):
  264. """Remove existing info data to create new records"""
  265. return (
  266. self.env[self._prepare_analysis_rule_model_info(analysis_rule_id)]
  267. .search(
  268. [
  269. ("analysis_rule_id", "=", analysis_rule_id.id),
  270. ("repository_branch_id", "=", self.id),
  271. ]
  272. )
  273. .sudo()
  274. .unlink()
  275. )
  276. def _prepare_analysis_rule_info_vals(self, analysis_rule_id):
  277. """Prepare info vals"""
  278. res = self._operation_analysis_rule_id(analysis_rule_id)
  279. return [
  280. {
  281. "analysis_rule_id": analysis_rule_id.id,
  282. "repository_branch_id": self.id,
  283. "code_count": res["code"],
  284. "documentation_count": res["documentation"],
  285. "empty_count": res["empty"],
  286. "string_count": res["string"],
  287. "scanned_files": len(res["paths"]),
  288. }
  289. ]
  290. def _operation_analysis_rule_id(self, analysis_rule_id):
  291. """This function allow to override with other addons that need
  292. to change this analysis
  293. """
  294. res = {
  295. "paths": [],
  296. "code": 0,
  297. "documentation": 0,
  298. "empty": 0,
  299. "string": 0,
  300. }
  301. for match in analysis_rule_id._get_matches(self.local_path):
  302. res_file = analysis_rule_id._analysis_file(self.local_path + "/" + match)
  303. res["paths"].append(res_file["path"])
  304. # define values
  305. for key in ("code", "documentation", "empty", "string"):
  306. res[key] += res_file[key]
  307. return res
  308. # Compute Section
  309. @api.depends("name", "repository_id.name")
  310. def _compute_complete_name(self):
  311. for branch in self:
  312. branch.complete_name = branch.repository_id.name + "/" + branch.name
  313. @api.depends("size")
  314. def _compute_mb_size(self):
  315. for branch in self:
  316. branch.mb_size = float(branch.size) / (1024 ** 2)
  317. @api.depends("organization_id", "name")
  318. def _compute_organization_serie_id(self):
  319. for branch in self:
  320. for serie in branch.organization_id.organization_serie_ids:
  321. if serie.name == branch.name:
  322. branch.organization_serie_id = serie
  323. @api.depends("complete_name")
  324. def _compute_local_path(self):
  325. source_path = self._get_source_path()
  326. if not source_path and not tools.config["test_enable"]:
  327. raise exceptions.Warning(
  328. _(
  329. "source_code_local_path should be defined in your "
  330. " configuration file"
  331. )
  332. )
  333. for branch in self:
  334. branch.local_path = os.path.join(
  335. source_path, branch.organization_id.github_login, branch.complete_name
  336. )
  337. @api.depends(
  338. "name",
  339. "repository_id.name",
  340. "organization_id.github_login",
  341. "organization_id.coverage_url_pattern",
  342. )
  343. def _compute_coverage(self):
  344. for branch in self:
  345. if not branch.organization_id.coverage_url_pattern:
  346. branch.coverage_url = ""
  347. else:
  348. # This is done because if not, black format the line in a wrong
  349. # way
  350. org_id = branch.organization_id
  351. branch.coverage_url = org_id.coverage_url_pattern.format(
  352. organization_name=org_id.github_login,
  353. repository_name=branch.repository_id.name,
  354. branch_name=branch.name,
  355. )
  356. @api.depends(
  357. "name",
  358. "repository_id.name",
  359. "organization_id.github_login",
  360. "organization_id.ci_url_pattern",
  361. )
  362. def _compute_ci(self):
  363. for branch in self:
  364. if not branch.organization_id.ci_url_pattern:
  365. branch.ci_url = ""
  366. continue
  367. branch.ci_url = branch.organization_id.ci_url_pattern.format(
  368. organization_name=branch.organization_id.github_login,
  369. repository_name=branch.repository_id.name,
  370. branch_name=branch.name,
  371. )
  372. @api.depends("name", "repository_id.complete_name")
  373. def _compute_github_url(self):
  374. for branch in self:
  375. branch.github_url = "https://github.com/{}/{}/tree/{}".format(
  376. branch.organization_id.github_login,
  377. branch.repository_id.name,
  378. branch.name,
  379. )
  380. class GithubRepositoryBranchRuleInfo(models.TransientModel):
  381. _inherit = "github.analysis.rule.info.mixin"
  382. _name = "github.repository.branch.rule.info"
  383. _description = " Github Repository Branch Rule Info"
  384. repository_branch_id = fields.Many2one(
  385. string="Repository Branch",
  386. comodel_name="github.repository.branch",
  387. ondelete="cascade",
  388. )
上海开阖软件有限公司 沪ICP备12045867号-1