This is an automated email from the ASF dual-hosted git repository.

sbp pushed a commit to branch sbp
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git


The following commit(s) were added to refs/heads/sbp by this push:
     new 517b7bd  Resolve conflicts during revision creation, and add a 
corresponding test
517b7bd is described below

commit 517b7bd83127683a07a9d6f9b86c29d8e6caf6b9
Author: Sean B. Palmer <[email protected]>
AuthorDate: Wed Feb 11 10:50:53 2026 +0000

    Resolve conflicts during revision creation, and add a corresponding test
---
 atr/get/test.py                 | 41 ++++++++++++++++++++++++++++++++++
 atr/storage/writers/revision.py | 27 +++++++++++++++++++++++
 tests/e2e/merge/__init__.py     | 16 ++++++++++++++
 tests/e2e/merge/conftest.py     | 49 +++++++++++++++++++++++++++++++++++++++++
 tests/e2e/merge/helpers.py      | 21 ++++++++++++++++++
 tests/e2e/merge/test_get.py     | 36 ++++++++++++++++++++++++++++++
 6 files changed, 190 insertions(+)

diff --git a/atr/get/test.py b/atr/get/test.py
index a76f623..102b710 100644
--- a/atr/get/test.py
+++ b/atr/get/test.py
@@ -15,10 +15,15 @@
 # specific language governing permissions and limitations
 # under the License.
 
+import json
+
+import aiofiles
 import asfquart.base as base
+import werkzeug.wrappers.response as response
 
 import atr.blueprints.get as get
 import atr.config as config
+import atr.db as db
 import atr.form as form
 import atr.get.root as root
 import atr.get.vote as vote
@@ -26,6 +31,7 @@ import atr.htm as htm
 import atr.models.session
 import atr.models.sql as sql
 import atr.shared as shared
+import atr.storage as storage
 import atr.template as template
 import atr.util as util
 import atr.web as web
@@ -68,6 +74,41 @@ async def test_login(session: web.Committer | None) -> 
web.WerkzeugResponse:
     return await web.redirect(root.index)
 
 
