diff --git a/frontend/src/i18n/translation.json b/frontend/src/i18n/translation.json index 4b2299fa80..721c732bcb 100644 --- a/frontend/src/i18n/translation.json +++ b/frontend/src/i18n/translation.json @@ -6766,55 +6766,55 @@ "ca": "Clonació de repositoris" }, "SETTINGS$REPOSITORY_CLONING_DESCRIPTION": { - "en": "OpenHands uses shallow clones by default to speed up startup for large repositories.", - "ja": "OpenHands は、大規模なリポジトリでの起動を高速化するため、デフォルトで浅いクローンを使用します。", - "zh-CN": "OpenHands 默认使用浅克隆,以加快大型仓库的启动速度。", - "zh-TW": "OpenHands 預設使用淺層複製,以加快大型儲存庫的啟動速度。", - "ko-KR": "OpenHands는 대규모 리포지토리의 시작 속도를 높이기 위해 기본적으로 얕은 클론을 사용합니다.", - "de": "OpenHands verwendet standardmäßig flache Klone, um den Start bei großen Repositorys zu beschleunigen.", - "no": "OpenHands bruker grunne kloner som standard for å gjøre oppstart raskere for store repositorier.", - "it": "OpenHands usa cloni superficiali per impostazione predefinita per velocizzare l'avvio con repository di grandi dimensioni.", - "pt": "O OpenHands usa clones rasos por padrão para acelerar a inicialização em repositórios grandes.", - "es": "OpenHands usa clones superficiales de forma predeterminada para acelerar el inicio en repositorios grandes.", - "ar": "يستخدم OpenHands النسخ السطحي افتراضيًا لتسريع بدء التشغيل للمستودعات الكبيرة.", - "fr": "OpenHands utilise par défaut des clones superficiels pour accélérer le démarrage avec les grands dépôts.", - "tr": "OpenHands, büyük depolarda başlatmayı hızlandırmak için varsayılan olarak sığ klonlar kullanır.", - "uk": "OpenHands типово використовує поверхневі клони, щоб прискорити запуск для великих репозиторіїв.", - "ca": "OpenHands utilitza clons superficials per defecte per accelerar l'inici en repositoris grans." + "en": "By default OpenHands downloads a lightweight copy of your repository to speed up startup for large repositories. The complete commit history is always included; individual file contents are downloaded only when a task needs them.", + "ja": "OpenHands は、大規模なリポジトリでの起動を高速化するため、デフォルトでリポジトリの軽量なコピーをダウンロードします。コミット履歴は常に完全に含まれ、個々のファイルの内容はタスクが必要とするときにのみダウンロードされます。", + "zh-CN": "OpenHands 默认下载仓库的轻量副本,以加快大型仓库的启动速度。完整的提交历史始终包含在内;各个文件的内容仅在任务需要时才会下载。", + "zh-TW": "OpenHands 預設下載儲存庫的輕量副本,以加快大型儲存庫的啟動速度。完整的提交歷史記錄一律包含在內;個別檔案的內容僅在工作需要時才會下載。", + "ko-KR": "OpenHands는 대규모 리포지토리의 시작 속도를 높이기 위해 기본적으로 리포지토리의 경량 사본을 다운로드합니다. 전체 커밋 기록은 항상 포함되며, 개별 파일 내용은 작업에 필요할 때만 다운로드됩니다.", + "de": "OpenHands lädt standardmäßig eine schlanke Kopie Ihres Repositorys herunter, um den Start bei großen Repositorys zu beschleunigen. Der vollständige Commit-Verlauf ist immer enthalten; einzelne Dateiinhalte werden nur dann heruntergeladen, wenn eine Aufgabe sie benötigt.", + "no": "OpenHands laster som standard ned en lettvektskopi av repositoriet for å gjøre oppstart raskere for store repositorier. Hele commit-historikken er alltid inkludert; innholdet i enkeltfiler lastes bare ned når en oppgave trenger det.", + "it": "Per impostazione predefinita OpenHands scarica una copia leggera del repository per velocizzare l'avvio con repository di grandi dimensioni. La cronologia completa dei commit è sempre inclusa; il contenuto dei singoli file viene scaricato solo quando un'attività ne ha bisogno.", + "pt": "Por padrão, o OpenHands baixa uma cópia leve do seu repositório para acelerar a inicialização em repositórios grandes. O histórico completo de commits é sempre incluído; o conteúdo de cada arquivo é baixado apenas quando uma tarefa precisa dele.", + "es": "De forma predeterminada, OpenHands descarga una copia ligera de tu repositorio para acelerar el inicio en repositorios grandes. El historial completo de commits siempre se incluye; el contenido de cada archivo se descarga solo cuando una tarea lo necesita.", + "ar": "يقوم OpenHands افتراضيًا بتنزيل نسخة خفيفة من المستودع لتسريع بدء التشغيل للمستودعات الكبيرة. يتم دائمًا تضمين سجل الالتزامات الكامل؛ ويتم تنزيل محتويات الملفات الفردية فقط عندما تحتاجها المهمة.", + "fr": "Par défaut, OpenHands télécharge une copie légère de votre dépôt pour accélérer le démarrage avec les grands dépôts. L'historique complet des commits est toujours inclus ; le contenu des fichiers individuels n'est téléchargé que lorsqu'une tâche en a besoin.", + "tr": "OpenHands, büyük depolarda başlatmayı hızlandırmak için varsayılan olarak deponuzun hafif bir kopyasını indirir. Tüm commit geçmişi her zaman dahildir; tek tek dosya içerikleri yalnızca bir görev gerektirdiğinde indirilir.", + "uk": "Типово OpenHands завантажує полегшену копію вашого репозиторію, щоб прискорити запуск для великих репозиторіїв. Повна історія комітів завжди включена; вміст окремих файлів завантажується лише тоді, коли він потрібен завданню.", + "ca": "Per defecte, OpenHands descarrega una còpia lleugera del vostre repositori per accelerar l'inici en repositoris grans. L'historial complet de commits sempre s'inclou; el contingut de cada fitxer es descarrega només quan una tasca el necessita." }, "SETTINGS$FETCH_FULL_GIT_HISTORY": { - "en": "Fetch full Git history for repositories", - "ja": "リポジトリの完全な Git 履歴を取得する", - "zh-CN": "获取仓库的完整 Git 历史", - "zh-TW": "擷取儲存庫的完整 Git 歷史記錄", - "ko-KR": "리포지토리의 전체 Git 기록 가져오기", - "de": "Vollständigen Git-Verlauf für Repositorys abrufen", - "no": "Hent full Git-historikk for repositorier", - "it": "Recupera la cronologia Git completa per i repository", - "pt": "Buscar o histórico completo do Git para repositórios", - "es": "Obtener el historial completo de Git para los repositorios", - "ar": "جلب سجل Git الكامل للمستودعات", - "fr": "Récupérer l’historique Git complet des dépôts", - "tr": "Depolar için tam Git geçmişini getir", - "uk": "Отримувати повну історію Git для репозиторіїв", - "ca": "Obtén l'historial complet de Git dels repositoris" + "en": "Download all repository files up front", + "ja": "リポジトリのすべてのファイルを事前にダウンロードする", + "zh-CN": "预先下载仓库的所有文件", + "zh-TW": "預先下載儲存庫的所有檔案", + "ko-KR": "리포지토리의 모든 파일을 미리 다운로드", + "de": "Alle Repository-Dateien im Voraus herunterladen", + "no": "Last ned alle filer i repositoriet på forhånd", + "it": "Scarica in anticipo tutti i file del repository", + "pt": "Baixar todos os arquivos do repositório antecipadamente", + "es": "Descargar todos los archivos del repositorio por adelantado", + "ar": "تنزيل جميع ملفات المستودع مسبقًا", + "fr": "Télécharger tous les fichiers du dépôt à l'avance", + "tr": "Tüm depo dosyalarını önceden indir", + "uk": "Завантажувати всі файли репозиторію заздалегідь", + "ca": "Descarrega tots els fitxers del repositori per avançat" }, "SETTINGS$FETCH_FULL_GIT_HISTORY_HELPER": { - "en": "Turn this on if your tasks require full Git history, tags, merge bases, historical blame, or checking out older commits. Full clones may take longer for large repositories.", - "ja": "タスクで完全な Git 履歴、タグ、マージベース、過去の blame、または古いコミットのチェックアウトが必要な場合はオンにしてください。大規模なリポジトリでは完全なクローンに時間がかかることがあります。", - "zh-CN": "如果任务需要完整的 Git 历史、标签、合并基准、历史 blame 信息,或签出较旧的提交,请开启此项。对于大型仓库,完整克隆可能需要更长时间。", - "zh-TW": "如果工作需要完整的 Git 歷史記錄、標籤、合併基準、歷史 blame 資訊,或簽出較舊的提交,請開啟此選項。大型儲存庫的完整複製可能需要更長時間。", - "ko-KR": "작업에 전체 Git 기록, 태그, 병합 기준, 과거 blame 정보 또는 이전 커밋 체크아웃이 필요한 경우 이 옵션을 켜세요. 대규모 리포지토리에서는 전체 클론에 시간이 더 오래 걸릴 수 있습니다.", - "de": "Aktivieren Sie dies, wenn Ihre Aufgaben den vollständigen Git-Verlauf, Tags, Merge-Bases, historische Blame-Informationen oder das Auschecken älterer Commits erfordern. Vollständige Klone können bei großen Repositorys länger dauern.", - "no": "Slå dette på hvis oppgavene dine krever full Git-historikk, tagger, merge-baser, historisk blame-informasjon eller utsjekking av eldre commits. Fullstendige kloner kan ta lengre tid for store repositorier.", - "it": "Attiva questa opzione se le tue attività richiedono la cronologia Git completa, tag, basi di merge, informazioni blame storiche o il checkout di commit precedenti. I cloni completi possono richiedere più tempo per repository di grandi dimensioni.", - "pt": "Ative isto se suas tarefas exigirem o histórico completo do Git, tags, bases de merge, informações históricas de blame ou checkout de commits mais antigos. Clones completos podem levar mais tempo em repositórios grandes.", - "es": "Activa esta opción si tus tareas requieren el historial completo de Git, etiquetas, bases de fusión, información histórica de blame o hacer checkout de commits anteriores. Los clones completos pueden tardar más en repositorios grandes.", - "ar": "فعّل هذا الخيار إذا كانت مهامك تتطلب سجل Git كاملًا، أو الوسوم، أو قواعد الدمج، أو معلومات blame التاريخية، أو التبديل إلى commits أقدم. قد تستغرق النسخ الكاملة وقتًا أطول للمستودعات الكبيرة.", - "fr": "Activez cette option si vos tâches nécessitent l’historique Git complet, les tags, les bases de fusion, les informations blame historiques ou l’extraction d’anciens commits. Les clones complets peuvent prendre plus de temps pour les grands dépôts.", - "tr": "Görevleriniz tam Git geçmişi, etiketler, birleştirme tabanları, geçmiş blame bilgisi veya eski commit’lere geçiş gerektiriyorsa bunu açın. Büyük depolarda tam klonlar daha uzun sürebilir.", - "uk": "Увімкніть це, якщо вашим завданням потрібні повна історія Git, теги, бази злиття, історична інформація blame або перехід на старіші коміти. Повні клони великих репозиторіїв можуть виконуватися довше.", - "ca": "Activeu aquesta opció si les vostres tasques requereixen l'historial complet de Git, etiquetes, bases de fusió, informació històrica de blame o fer checkout de commits antics. Els clons complets poden trigar més en repositoris grans." + "en": "Turn this on to download every file in the repository up front instead of on demand. This is useful when tasks need to work offline after cloning or run operations across the whole repository at once. It may take longer for large repositories.", + "ja": "オンにすると、リポジトリのすべてのファイルを必要に応じてではなく事前にダウンロードします。クローン後にオフラインで作業する必要がある場合や、リポジトリ全体に対する操作を一度に実行する場合に便利です。大規模なリポジトリでは時間がかかることがあります。", + "zh-CN": "开启后,将预先下载仓库中的所有文件,而不是按需下载。当任务需要在克隆后离线工作,或一次性对整个仓库执行操作时,这很有用。对于大型仓库可能需要更长时间。", + "zh-TW": "開啟後,將預先下載儲存庫中的所有檔案,而不是按需下載。當工作需要在複製後離線進行,或一次性對整個儲存庫執行操作時,這很有用。大型儲存庫可能需要更長時間。", + "ko-KR": "이 옵션을 켜면 필요할 때가 아니라 미리 리포지토리의 모든 파일을 다운로드합니다. 클론 후 오프라인으로 작업해야 하거나 리포지토리 전체에 대한 작업을 한 번에 실행할 때 유용합니다. 대규모 리포지토리에서는 시간이 더 오래 걸릴 수 있습니다.", + "de": "Aktivieren Sie dies, um alle Dateien des Repositorys im Voraus statt bei Bedarf herunterzuladen. Das ist nützlich, wenn Aufgaben nach dem Klonen offline arbeiten oder Vorgänge auf einmal über das gesamte Repository ausführen müssen. Bei großen Repositorys kann es länger dauern.", + "no": "Slå dette på for å laste ned alle filene i repositoriet på forhånd i stedet for ved behov. Dette er nyttig når oppgaver må jobbe offline etter kloning eller kjøre operasjoner på hele repositoriet på én gang. Det kan ta lengre tid for store repositorier.", + "it": "Attiva questa opzione per scaricare in anticipo tutti i file del repository anziché su richiesta. È utile quando le attività devono lavorare offline dopo la clonazione o eseguire operazioni sull'intero repository in una sola volta. Può richiedere più tempo per repository di grandi dimensioni.", + "pt": "Ative isto para baixar todos os arquivos do repositório antecipadamente em vez de sob demanda. Isso é útil quando as tarefas precisam trabalhar offline após a clonagem ou executar operações em todo o repositório de uma vez. Pode levar mais tempo em repositórios grandes.", + "es": "Activa esta opción para descargar todos los archivos del repositorio por adelantado en lugar de bajo demanda. Es útil cuando las tareas necesitan trabajar sin conexión después de clonar o ejecutar operaciones sobre todo el repositorio a la vez. Puede tardar más en repositorios grandes.", + "ar": "فعّل هذا الخيار لتنزيل جميع ملفات المستودع مسبقًا بدلًا من تنزيلها عند الحاجة. يكون ذلك مفيدًا عندما تحتاج المهام إلى العمل دون اتصال بعد الاستنساخ أو تنفيذ عمليات على المستودع بأكمله دفعة واحدة. قد يستغرق ذلك وقتًا أطول للمستودعات الكبيرة.", + "fr": "Activez cette option pour télécharger tous les fichiers du dépôt à l'avance plutôt qu'à la demande. C'est utile lorsque les tâches doivent travailler hors ligne après le clonage ou exécuter des opérations sur l'ensemble du dépôt en une seule fois. Cela peut prendre plus de temps pour les grands dépôts.", + "tr": "Depodaki tüm dosyaları istek üzerine değil önceden indirmek için bunu açın. Bu, görevlerin klonlamadan sonra çevrimdışı çalışması veya tüm depoda tek seferde işlem yapması gerektiğinde kullanışlıdır. Büyük depolarda daha uzun sürebilir.", + "uk": "Увімкніть це, щоб завантажити всі файли репозиторію заздалегідь, а не за потреби. Це корисно, коли завданням потрібно працювати офлайн після клонування або виконувати операції над усім репозиторієм одразу. Для великих репозиторіїв це може тривати довше.", + "ca": "Activeu aquesta opció per descarregar tots els fitxers del repositori per avançat en lloc de sota demanda. És útil quan les tasques han de treballar sense connexió després de clonar o executar operacions sobre tot el repositori alhora. Pot trigar més en repositoris grans." }, "SETTINGS$SOUND_NOTIFICATIONS": { "en": "Sound Notifications", diff --git a/openhands/app_server/app_conversation/app_conversation_service_base.py b/openhands/app_server/app_conversation/app_conversation_service_base.py index 9f23b34dc9..b7aeb5cc3b 100644 --- a/openhands/app_server/app_conversation/app_conversation_service_base.py +++ b/openhands/app_server/app_conversation/app_conversation_service_base.py @@ -394,9 +394,16 @@ class AppConversationServiceBase(AppConversationService, ABC): full_clone = bool(getattr(user_info, 'git_full_clone', False)) clone_flags = '' if not full_clone: - clone_flags = ' --depth 1' + # Partial ("blobless") clone: fetch the complete commit history so + # merges, rebases, and blame have a valid merge base, but defer + # downloading file contents until they are actually needed. This + # keeps startup fast for large repositories without the broken-merge + # problems of a shallow (--depth) clone. + clone_flags = ' --filter=blob:none' if request.selected_branch: - clone_flags += f' --branch {shlex.quote(request.selected_branch)}' + clone_flags += ( + f' --single-branch --branch {shlex.quote(request.selected_branch)}' + ) # Clone the repo - this is the slow part! if azure_devops_bearer_token: @@ -426,7 +433,7 @@ class AppConversationServiceBase(AppConversationService, ABC): # Checkout the appropriate branch if request.selected_branch: - # Needed for full clones; harmless no-op after shallow clones with --branch. + # Needed for full clones; harmless no-op after single-branch clones. checkout_command = f'git checkout {shlex.quote(request.selected_branch)}' else: # Generate a random branch name to avoid conflicts diff --git a/openhands/app_server/app_conversation/live_status_app_conversation_service.py b/openhands/app_server/app_conversation/live_status_app_conversation_service.py index c1a8e8224c..b552d7864d 100644 --- a/openhands/app_server/app_conversation/live_status_app_conversation_service.py +++ b/openhands/app_server/app_conversation/live_status_app_conversation_service.py @@ -204,8 +204,8 @@ After you finalize the plan in PLAN.md: Your role ends when the plan is finalized. Implementation is handled by the code agent. """ -GIT_SHALLOW_CLONE_CONTEXT = """ -The selected repository was cloned as a shallow clone. Git history may be incomplete. Before using operations that depend on full history, tags, merge bases, historical blame, or arbitrary commit checkout, run `git rev-parse --is-shallow-repository`. If full history is needed, run `git fetch --unshallow` or `git fetch --deepen=`. +GIT_PARTIAL_CLONE_CONTEXT = """ +The selected repository was cloned as a partial clone (`--filter=blob:none`). The complete commit history is present, so merges, rebases, tags, blame, and checking out older commits all work normally. File contents are downloaded on demand, so operations that touch many files or older revisions may pause briefly to fetch data and require network access to the remote. """ @@ -313,7 +313,7 @@ class LiveStatusAppConversationService(AppConversationServiceBase): ) return self._launch_snapshot_from_user(user) - def _maybe_append_shallow_clone_context( + def _maybe_append_partial_clone_context( self, user: UserInfo, selected_repository: str | None, @@ -321,7 +321,7 @@ class LiveStatusAppConversationService(AppConversationServiceBase): ) -> str | None: if selected_repository and not bool(getattr(user, 'git_full_clone', False)): return append_system_context( - system_message_suffix, GIT_SHALLOW_CLONE_CONTEXT + system_message_suffix, GIT_PARTIAL_CLONE_CONTEXT ) return system_message_suffix @@ -1788,7 +1788,7 @@ class LiveStatusAppConversationService(AppConversationServiceBase): ) secrets[name] = StaticSecret(value=value) - system_message_suffix = self._maybe_append_shallow_clone_context( + system_message_suffix = self._maybe_append_partial_clone_context( user, selected_repository, system_message_suffix ) @@ -2128,7 +2128,7 @@ class LiveStatusAppConversationService(AppConversationServiceBase): ) secrets[name] = StaticSecret(value=value) - system_message_suffix = self._maybe_append_shallow_clone_context( + system_message_suffix = self._maybe_append_partial_clone_context( user, selected_repository, system_message_suffix ) diff --git a/tests/unit/app_server/test_app_conversation_service_base.py b/tests/unit/app_server/test_app_conversation_service_base.py index 1919403634..feadc91deb 100644 --- a/tests/unit/app_server/test_app_conversation_service_base.py +++ b/tests/unit/app_server/test_app_conversation_service_base.py @@ -768,7 +768,7 @@ def mock_workspace(): @pytest.mark.asyncio -async def test_clone_or_init_git_repo_uses_shallow_clone_by_default(mock_workspace): +async def test_clone_or_init_git_repo_uses_partial_clone_by_default(mock_workspace): user_info = MockUserInfo() service, mock_user_context = _create_service_with_mock_user_context( user_info, @@ -792,14 +792,14 @@ async def test_clone_or_init_git_repo_uses_shallow_clone_by_default(mock_workspa await service.clone_or_init_git_repo(task, mock_workspace) mock_workspace.execute_command.assert_any_call( - 'git clone --depth 1 https://github.com/owner/repo.git repo', + 'git clone --filter=blob:none https://github.com/owner/repo.git repo', mock_workspace.working_dir, 120, ) @pytest.mark.asyncio -async def test_clone_or_init_git_repo_shallow_clones_selected_branch(mock_workspace): +async def test_clone_or_init_git_repo_partial_clones_selected_branch(mock_workspace): user_info = MockUserInfo() service, mock_user_context = _create_service_with_mock_user_context( user_info, @@ -823,7 +823,7 @@ async def test_clone_or_init_git_repo_shallow_clones_selected_branch(mock_worksp await service.clone_or_init_git_repo(task, mock_workspace) mock_workspace.execute_command.assert_any_call( - 'git clone --depth 1 --branch feature-branch https://github.com/owner/repo.git repo', + 'git clone --filter=blob:none --single-branch --branch feature-branch https://github.com/owner/repo.git repo', mock_workspace.working_dir, 120, ) @@ -861,7 +861,7 @@ async def test_clone_or_init_git_repo_preserves_full_clone_when_enabled( 120, ) commands = [call.args[0] for call in mock_workspace.execute_command.call_args_list] - assert not any('git clone --depth 1' in command for command in commands) + assert not any('--filter=blob:none' in command for command in commands) @pytest.mark.asyncio @@ -933,7 +933,7 @@ async def test_clone_or_init_git_repo_configures_dynamic_azure_devops_helper( commands = [call.args[0] for call in mock_workspace.execute_command.call_args_list] assert any( - "git -c http.extraheader='Authorization: Bearer header.payload.signature' clone --depth 1 --branch main" + "git -c http.extraheader='Authorization: Bearer header.payload.signature' clone --filter=blob:none --single-branch --branch main" in command for command in commands ) diff --git a/tests/unit/app_server/test_live_status_app_conversation_service.py b/tests/unit/app_server/test_live_status_app_conversation_service.py index ddc6b3a41e..f00b450d6f 100644 --- a/tests/unit/app_server/test_live_status_app_conversation_service.py +++ b/tests/unit/app_server/test_live_status_app_conversation_service.py @@ -1666,7 +1666,7 @@ class TestLiveStatusAppConversationService: return_value=[], ) @pytest.mark.asyncio - async def test_build_request_appends_shallow_clone_context(self, _mock_tools): + async def test_build_request_appends_partial_clone_context(self, _mock_tools): self.mock_user.git_full_clone = False self.mock_user_context.get_user_info.return_value = self.mock_user real_llm = LLM(model='gpt-4', api_key=SecretStr('test-key')) @@ -1686,14 +1686,14 @@ class TestLiveStatusAppConversationService: suffix = result.agent.agent_context.system_message_suffix assert 'Existing integration instructions.' in suffix assert '' in suffix - assert 'git fetch --unshallow' in suffix + assert 'partial clone' in suffix @patch( 'openhands.app_server.app_conversation.live_status_app_conversation_service.get_default_tools', return_value=[], ) @pytest.mark.asyncio - async def test_build_request_omits_shallow_clone_context_for_full_clone( + async def test_build_request_omits_partial_clone_context_for_full_clone( self, _mock_tools ): self.mock_user.git_full_clone = True