From c399170e24f8e6e530dcaf199fd04c2010e44f91 Mon Sep 17 00:00:00 2001 From: Alexandra Borovova Date: Mon, 23 Feb 2026 17:59:24 +0100 Subject: [PATCH 1/3] Implement backouts handling for commits with git reverts. Since the migration of gecko repo the backout commit look like git revert commits, so we have to parse them differently. They also contain only the original git hash, which we have to convert to identify backout commits. --- config/dev/sync.ini | 1 + sync/commit.py | 25 ++++++++++++++++++++++-- sync/lando.py | 31 +++++++++++++++++++++++++++++ sync_prod.ini | 1 + test/config/sync.ini | 1 + test/test_upstream.py | 45 +++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 102 insertions(+), 2 deletions(-) create mode 100644 sync/lando.py diff --git a/config/dev/sync.ini b/config/dev/sync.ini index 7597892e3..bc70f8584 100644 --- a/config/dev/sync.ini +++ b/config/dev/sync.ini @@ -57,4 +57,5 @@ listener.interval = 60 [lando] api_token=%SECRET% +api_url = https://lando.moz.tools/api user_email=wptsync@mozilla.com diff --git a/sync/commit.py b/sync/commit.py index 0d74a93f1..87f89009d 100644 --- a/sync/commit.py +++ b/sync/commit.py @@ -11,6 +11,7 @@ from . import log from .env import Environment from .errors import AbortError +from .lando import git2hg from .repos import cinnabar, cinnabar_map, pygit2_get from typing import Dict @@ -544,8 +545,16 @@ def has_wpt_changes(self) -> bool: @property def is_backout(self) -> bool: + return self.is_hg_backout or self.is_git_revert + + @property + def is_hg_backout(self) -> bool: return commitparser.is_backout(self.msg) + @property + def is_git_revert(self) -> bool: + return commitparser.parse_reverts(self.msg) is not None + @property def is_downstream(self) -> bool: from . import downstream @@ -558,12 +567,20 @@ def is_landing(self) -> bool: return landing.LandingSync.has_metadata(self.msg) + def parse_backouts(self) -> tuple[list[bytes], list[int]] | None: + if self.is_hg_backout: + nodes_bugs = commitparser.parse_backouts(self.msg) + elif self.is_git_revert: + nodes_bugs = commitparser.parse_reverts(self.msg) + + return nodes_bugs + def commits_backed_out(self) -> tuple[list[GeckoCommit], set[int]]: # TODO: should bugs be int here commits: list[GeckoCommit] = [] bugs: list[int] = [] if self.is_backout: - nodes_bugs = commitparser.parse_backouts(self.msg) + nodes_bugs = self.parse_backouts() if nodes_bugs is None: # We think this a backout, but have no idea what it backs out # it's not clear how to handle that case so for now we pretend it isn't @@ -573,7 +590,11 @@ def commits_backed_out(self) -> tuple[list[GeckoCommit], set[int]]: nodes, bugs = nodes_bugs # Assuming that all commits are listed. for node in nodes: - git_sha = cinnabar(self.repo).hg2git(node.decode("ascii")) + hg_revision = node.decode("ascii") + if self.is_git_revert: + # Convert original git hash to mercurial + hg_revision = git2hg(hg_revision) + git_sha = cinnabar(self.repo).hg2git(hg_revision) commits.append(GeckoCommit(self.repo, git_sha)) return commits, set(bugs) diff --git a/sync/lando.py b/sync/lando.py new file mode 100644 index 000000000..6e58c5c11 --- /dev/null +++ b/sync/lando.py @@ -0,0 +1,31 @@ +import json +import urllib.request + +from .env import Environment +from . import log + +env = Environment() + +logger = log.get_logger(__name__) + + +def hg2git(hg_hash: str) -> str: + response = urllib.request.urlopen(env.config["lando"]["api_url"] + "/hg2git/firefox/" + hg_hash) + data = response.read() + map = json.loads(data.decode("utf-8")) + assert isinstance(map, dict) + assert isinstance(map["git_hash"], str) + + return map["git_hash"] + + +def git2hg(git_hash: str) -> str: + response = urllib.request.urlopen( + env.config["lando"]["api_url"] + "/git2hg/firefox/" + git_hash + ) + data = response.read() + map = json.loads(data.decode("utf-8")) + assert isinstance(map, dict) + assert isinstance(map["hg_hash"], str) + + return map["hg_hash"] diff --git a/sync_prod.ini b/sync_prod.ini index 6436ebfaa..ca10bad67 100644 --- a/sync_prod.ini +++ b/sync_prod.ini @@ -107,4 +107,5 @@ repo.url = https://github.com/web-platform-tests/wpt-metadata [lando] api_token=%SECRET% +api_url = https://lando.moz.tools/api user_email=wptsync@mozilla.com diff --git a/test/config/sync.ini b/test/config/sync.ini index 331928718..c41c82094 100644 --- a/test/config/sync.ini +++ b/test/config/sync.ini @@ -63,4 +63,5 @@ listener.interval = 60 [lando] api_token="%SECRET%" +api_url = https://lando.moz.tools/api user_email="wptsync@mozilla.com" diff --git a/test/test_upstream.py b/test/test_upstream.py index 5ae8f1300..707f33f5a 100644 --- a/test/test_upstream.py +++ b/test/test_upstream.py @@ -1,3 +1,5 @@ +from unittest.mock import patch + from sync import commit as sync_commit, upstream from sync.gitutils import update_repositories from sync.lock import SyncLock @@ -84,6 +86,49 @@ def test_create_pr_backout(git_gecko, git_wpt, upstream_gecko_commit, upstream_g assert backout_commit.upstream_sync(git_gecko, git_wpt) == sync +def test_create_pr_revert(git_gecko, git_wpt, upstream_gecko_commit, upstream_gecko_backout): + bug = 1234 + test_changes = {"README": b"Change README\n"} + message = f"Bug {bug} - Change README" + rev = upstream_gecko_commit(test_changes=test_changes, bug=bug, message=message.encode()) + + update_repositories(git_gecko, git_wpt, wait_gecko_commit=rev) + upstream.gecko_push(git_gecko, git_wpt, "autoland", rev, raise_on_error=True) + + syncs = upstream.UpstreamSync.for_bug(git_gecko, git_wpt, bug) + assert list(syncs.keys()) == ["open"] + assert len(syncs["open"]) == 1 + sync = syncs["open"].pop() + assert sync.bug == 1234 + assert sync.status == "open" + assert len(sync.gecko_commits) == 1 + assert len(sync.wpt_commits) == 1 + assert sync.pr + + print("rev", rev) + backout_rev = upstream_gecko_backout( + rev, + bug, + f"""Revert \"{message}\"\nThis reverts commit bd771e8b679de5312fbb0e8bfa24edc1ca87b1e5.""".encode(), + ) + + update_repositories(git_gecko, git_wpt, wait_gecko_commit=backout_rev) + with patch("sync.commit.git2hg", return_value=rev): + upstream.gecko_push(git_gecko, git_wpt, "autoland", backout_rev, raise_on_error=True) + syncs = upstream.UpstreamSync.for_bug(git_gecko, git_wpt, bug) + assert list(syncs.keys()) == ["incomplete"] + assert len(syncs["incomplete"]) == 1 + sync = syncs["incomplete"].pop() + assert sync.bug == 1234 + with patch("sync.commit.git2hg", return_value=rev): + assert len(sync.gecko_commits) == 0 + assert len(sync.wpt_commits) == 1 + assert len(sync.upstreamed_gecko_commits) == 1 + assert sync.status == "incomplete" + backout_commit = sync_commit.GeckoCommit(git_gecko, cinnabar(git_gecko).hg2git(rev)) + assert backout_commit.upstream_sync(git_gecko, git_wpt) == sync + + def test_create_pr_backout_reland( git_gecko, git_wpt, upstream_gecko_commit, upstream_gecko_backout ): From 461e9bd7dac9ea86dd7f97aeaf970b19f4580d43 Mon Sep 17 00:00:00 2001 From: Alexandra Borovova Date: Mon, 23 Feb 2026 18:09:29 +0100 Subject: [PATCH 2/3] Ignore the bandit issue for the url schema. --- sync/lando.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sync/lando.py b/sync/lando.py index 6e58c5c11..a39452d10 100644 --- a/sync/lando.py +++ b/sync/lando.py @@ -10,7 +10,7 @@ def hg2git(hg_hash: str) -> str: - response = urllib.request.urlopen(env.config["lando"]["api_url"] + "/hg2git/firefox/" + hg_hash) + response = urllib.request.urlopen(env.config["lando"]["api_url"] + "/hg2git/firefox/" + hg_hash) # nosec B310 data = response.read() map = json.loads(data.decode("utf-8")) assert isinstance(map, dict) @@ -22,7 +22,7 @@ def hg2git(hg_hash: str) -> str: def git2hg(git_hash: str) -> str: response = urllib.request.urlopen( env.config["lando"]["api_url"] + "/git2hg/firefox/" + git_hash - ) + ) # nosec B310 data = response.read() map = json.loads(data.decode("utf-8")) assert isinstance(map, dict) From 1385145640e2c0023999a618e647ab708e343f86 Mon Sep 17 00:00:00 2001 From: Alexandra Borovova Date: Tue, 24 Feb 2026 11:36:02 +0100 Subject: [PATCH 3/3] Address review feedback. --- sync/commit.py | 16 +++++++--------- sync/lando.py | 10 ---------- test/conftest.py | 15 +++++++++++++++ test/test_upstream.py | 9 ++------- 4 files changed, 24 insertions(+), 26 deletions(-) diff --git a/sync/commit.py b/sync/commit.py index 87f89009d..1c4228747 100644 --- a/sync/commit.py +++ b/sync/commit.py @@ -567,20 +567,18 @@ def is_landing(self) -> bool: return landing.LandingSync.has_metadata(self.msg) - def parse_backouts(self) -> tuple[list[bytes], list[int]] | None: - if self.is_hg_backout: - nodes_bugs = commitparser.parse_backouts(self.msg) - elif self.is_git_revert: - nodes_bugs = commitparser.parse_reverts(self.msg) - - return nodes_bugs - def commits_backed_out(self) -> tuple[list[GeckoCommit], set[int]]: # TODO: should bugs be int here commits: list[GeckoCommit] = [] bugs: list[int] = [] if self.is_backout: - nodes_bugs = self.parse_backouts() + nodes_bugs = None + + if self.is_hg_backout: + nodes_bugs = commitparser.parse_backouts(self.msg) + elif self.is_git_revert: + nodes_bugs = commitparser.parse_reverts(self.msg) + if nodes_bugs is None: # We think this a backout, but have no idea what it backs out # it's not clear how to handle that case so for now we pretend it isn't diff --git a/sync/lando.py b/sync/lando.py index a39452d10..075663a03 100644 --- a/sync/lando.py +++ b/sync/lando.py @@ -9,16 +9,6 @@ logger = log.get_logger(__name__) -def hg2git(hg_hash: str) -> str: - response = urllib.request.urlopen(env.config["lando"]["api_url"] + "/hg2git/firefox/" + hg_hash) # nosec B310 - data = response.read() - map = json.loads(data.decode("utf-8")) - assert isinstance(map, dict) - assert isinstance(map["git_hash"], str) - - return map["git_hash"] - - def git2hg(git_hash: str) -> str: response = urllib.request.urlopen( env.config["lando"]["api_url"] + "/git2hg/firefox/" + git_hash diff --git a/test/conftest.py b/test/conftest.py index bd347342e..3fa89b435 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -1,5 +1,6 @@ import copy import glob +import hashlib import io import json import os @@ -383,6 +384,20 @@ def inner(revs, bugs, message=None, bookmarks="mozilla/autoland"): return inner +@pytest.fixture +def upstream_gecko_revert(env, hg_gecko_upstream): + def inner(message, rev, bookmarks="mozilla/autoland"): + # Git hash has to be converted to hg hash with API. + # Since we going to mock this API call, this git hash + # can have a random value that just looks like git hash. + random_hash = hashlib.sha1(os.urandom(20)).hexdigest() + commit_message = f"""Revert \"{message}\"\nThis reverts commit {random_hash}.""".encode() + hg_gecko_upstream.backout("--no-commit", rev) + return hg_commit(hg_gecko_upstream, commit_message, bookmarks) + + return inner + + @pytest.fixture def gecko_worktree(env, git_gecko): path = os.path.join(env.config["root"], env.config["paths"]["worktrees"], "gecko", "autoland") diff --git a/test/test_upstream.py b/test/test_upstream.py index 707f33f5a..a662a8b6b 100644 --- a/test/test_upstream.py +++ b/test/test_upstream.py @@ -86,7 +86,7 @@ def test_create_pr_backout(git_gecko, git_wpt, upstream_gecko_commit, upstream_g assert backout_commit.upstream_sync(git_gecko, git_wpt) == sync -def test_create_pr_revert(git_gecko, git_wpt, upstream_gecko_commit, upstream_gecko_backout): +def test_create_pr_revert(git_gecko, git_wpt, upstream_gecko_commit, upstream_gecko_revert): bug = 1234 test_changes = {"README": b"Change README\n"} message = f"Bug {bug} - Change README" @@ -105,12 +105,7 @@ def test_create_pr_revert(git_gecko, git_wpt, upstream_gecko_commit, upstream_ge assert len(sync.wpt_commits) == 1 assert sync.pr - print("rev", rev) - backout_rev = upstream_gecko_backout( - rev, - bug, - f"""Revert \"{message}\"\nThis reverts commit bd771e8b679de5312fbb0e8bfa24edc1ca87b1e5.""".encode(), - ) + backout_rev = upstream_gecko_revert(message, rev) update_repositories(git_gecko, git_wpt, wait_gecko_commit=backout_rev) with patch("sync.commit.git2hg", return_value=rev):