Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion odoo_project_migration/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
{
"name": "Odoo Project Migration Data",
"summary": "Analyze your Odoo project migrations.",
"version": "16.0.1.0.0",
"version": "16.0.1.1.0",
"category": "Tools",
"author": "Camptocamp, Odoo Community Association (OCA)",
"website": "https://github.com/camptocamp/odoo-repository",
Expand Down
38 changes: 38 additions & 0 deletions odoo_project_migration/migrations/16.0.1.1.0/post-migration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Copyright 2025 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)

import logging

from odoo import SUPERUSER_ID, api

_logger = logging.getLogger(__name__)


def migrate(cr, version):
if not version:
return
env = api.Environment(cr, SUPERUSER_ID, {})
fix_migration_states(env)


def fix_migration_states(env):
_logger.info("Update '<odoo.project.module.migration>.state' field...")
query = """
SELECT mig.id
FROM odoo_module_branch_migration mig
JOIN odoo_module_branch AS source
ON mig.module_branch_id=source.id
JOIN odoo_module_branch AS target
ON mig.target_module_branch_id=target.id
WHERE source.repository_branch_id IS NOT NULL
AND target.repository_branch_id IS NOT NULL
AND source.repository_id != target.repository_id;
"""
env.cr.execute(query)
mig_ids = [row[0] for row in env.cr.fetchall()]
# Recompute migration state, especially for modules moved to another repo
project_migs = env["odoo.project.module.migration"].search(
[("module_migration_id", "in", mig_ids)]
)
env.add_to_compute(project_migs._fields["state"], project_migs)
project_migs.modified(["state"])
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,9 @@ class OdooProjectModuleMigration(models.Model):
("migrate", "To migrate"),
("port_commits", "Commits to port"),
("review_migration", "Migration to review"),
("moved_to_standard", "Moved to standard"),
("moved_to_oca", "Moved to OCA"),
("moved_to_generic", "Moved to generic repo"),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved_to_standard does it exists ?

