Le traitement de `add_root` est en FIN de fonction, et deux retours anticipés lui sautaient par-dessus : sans manifeste, la liste revenait vide alors que la racine était demandée. Le script qui réécrit les remotes ne trouvait donc rien à faire sur un checkout sans `.repo`, et l'annonçait comme un succès. L'ajout de la racine et le tri passent par un point de sortie unique, ce qui supprime la copie qui gardait le défaut dans une variante sur deux. Une origine absente vaut l'URL par défaut plutôt qu'une exception. Vérifié dans un dépôt jetable, dans les deux sens et sans origine ; 4 tests neufs, qui échouent si le retour anticipé revient. --- EN --- The `add_root` handling sits at the END of the function, and two early returns jumped over it: with no manifest, the list came back empty although the root had been asked for. The script that rewrites the remotes therefore found nothing to do on a checkout without `.repo`, and reported it as a success. Adding the root and sorting now go through a single exit point, which drops the copy that kept the fault in one variant out of two. A missing origin falls back to the default URL rather than raising. Checked in a throwaway repository, both ways and with no origin; 4 new tests, which fail if the early return comes back. Assisted-by: Claude Opus 5
404 lines
14 KiB
Python
404 lines
14 KiB
Python
#!/usr/bin/env python3
|
|
# © 2026 TechnoLibre (http://www.technolibre.ca)
|
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
|
|
|
import io
|
|
import os
|
|
import subprocess
|
|
import tempfile
|
|
import unittest
|
|
from collections import OrderedDict
|
|
from contextlib import redirect_stdout
|
|
from unittest.mock import MagicMock, mock_open, patch
|
|
|
|
from script.git.git_tool import (
|
|
DEFAULT_PROJECT_NAME,
|
|
DEFAULT_REMOTE_URL,
|
|
DEFAULT_WEBSITE,
|
|
EL_GITHUB_TOKEN,
|
|
SOURCE_REPO_ADDONS_FILE,
|
|
GitTool,
|
|
RepoAttrs,
|
|
)
|
|
|
|
|
|
class TestRepoAttrs(unittest.TestCase):
|
|
def test_basic_attributes(self):
|
|
s = RepoAttrs(a=1, b="hello")
|
|
self.assertEqual(s.a, 1)
|
|
self.assertEqual(s.b, "hello")
|
|
|
|
def test_empty_struct(self):
|
|
s = RepoAttrs()
|
|
self.assertEqual(s.__dict__, {})
|
|
|
|
def test_override_existing(self):
|
|
s = RepoAttrs(x=10)
|
|
self.assertEqual(s.x, 10)
|
|
|
|
|
|
class TestGetUrl(unittest.TestCase):
|
|
def test_https_to_git(self):
|
|
url, url_https, url_git = GitTool.get_url(
|
|
"https://github.com/OCA/server-tools.git"
|
|
)
|
|
self.assertEqual(url, "https://github.com/OCA/server-tools.git")
|
|
self.assertEqual(url_https, "https://github.com/OCA/server-tools.git")
|
|
self.assertEqual(url_git, "git@github.com:OCA/server-tools.git")
|
|
|
|
def test_git_to_https(self):
|
|
url, url_https, url_git = GitTool.get_url(
|
|
"git@github.com:OCA/server-tools.git"
|
|
)
|
|
self.assertEqual(url_https, "https://github.com/OCA/server-tools.git")
|
|
self.assertEqual(url_git, "git@github.com:OCA/server-tools.git")
|
|
|
|
def test_https_without_git_suffix(self):
|
|
url, url_https, url_git = GitTool.get_url(
|
|
"https://github.com/ERPLibre/ERPLibre"
|
|
)
|
|
self.assertEqual(url_https, "https://github.com/ERPLibre/ERPLibre")
|
|
self.assertEqual(url_git, "git@github.com:ERPLibre/ERPLibre")
|
|
|
|
|
|
class TestGetTransformedRepoInfo(unittest.TestCase):
|
|
def setUp(self):
|
|
self.gt = GitTool()
|
|
|
|
def test_https_url_as_submodule(self):
|
|
result = self.gt.get_transformed_repo_info_from_url(
|
|
"https://github.com/OCA/server-tools.git",
|
|
repo_path=".",
|
|
get_obj=False,
|
|
)
|
|
self.assertEqual(result["organization"], "OCA")
|
|
self.assertEqual(result["repo_name"], "server-tools")
|
|
self.assertEqual(result["project_name"], "server-tools.git")
|
|
self.assertEqual(result["path"], "addons/OCA_server-tools")
|
|
self.assertTrue(result["is_submodule"])
|
|
|
|
def test_git_url_as_submodule(self):
|
|
result = self.gt.get_transformed_repo_info_from_url(
|
|
"git@github.com:ERPLibre/erplibre_addons.git",
|
|
repo_path=".",
|
|
get_obj=False,
|
|
)
|
|
self.assertEqual(result["organization"], "ERPLibre")
|
|
self.assertEqual(result["repo_name"], "erplibre_addons")
|
|
|
|
def test_not_submodule(self):
|
|
result = self.gt.get_transformed_repo_info_from_url(
|
|
"https://github.com/OCA/server-tools.git",
|
|
repo_path="/tmp/test",
|
|
get_obj=False,
|
|
is_submodule=False,
|
|
)
|
|
self.assertEqual(result["path"], "/tmp/test")
|
|
self.assertFalse(result["is_submodule"])
|
|
|
|
def test_get_obj_true_returns_struct(self):
|
|
result = self.gt.get_transformed_repo_info_from_url(
|
|
"https://github.com/OCA/web.git",
|
|
get_obj=True,
|
|
)
|
|
self.assertIsInstance(result, RepoAttrs)
|
|
self.assertEqual(result.organization, "OCA")
|
|
|
|
def test_organization_force(self):
|
|
result = self.gt.get_transformed_repo_info_from_url(
|
|
"https://github.com/OCA/server-tools.git",
|
|
get_obj=False,
|
|
organization_force="MyOrg",
|
|
)
|
|
self.assertEqual(result["organization"], "MyOrg")
|
|
self.assertEqual(result["original_organization"], "OCA")
|
|
self.assertIn("MyOrg", result["url_https"])
|
|
|
|
def test_custom_sub_path(self):
|
|
result = self.gt.get_transformed_repo_info_from_url(
|
|
"https://github.com/OCA/server-tools.git",
|
|
get_obj=False,
|
|
sub_path="custom",
|
|
)
|
|
self.assertEqual(result["path"], "custom/OCA_server-tools")
|
|
|
|
def test_empty_sub_path(self):
|
|
result = self.gt.get_transformed_repo_info_from_url(
|
|
"https://github.com/OCA/server-tools.git",
|
|
get_obj=False,
|
|
sub_path="",
|
|
)
|
|
self.assertEqual(result["path"], "server-tools")
|
|
|
|
def test_dot_sub_path(self):
|
|
result = self.gt.get_transformed_repo_info_from_url(
|
|
"https://github.com/OCA/server-tools.git",
|
|
get_obj=False,
|
|
sub_path=".",
|
|
)
|
|
self.assertEqual(result["path"], "server-tools")
|
|
|
|
def test_revision_and_clone_depth(self):
|
|
result = self.gt.get_transformed_repo_info_from_url(
|
|
"https://github.com/OCA/web.git",
|
|
get_obj=False,
|
|
revision="16.0",
|
|
clone_depth="1",
|
|
)
|
|
self.assertEqual(result["revision"], "16.0")
|
|
self.assertEqual(result["clone_depth"], "1")
|
|
|
|
def test_url_without_git_suffix(self):
|
|
result = self.gt.get_transformed_repo_info_from_url(
|
|
"https://github.com/OCA/server-tools",
|
|
get_obj=False,
|
|
)
|
|
self.assertEqual(result["repo_name"], "server-tools")
|
|
|
|
|
|
class TestDefaultProperties(unittest.TestCase):
|
|
def test_default_project_name(self):
|
|
gt = GitTool()
|
|
self.assertEqual(gt.default_project_name, DEFAULT_PROJECT_NAME)
|
|
|
|
def test_default_website(self):
|
|
gt = GitTool()
|
|
self.assertEqual(gt.default_website, DEFAULT_WEBSITE)
|
|
|
|
def test_default_remote_url(self):
|
|
gt = GitTool()
|
|
self.assertEqual(gt.default_remote_url, DEFAULT_REMOTE_URL)
|
|
|
|
@patch(
|
|
"builtins.open",
|
|
mock_open(read_data="18.0"),
|
|
)
|
|
def test_odoo_version(self):
|
|
gt = GitTool()
|
|
self.assertEqual(gt.odoo_version, "18.0")
|
|
|
|
@patch(
|
|
"builtins.open",
|
|
mock_open(read_data="16.0"),
|
|
)
|
|
def test_odoo_version_long(self):
|
|
gt = GitTool()
|
|
self.assertEqual(gt.odoo_version_long, "odoo16.0")
|
|
|
|
|
|
class TestStrInsert(unittest.TestCase):
|
|
def test_insert_middle(self):
|
|
result = GitTool.str_insert("abcdef", "XY", 3)
|
|
self.assertEqual(result, "abcXYdef")
|
|
|
|
def test_insert_beginning(self):
|
|
result = GitTool.str_insert("hello", "X", 0)
|
|
self.assertEqual(result, "Xhello")
|
|
|
|
def test_insert_end(self):
|
|
result = GitTool.str_insert("hello", "X", 5)
|
|
self.assertEqual(result, "helloX")
|
|
|
|
|
|
class TestGetProjectConfig(unittest.TestCase):
|
|
def test_reads_github_token(self):
|
|
content = (
|
|
"#!/bin/bash\n"
|
|
'EL_GITHUB_TOKEN="my_token_123"\n'
|
|
'OTHER_VAR="value"\n'
|
|
)
|
|
with tempfile.NamedTemporaryFile(
|
|
mode="w", suffix=".sh", delete=False, dir="/tmp"
|
|
) as f:
|
|
f.write(content)
|
|
f.flush()
|
|
tmpdir = os.path.dirname(f.name)
|
|
tmpname = os.path.basename(f.name)
|
|
try:
|
|
# We need env_var.sh in a directory
|
|
env_var_path = os.path.join(tmpdir, "env_var.sh")
|
|
os.rename(f.name, env_var_path)
|
|
result = GitTool.get_project_config(repo_path=tmpdir)
|
|
self.assertEqual(result[EL_GITHUB_TOKEN], "my_token_123")
|
|
finally:
|
|
if os.path.exists(env_var_path):
|
|
os.unlink(env_var_path)
|
|
|
|
|
|
class TestGetRepoInfoSubmodule(unittest.TestCase):
|
|
def test_parses_gitmodules(self):
|
|
gitmodules_content = (
|
|
'[submodule "addons/OCA_server-tools"]\n'
|
|
"\turl = https://github.com/OCA/server-tools.git\n"
|
|
"\tpath = addons/OCA_server-tools\n"
|
|
"\n"
|
|
'[submodule "addons/OCA_web"]\n'
|
|
"\turl = https://github.com/OCA/web.git\n"
|
|
"\tpath = addons/OCA_web\n"
|
|
)
|
|
gt = GitTool()
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
gitmodules_path = os.path.join(tmpdir, ".gitmodules")
|
|
with open(gitmodules_path, "w") as f:
|
|
f.write(gitmodules_content)
|
|
|
|
result = gt.get_repo_info_submodule(
|
|
repo_path=tmpdir, add_root=False
|
|
)
|
|
self.assertEqual(len(result), 2)
|
|
names = [r["name"] for r in result]
|
|
self.assertIn("addons/OCA_server-tools", names)
|
|
self.assertIn("addons/OCA_web", names)
|
|
|
|
def test_single_submodule(self):
|
|
gitmodules_content = (
|
|
'[submodule "addons/test"]\n'
|
|
"\turl = https://github.com/Test/repo.git\n"
|
|
"\tpath = addons/test\n"
|
|
)
|
|
gt = GitTool()
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
with open(os.path.join(tmpdir, ".gitmodules"), "w") as f:
|
|
f.write(gitmodules_content)
|
|
|
|
result = gt.get_repo_info_submodule(repo_path=tmpdir)
|
|
self.assertEqual(len(result), 1)
|
|
self.assertEqual(result[0]["name"], "addons/test")
|
|
self.assertIn("https://", result[0]["url_https"])
|
|
self.assertIn("git@", result[0]["url_git"])
|
|
|
|
|
|
class TestTheRootRepoSurvivesAMissingManifest(unittest.TestCase):
|
|
"""Le dépôt racine ne dépend ni du manifeste ni des sous-modules.
|
|
|
|
Une liste vide là où la racine était demandée fait conclure à
|
|
l'appelant qu'il n'a rien à faire : sur un checkout sans `.repo`, le
|
|
remote du dépôt principal n'était jamais réécrit, sans message.
|
|
"""
|
|
|
|
def _depot(self, chemin, origine="https://github.com/ERPLibre/ERPLibre"):
|
|
"""Un vrai dépôt git, sans manifeste. Rien n'est bouchonné : la
|
|
lecture de l'origine passe par git, et c'est elle qu'on vérifie."""
|
|
subprocess.run(["git", "init", "-q", chemin], check=True)
|
|
if origine:
|
|
subprocess.run(
|
|
["git", "-C", chemin, "remote", "add", "origin", origine],
|
|
check=True,
|
|
)
|
|
return chemin
|
|
|
|
def test_without_a_manifest_the_root_is_still_returned(self):
|
|
gt = GitTool()
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
self._depot(tmpdir)
|
|
result = gt.get_repo_info(tmpdir, add_root=True)
|
|
self.assertEqual(len(result), 1)
|
|
self.assertEqual(result[0]["name"], "")
|
|
self.assertEqual(
|
|
result[0]["url_git"], "git@github.com:ERPLibre/ERPLibre"
|
|
)
|
|
|
|
def test_without_the_root_asked_nothing_is_returned(self):
|
|
"""Deux appelants internes lisent le manifeste SANS la racine : le
|
|
correctif ne doit rien rendre à qui n'en veut pas."""
|
|
gt = GitTool()
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
self._depot(tmpdir)
|
|
self.assertEqual(gt.get_repo_info(tmpdir), [])
|
|
|
|
def test_a_root_without_origin_falls_back_instead_of_raising(self):
|
|
"""La liste sert à RÉÉCRIRE les remotes : un dépôt sans origine est
|
|
précisément un de ceux qu'on vient corriger."""
|
|
gt = GitTool()
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
self._depot(tmpdir, origine="")
|
|
with redirect_stdout(io.StringIO()):
|
|
result = gt.get_repo_info(tmpdir, add_root=True)
|
|
self.assertEqual(len(result), 1)
|
|
self.assertTrue(result[0]["url_git"].startswith("git@"))
|
|
|
|
def test_the_root_comes_first_alongside_the_manifest_projects(self):
|
|
"""La racine porte un nom vide : le tri la met en tête, et le
|
|
script qui réécrit les remotes commence donc par elle."""
|
|
xml_content = (
|
|
'<?xml version="1.0" encoding="UTF-8"?>\n'
|
|
"<manifest>\n"
|
|
' <remote name="OCA" fetch="https://github.com/OCA/"/>\n'
|
|
' <default remote="OCA" revision="16.0"/>\n'
|
|
' <project name="web.git" path="addons/OCA_web"'
|
|
' remote="OCA" groups="odoo16.0"/>\n'
|
|
"</manifest>\n"
|
|
)
|
|
gt = GitTool()
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
self._depot(tmpdir)
|
|
manifeste = os.path.join(tmpdir, "manifest_test.xml")
|
|
with open(manifeste, "w") as fh:
|
|
fh.write(xml_content)
|
|
with patch.object(
|
|
GitTool, "get_manifest_file", return_value=manifeste
|
|
):
|
|
result = gt.get_repo_info(tmpdir, add_root=True)
|
|
self.assertEqual([r["name"] for r in result], ["", "addons/OCA_web"])
|
|
|
|
|
|
class TestGetManifestXmlInfo(unittest.TestCase):
|
|
def test_parses_manifest(self):
|
|
xml_content = """<?xml version="1.0" encoding="UTF-8"?>
|
|
<manifest>
|
|
<remote name="OCA" fetch="https://github.com/OCA/"/>
|
|
<default remote="OCA" revision="16.0"/>
|
|
<project name="server-tools.git" path="addons/OCA_server-tools"
|
|
remote="OCA" groups="odoo16.0"/>
|
|
</manifest>
|
|
"""
|
|
gt = GitTool()
|
|
with tempfile.NamedTemporaryFile(
|
|
mode="w", suffix=".xml", delete=False
|
|
) as f:
|
|
f.write(xml_content)
|
|
f.flush()
|
|
try:
|
|
remotes, projects, default_remote = gt.get_manifest_xml_info(
|
|
filename=f.name
|
|
)
|
|
self.assertIn("OCA", remotes)
|
|
self.assertIn("server-tools.git", projects)
|
|
self.assertEqual(default_remote["@remote"], "OCA")
|
|
finally:
|
|
os.unlink(f.name)
|
|
|
|
def test_empty_manifest(self):
|
|
xml_content = """<?xml version="1.0" encoding="UTF-8"?>
|
|
<manifest/>
|
|
"""
|
|
gt = GitTool()
|
|
with tempfile.NamedTemporaryFile(
|
|
mode="w", suffix=".xml", delete=False
|
|
) as f:
|
|
f.write(xml_content)
|
|
f.flush()
|
|
try:
|
|
remotes, projects, default_remote = gt.get_manifest_xml_info(
|
|
filename=f.name
|
|
)
|
|
self.assertEqual(remotes, {})
|
|
self.assertEqual(projects, {})
|
|
self.assertIsNone(default_remote)
|
|
finally:
|
|
os.unlink(f.name)
|
|
|
|
|
|
class TestConstants(unittest.TestCase):
|
|
def test_file_source_repo_addons(self):
|
|
self.assertEqual(SOURCE_REPO_ADDONS_FILE, "source_repo_addons.csv")
|
|
|
|
def test_default_project_name(self):
|
|
self.assertEqual(DEFAULT_PROJECT_NAME, "ERPLibre")
|
|
|
|
def test_default_website(self):
|
|
self.assertEqual(DEFAULT_WEBSITE, "erplibre.ca")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|