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

potiuk pushed a commit to branch v3-1-test
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/v3-1-test by this push:
     new 5fd3fa5a790 [v3-1-test] Fix breeze worktree issue (#62905) (#63304)
5fd3fa5a790 is described below

commit 5fd3fa5a79013223ba2349ece780185ed13920a3
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Tue Mar 10 23:26:42 2026 +0100

    [v3-1-test] Fix breeze worktree issue (#62905) (#63304)
    
    * Fix Breeze commands failing in git worktrees
    
    * Refine worktree fix: address review feedback (YAML formatting and path 
safety)
    
    * Fix unit tests: use valid paths for git worktree detection tests
    (cherry picked from commit bdb9a7e042b9951160f654da37cc7c40be18bfb0)
    
    Co-authored-by: Subham <[email protected]>
---
 .../src/airflow_breeze/params/shell_params.py      |  22 +++++
 .../airflow_breeze/utils/docker_command_utils.py   |  36 ++++---
 dev/breeze/src/airflow_breeze/utils/path_utils.py  |  26 +++++
 dev/breeze/tests/test_git_worktree.py              | 106 +++++++++++++++++++++
 4 files changed, 176 insertions(+), 14 deletions(-)

diff --git a/dev/breeze/src/airflow_breeze/params/shell_params.py 
b/dev/breeze/src/airflow_breeze/params/shell_params.py
index d38880b1eca..5565806efee 100644
--- a/dev/breeze/src/airflow_breeze/params/shell_params.py
+++ b/dev/breeze/src/airflow_breeze/params/shell_params.py
@@ -103,6 +103,7 @@ from airflow_breeze.utils.path_utils import (
     SCRIPTS_CI_DOCKER_COMPOSE_PROVIDERS_AND_TESTS_SOURCES_PATH,
     SCRIPTS_CI_DOCKER_COMPOSE_REMOVE_SOURCES_PATH,
     SCRIPTS_CI_DOCKER_COMPOSE_TESTS_SOURCES_PATH,
+    get_main_git_dir_for_worktree,
 )
 from airflow_breeze.utils.run_utils import commit_sha, run_command
 from airflow_breeze.utils.shared_options import get_forced_answer, get_verbose
@@ -385,6 +386,7 @@ class ShellParams:
 
         compose_file_list.append(SCRIPTS_CI_DOCKER_COMPOSE_BASE_PATH)
         self.add_docker_in_docker(compose_file_list)
+        self.add_git_worktree_mount(compose_file_list)
         compose_file_list.extend(backend_files)
         compose_file_list.append(SCRIPTS_CI_DOCKER_COMPOSE_FILES_PATH)
         if os.environ.get("CI", "false") == "true":
@@ -530,6 +532,26 @@ class ShellParams:
             # the /var/run/docker.sock is available. See 
https://github.com/docker/for-mac/issues/6545
             
compose_file_list.append(SCRIPTS_CI_DOCKER_COMPOSE_DOCKER_SOCKET_PATH)
 
+    def add_git_worktree_mount(self, compose_file_list: list[Path]):
+        main_git_directory = get_main_git_dir_for_worktree()
+        if main_git_directory:
+            get_console().print(
+                f"[info]Detected git worktree. Mounting main git directory: 
{main_git_directory}[/]"
+            )
+            generated_compose_file = SCRIPTS_CI_DOCKER_COMPOSE_PATH / 
"_generated_git_worktree_mount.yml"
+            generated_compose_file.write_text(
+                f"""---
+services:
+  airflow:
+    volumes:
+      - type: bind
+        source: "{main_git_directory}"
+        target: "{main_git_directory}"
+        read_only: true
+"""
+            )
+            compose_file_list.append(generated_compose_file)
+
     @cached_property
     def rootless_docker(self) -> bool:
         return is_docker_rootless()
diff --git a/dev/breeze/src/airflow_breeze/utils/docker_command_utils.py 
b/dev/breeze/src/airflow_breeze/utils/docker_command_utils.py
index 3e717569ddb..e5a49a94dc3 100644
--- a/dev/breeze/src/airflow_breeze/utils/docker_command_utils.py
+++ b/dev/breeze/src/airflow_breeze/utils/docker_command_utils.py
@@ -37,6 +37,7 @@ from airflow_breeze.utils.path_utils import (
     SCRIPTS_DOCKER_PATH,
     cleanup_python_generated_files,
     create_mypy_volume_if_needed,
+    get_main_git_dir_for_worktree,
 )
 from airflow_breeze.utils.shared_options import get_verbose
 from airflow_breeze.utils.visuals import ASCIIART, ASCIIART_STYLE, CHEATSHEET, 
CHEATSHEET_STYLE
@@ -659,21 +660,28 @@ def fix_ownership_using_docker(quiet: bool = True):
         "run",
         "-v",
         f"{AIRFLOW_ROOT_PATH}:/opt/airflow/",
-        "-e",
-        f"HOST_OS={get_host_os()}",
-        "-e",
-        f"HOST_USER_ID={get_host_user_id()}",
-        "-e",
-        f"HOST_GROUP_ID={get_host_group_id()}",
-        "-e",
-        f"VERBOSE={str(get_verbose()).lower()}",
-        "-e",
-        f"DOCKER_IS_ROOTLESS={is_docker_rootless()}",
-        "--rm",
-        "-t",
-        OWNERSHIP_CLEANUP_DOCKER_TAG,
-        "/opt/airflow/scripts/in_container/run_fix_ownership.py",
     ]
+    main_git_directory = get_main_git_dir_for_worktree()
+    if main_git_directory:
+        cmd.extend(["-v", f"{main_git_directory}:{main_git_directory}:ro"])
+    cmd.extend(
+        [
+            "-e",
+            f"HOST_OS={get_host_os()}",
+            "-e",
+            f"HOST_USER_ID={get_host_user_id()}",
+            "-e",
+            f"HOST_GROUP_ID={get_host_group_id()}",
+            "-e",
+            f"VERBOSE={str(get_verbose()).lower()}",
+            "-e",
+            f"DOCKER_IS_ROOTLESS={is_docker_rootless()}",
+            "--rm",
+            "-t",
+            OWNERSHIP_CLEANUP_DOCKER_TAG,
+            "/opt/airflow/scripts/in_container/run_fix_ownership.py",
+        ]
+    )
     run_command(cmd, text=True, check=False, quiet=quiet)
 
 
diff --git a/dev/breeze/src/airflow_breeze/utils/path_utils.py 
b/dev/breeze/src/airflow_breeze/utils/path_utils.py
index 6ee4863a093..b7081dbae35 100644
--- a/dev/breeze/src/airflow_breeze/utils/path_utils.py
+++ b/dev/breeze/src/airflow_breeze/utils/path_utils.py
@@ -422,3 +422,29 @@ def cleanup_python_generated_files():
             get_console().print("You can also remove those files manually 
using sudo.")
     if get_verbose():
         get_console().print("[info]Cleaned")
+
+
+def get_main_git_dir_for_worktree() -> Path | None:
+    """
+    Detect if we are in a git worktree and return the main repository's .git 
directory.
+
+    Git worktrees store a ``.git`` *file* (not a directory) containing a 
``gitdir:`` reference
+    pointing to ``<main-repo>/.git/worktrees/<name>``.  This helper resolves 
that reference
+    (handling both absolute and relative paths) and returns the main ``.git`` 
directory
+    (i.e. the grandparent of the ``gitdir`` path).
+
+    :return: Absolute path to the main repository's ``.git`` directory, or 
``None``
+             if the current checkout is not a worktree.
+    """
+    git_path = AIRFLOW_ROOT_PATH / ".git"
+    if git_path.is_file():
+        git_content = git_path.read_text().strip()
+        if git_content.startswith("gitdir:"):
+            gitdir = Path(git_content.removeprefix("gitdir:").strip())
+            if not gitdir.is_absolute():
+                gitdir = (AIRFLOW_ROOT_PATH / gitdir).resolve()
+            # gitdir points to <main-repo>/.git/worktrees/<name>
+            # .parent.parent gives us <main-repo>/.git
+            main_git_dir = gitdir.parent.parent
+            return main_git_dir if main_git_dir.is_dir() else None
+    return None
diff --git a/dev/breeze/tests/test_git_worktree.py 
b/dev/breeze/tests/test_git_worktree.py
new file mode 100644
index 00000000000..a0c9d439bd2
--- /dev/null
+++ b/dev/breeze/tests/test_git_worktree.py
@@ -0,0 +1,106 @@
+# 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 pathlib import Path
+from unittest import mock
+
+import pytest
+
+from airflow_breeze.utils.path_utils import get_main_git_dir_for_worktree
+
+
+class TestGetMainGitDirForWorktree:
+    """Tests for get_main_git_dir_for_worktree detection."""
+
+    def test_returns_none_when_dot_git_is_directory(self, tmp_path):
+        """Standard clone: .git is a directory, not a worktree."""
+        git_dir = tmp_path / ".git"
+        git_dir.mkdir()
+        with mock.patch("airflow_breeze.utils.path_utils.AIRFLOW_ROOT_PATH", 
tmp_path):
+            assert get_main_git_dir_for_worktree() is None
+
+    def test_returns_none_when_dot_git_missing(self, tmp_path):
+        """No .git at all — not a git repo."""
+        with mock.patch("airflow_breeze.utils.path_utils.AIRFLOW_ROOT_PATH", 
tmp_path):
+            assert get_main_git_dir_for_worktree() is None
+
+    def test_returns_none_when_dot_git_file_without_gitdir_prefix(self, 
tmp_path):
+        """.git file exists but does not start with 'gitdir: '."""
+        git_file = tmp_path / ".git"
+        git_file.write_text("something unexpected\n")
+        with mock.patch("airflow_breeze.utils.path_utils.AIRFLOW_ROOT_PATH", 
tmp_path):
+            assert get_main_git_dir_for_worktree() is None
+
+    def test_absolute_gitdir_path(self, tmp_path):
+        """Worktree with an absolute gitdir path resolves to the main .git 
directory."""
+        # Simulate: /main-repo/.git/worktrees/my-worktree
+        main_repo = tmp_path / "main-repo"
+        main_git = main_repo / ".git"
+        worktree_gitdir = main_git / "worktrees" / "my-worktree"
+        worktree_gitdir.mkdir(parents=True)
+
+        worktree_dir = tmp_path / "my-worktree"
+        worktree_dir.mkdir()
+        (worktree_dir / ".git").write_text(f"gitdir: {worktree_gitdir}\n")
+
+        with mock.patch("airflow_breeze.utils.path_utils.AIRFLOW_ROOT_PATH", 
worktree_dir):
+            result = get_main_git_dir_for_worktree()
+            assert result is not None
+            assert result == main_git
+
+    def test_relative_gitdir_path(self, tmp_path):
+        """Worktree with a relative gitdir path is resolved correctly."""
+        # Simulate: main-repo/.git/worktrees/my-worktree
+        main_repo = tmp_path / "main-repo"
+        main_git = main_repo / ".git"
+        worktree_gitdir = main_git / "worktrees" / "my-worktree"
+        worktree_gitdir.mkdir(parents=True)
+
+        worktree_dir = tmp_path / "my-worktree"
+        worktree_dir.mkdir()
+        # Write a relative path from worktree_dir to worktree_gitdir
+        relative_gitdir = Path("../main-repo/.git/worktrees/my-worktree")
+        (worktree_dir / ".git").write_text(f"gitdir: {relative_gitdir}\n")
+
+        with mock.patch("airflow_breeze.utils.path_utils.AIRFLOW_ROOT_PATH", 
worktree_dir):
+            result = get_main_git_dir_for_worktree()
+            assert result is not None
+            assert result.resolve() == main_git.resolve()
+
+    @pytest.mark.parametrize(
+        "gitdir_content",
+        [
+            "gitdir: {path}/worktrees/wt\n",
+            "gitdir: {path}/worktrees/wt",
+            "gitdir:  {path}/worktrees/wt  \n",
+        ],
+        ids=["trailing-newline", "no-trailing-newline", "extra-whitespace"],
+    )
+    def test_strips_whitespace_from_gitdir(self, tmp_path, gitdir_content):
+        """Whitespace and trailing newlines are stripped from the gitdir 
content."""
+        main_repo = tmp_path / "main-repo"
+        main_git = main_repo / ".git"
+        (main_git / "worktrees" / "wt").mkdir(parents=True)
+
+        content = gitdir_content.format(path=main_git)
+        (tmp_path / ".git").write_text(content)
+
+        with mock.patch("airflow_breeze.utils.path_utils.AIRFLOW_ROOT_PATH", 
tmp_path):
+            result = get_main_git_dir_for_worktree()
+            assert result is not None
+            assert result == main_git

Reply via email to