May be just moved is enough ?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, module l10n_eu_oss (while it's not really a move, more Odoo that creates its own, and OCA needed to rename its own with suffix _oca), same for knowledge module, Odoo creates its own enterprise module starting from 16.0, forcing OCA to rename it document_knowledge.

So I agree it's not really a "move" functionally speaking, it's the technical module name that has been moved only, but that highlight something we should take care during a migration.

# New states to qualify modules without migration data
("available", "Available"),
("removed", "Removed"),
Expand Down
77 changes: 57 additions & 20 deletions odoo_repository/lib/scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ def sync(self, fetch=True):
if self.is_cloned:
with self.repo() as repo:
self._apply_git_config(repo)
self._set_git_remote_url(repo)
self._set_git_remote_url(repo, "origin", self.clone_url)
if fetch:
res = self._fetch(repo)
return res
Expand Down Expand Up @@ -182,13 +182,16 @@ def _apply_git_config(self, repo):
writer.set_value("gc", "reflogExpire", "never")
writer.set_value("gc", "reflogExpireUnreachable", "never")

def _set_git_remote_url(self, repo):
"""Ensure that 'origin' remote is set with the right URL."""
def _set_git_remote_url(self, repo, remote, url):
"""Ensure that `remote` has `url` set."""
# Check first the URL before setting it, as this triggers a 'chmod'
# command on '.git/config' file (to protect sensitive data) that could
# be not allowed on some mounted file systems.
if repo.remotes["origin"].url != self.clone_url:
repo.remotes["origin"].set_url(self.clone_url)
if remote in repo.remotes:
if repo.remotes[remote].url != url:
repo.remotes[remote].set_url(url)
else:
repo.create_remote(remote, url)

@property
def is_cloned(self):
Expand Down Expand Up @@ -278,32 +281,32 @@ def _fetch(self, repo):
# Return True as soon as we fetched at least one branch
return bool(branches_fetched)

def _branch_exists(self, repo, branch):
refs = [r.name for r in repo.remotes.origin.refs]
branch = f"origin/{branch}"
def _branch_exists(self, repo, branch, remote="origin"):
refs = [r.name for r in repo.remotes[remote].refs]
branch = f"{remote}/{branch}"
return branch in refs

def _checkout_branch(self, repo, branch):
def _checkout_branch(self, repo, branch, remote="origin"):
# Ensure to clean up the repository before a checkout
index_lock_path = pathlib.Path(repo.common_dir).joinpath("index.lock")
if index_lock_path.exists():
index_lock_path.unlink()
repo.git.reset("--hard")
repo.git.clean("-xdf")
repo.git.checkout("-f", f"remotes/origin/{branch}")
repo.git.checkout("-f", f"remotes/{remote}/{branch}")

def _get_last_fetched_commit(self, repo, branch):
def _get_last_fetched_commit(self, repo, branch, remote="origin"):
"""Return the last fetched commit for the given `branch`."""
return repo.rev_parse(f"remotes/origin/{branch}").hexsha
return repo.rev_parse(f"remotes/{remote}/{branch}").hexsha

def _get_module_paths(self, repo, relative_path, branch):
def _get_module_paths(self, repo, relative_path, branch, remote="origin"):
"""Return the list of modules available in `branch`."""
# Clean up 'relative_path' to make it compatible with 'git.Tree' object
relative_tree_path = "/".join(
[dir_ for dir_ in relative_path.split("/") if dir_ and dir_ != "."]
)
# Return all available modules from 'relative_tree_path'
branch_commit = repo.remotes.origin.refs[branch].commit
branch_commit = repo.remotes[remote].refs[branch].commit
addons_trees = branch_commit.tree.trees
if relative_tree_path:
addons_trees = (branch_commit.tree / relative_tree_path).trees
Expand Down Expand Up @@ -419,6 +422,8 @@ def __init__(
name: str,
clone_url: str,
migration_path: tuple[str],
new_repo_name: str = None,
new_repo_url: str = None,
repositories_path: str = None,
repo_type: str = None,
ssh_key: str = None,
Expand All @@ -440,6 +445,20 @@ def __init__(
clone_name,
)
self.migration_path = migration_path
self.new_repo_name = new_repo_name
self.new_repo_url = (
self._prepare_clone_url(repo_type, new_repo_url, token)
if new_repo_url
else None
)

def sync(self, fetch=True):
res = super().sync(fetch=fetch)
# Set the new repository as remote
if self.is_cloned and self.new_repo_name and self.new_repo_url:
with self.repo() as repo:
self._set_git_remote_url(repo, self.new_repo_name, self.new_repo_url)
return res

def scan(self, addons_path=".", module_names=None):
# Clone/fetch has been done during the repository scan, the migration
Expand All @@ -450,24 +469,40 @@ def scan(self, addons_path=".", module_names=None):
if not res:
return False
source_branch, target_branch = self.migration_path
target_remote = "origin"
with self.repo() as repo:
if self.new_repo_name and self.new_repo_url:
target_remote = self.new_repo_name
# Fetch target branch from new repo
with self._get_git_env() as git_env:
with repo.git.custom_environment(**git_env):
repo.remotes[target_remote].fetch(target_branch)
if self._branch_exists(repo, source_branch) and self._branch_exists(
repo, target_branch
repo, target_branch, remote=target_remote
):
return self._scan_migration_path(
repo,
source_branch,
target_remote,
target_branch,
addons_path=addons_path,
module_names=module_names,
)
return res

def _scan_migration_path(
self, repo, source_branch, target_branch, addons_path=".", module_names=None
self,
repo,
source_branch,
target_remote,
target_branch,
addons_path=".",
module_names=None,
):
repo_source_commit = self._get_last_fetched_commit(repo, source_branch)
repo_target_commit = self._get_last_fetched_commit(repo, target_branch)
repo_target_commit = self._get_last_fetched_commit(
repo, target_branch, remote=target_remote
)
if not module_names:
module_names = self._get_module_paths(repo, addons_path, source_branch)
res = []
Expand Down Expand Up @@ -523,6 +558,7 @@ def _scan_migration_path(
module,
module_branch_id,
source_branch,
target_remote,
target_branch,
module_source_commit,
module_target_commit,
Expand All @@ -541,6 +577,7 @@ def _scan_module(
module: str,
module_branch_id: int,
source_branch: str,
target_remote: str,
target_branch: str,
source_commit: str,
target_commit: str,
Expand Down Expand Up @@ -591,7 +628,7 @@ def _scan_module(
target_branch,
)
oca_port_data = self._run_oca_port(
module_path, source_branch, target_branch
module_path, source_branch, target_remote, target_branch
)
data["report"] = oca_port_data
self._push_scanned_data(module_branch_id, data)
Expand Down Expand Up @@ -644,7 +681,7 @@ def _check_relevant_commits(self, repo, module_path, commits):
return True
return False

def _run_oca_port(self, module_path, source_branch, target_branch):
def _run_oca_port(self, module_path, source_branch, target_remote, target_branch):
_logger.info(
"%s: collect migration data for '%s' (%s -> %s)",
self.full_name,
Expand All @@ -655,7 +692,7 @@ def _run_oca_port(self, module_path, source_branch, target_branch):
# Initialize the oca-port app
params = {
"source": f"origin/{source_branch}",
"target": f"origin/{target_branch}",
"target": f"{target_remote}/{target_branch}",
"addon_path": module_path,
"upstream_org": self.org,
"repo_path": self.path,
Expand Down
2 changes: 1 addition & 1 deletion odoo_repository_migration/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
{
"name": "Odoo Repository Migration Data",
"summary": "Collect modules migration data for Odoo Repositories.",
"version": "16.0.1.1.0",
"version": "16.0.1.2.0",
"category": "Tools",
"author": "Camptocamp, Odoo Community Association (OCA)",
"website": "https://github.com/camptocamp/odoo-repository",
Expand Down
56 changes: 56 additions & 0 deletions odoo_repository_migration/migrations/16.0.1.2.0/post-migration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Copyright 2024 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)

import logging

from odoo import SUPERUSER_ID, api

_logger = logging.getLogger(__name__)


def migrate(cr, version):
if not version:
return
env = api.Environment(cr, SUPERUSER_ID, {})
fix_migration_states(env)


def fix_migration_states(env):
_logger.info("Plan a scan to fix '<odoo.module.branch.migration>.state' field...")
query = """
SELECT mig.id
FROM odoo_module_branch_migration mig
JOIN odoo_module_branch AS source
ON mig.module_branch_id=source.id
JOIN odoo_module_branch AS target
ON mig.target_module_branch_id=target.id
WHERE source.repository_branch_id IS NOT NULL
AND target.repository_branch_id IS NOT NULL
AND source.repository_id != target.repository_id;
"""
env.cr.execute(query)
mig_ids = [row[0] for row in env.cr.fetchall()]
# Reset 'last_target_scanned_commit' to recompute 'migration_scan'
if mig_ids:
query = """
UPDATE odoo_module_branch_migration
SET last_target_scanned_commit=NULL
WHERE id IN %s;
"""
args = (tuple(mig_ids),)
env.cr.execute(query, args)
# Reset collected migration data on modules that shouldn't have any
mbm_model = env["odoo.module.branch.migration"]
migs = mbm_model.search(
[
("results", "!=", False),
("repository_id.collect_migration_data", "=", False),
]
)
migs.results = False
# Recompute migration state/flag, especially for modules moved to another repo
# that won't trigger a migration scan.
migs = mbm_model.search([("id", "in", mig_ids)])
env.add_to_compute(migs._fields["state"], migs)
env.add_to_compute(migs._fields["migration_scan"], migs)
migs.modified(["state", "migration_scan"])
Loading