[email protected]("/test/merge/<project_name>/<version_name>")
+async def test_merge(session: web.Committer, project_name: str, version_name: 
str) -> web.WerkzeugResponse:
+    if not config.get().ALLOW_TESTS:
+        raise base.ASFQuartException("Test routes not enabled", errorcode=404)
+
+    async with storage.write(session) as write_n:
+        wacp_n = await write_n.as_project_committee_participant(project_name)
+        async with wacp_n.release.create_and_manage_revision(
+            project_name, version_name, "Test merge: new revision"
+        ) as creating_n:
+            async with aiofiles.open(creating_n.interim_path / "from_new.txt", 
"w") as f:
+                await f.write("new content")
+
+            async with storage.write(session) as write_p:
+                wacp_p = await 
write_p.as_project_committee_participant(project_name)
+                async with wacp_p.release.create_and_manage_revision(
+                    project_name, version_name, "Test merge: prior revision"
+                ) as creating_p:
+                    async with aiofiles.open(creating_p.interim_path / 
"from_prior.txt", "w") as f:
+                        await f.write("prior content")
+
+    files: list[str] = []
+    async with db.session() as data:
+        release_name = sql.release_name(project_name, version_name)
+        release = await data.release(name=release_name, _project=True).demand(
+            RuntimeError("Release not found after merge test")
+        )
+        release_dir = util.release_directory(release)
+    async for path in util.paths_recursive(release_dir):
+        files.append(str(path))
+
+    result = json.dumps({"files": sorted(files)})
+    return response.Response(result, status=200, mimetype="application/json")
+
+
 @get.public("/test/multiple")
 async def test_multiple(session: web.Committer | None) -> str:
     apple_form = form.render(
diff --git a/atr/storage/writers/revision.py b/atr/storage/writers/revision.py
index e776fc3..5371a0b 100644
--- a/atr/storage/writers/revision.py
+++ b/atr/storage/writers/revision.py
@@ -33,6 +33,7 @@ import atr.attestable as attestable
 import atr.db as db
 import atr.db.interaction as interaction
 import atr.detection as detection
+import atr.merge as merge
 import atr.models.sql as sql
 import atr.storage as storage
 import atr.storage.types as types
@@ -171,6 +172,13 @@ class CommitteeParticipant(FoundationCommitter):
             previous_attestable = None
             if parent_revision_number is not None:
                 previous_attestable = await attestable.load(project_name, 
version_name, parent_revision_number)
+            base_inodes: dict[str, int] = {}
+            base_hashes: dict[str, str] = {}
+            if old_revision is not None:
+                base_dir = util.release_directory(release)
+                base_inodes = await asyncio.to_thread(util.paths_to_inodes, 
base_dir)
+                base_hashes = dict(previous_attestable.paths) if 
(previous_attestable is not None) else {}
+            n_inodes = await asyncio.to_thread(util.paths_to_inodes, 
temp_dir_path)
         except Exception:
             await aioshutil.rmtree(temp_dir)
             raise
@@ -203,6 +211,25 @@ class CommitteeParticipant(FoundationCommitter):
                 # Give the caller details about the new revision
                 creating.new = new_revision
 
+                # Merge with the prior revision if there was an intervening 
change
+                prior_name = new_revision.parent_name
+                if (old_revision is not None) and (prior_name is not None) and 
(prior_name != old_revision.name):
+                    prior_number = prior_name.split()[-1]
+                    prior_dir = util.release_directory_base(release) / 
prior_number
+                    await merge.merge(
+                        base_inodes,
+                        base_hashes,
+                        prior_dir,
+                        project_name,
+                        version_name,
+                        prior_number,
+                        temp_dir_path,
+                        n_inodes,
+                        path_to_hash,
+                        path_to_size,
+                    )
+                    previous_attestable = await attestable.load(project_name, 
version_name, prior_number)
+
                 # Rename the directory to the new revision number
                 await data.refresh(release)
                 new_revision_dir = util.release_directory(release)
diff --git a/tests/e2e/merge/__init__.py b/tests/e2e/merge/__init__.py
new file mode 100644
index 0000000..13a8339
--- /dev/null
+++ b/tests/e2e/merge/__init__.py
@@ -0,0 +1,16 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
diff --git a/tests/e2e/merge/conftest.py b/tests/e2e/merge/conftest.py
new file mode 100644
index 0000000..63241d1
--- /dev/null
+++ b/tests/e2e/merge/conftest.py
@@ -0,0 +1,49 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+import e2e.helpers as helpers
+import e2e.merge.helpers as merge_helpers
+import pytest
+
+if TYPE_CHECKING:
+    from collections.abc import Generator
+
+    from playwright.sync_api import Browser, BrowserContext
+
+
[email protected](scope="module")
+def merge_context(browser: Browser) -> Generator[BrowserContext]:
+    context = browser.new_context(ignore_https_errors=True)
+    page = context.new_page()
+
+    helpers.log_in(page)
+    helpers.delete_release_if_exists(page, merge_helpers.PROJECT_NAME, 
merge_helpers.VERSION_NAME)
+
+    helpers.visit(page, f"/start/{merge_helpers.PROJECT_NAME}")
+    page.locator("input#version_name").fill(merge_helpers.VERSION_NAME)
+    page.get_by_role("button", name="Start new release").click()
+    
page.wait_for_url(f"**/compose/{merge_helpers.PROJECT_NAME}/{merge_helpers.VERSION_NAME}")
+
+    page.close()
+
+    yield context
+
+    context.close()
diff --git a/tests/e2e/merge/helpers.py b/tests/e2e/merge/helpers.py
new file mode 100644
index 0000000..13c4af6
--- /dev/null
+++ b/tests/e2e/merge/helpers.py
@@ -0,0 +1,21 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from typing import Final
+
+PROJECT_NAME: Final[str] = "test"
+VERSION_NAME: Final[str] = "0.1+e2e-merge"
diff --git a/tests/e2e/merge/test_get.py b/tests/e2e/merge/test_get.py
new file mode 100644
index 0000000..27cd520
--- /dev/null
+++ b/tests/e2e/merge/test_get.py
@@ -0,0 +1,36 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+import e2e.helpers as helpers
+import e2e.merge.helpers as merge_helpers
+
+if TYPE_CHECKING:
+    from playwright.sync_api import BrowserContext
+
+
+def test_merge_interleaved_revisions(merge_context: BrowserContext) -> None:
+    result = helpers.api_get(
+        merge_context.request,
+        
f"/test/merge/{merge_helpers.PROJECT_NAME}/{merge_helpers.VERSION_NAME}",
+    )
+    files = result["files"]
+    assert "from_new.txt" in files
+    assert "from_prior.txt" in files


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to