sync: Don't checkout if no worktree

Interleaved sync should not try checkout out a project if it's a mirror.

Change-Id: I2549faab197a3202d79a10e44b449b68d53e3fe7
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/492942
Commit-Queue: Gavin Mak <gavinmak@google.com>
Reviewed-by: Scott Lee <ddoman@google.com>
Tested-by: Gavin Mak <gavinmak@google.com>
diff --git a/subcmds/sync.py b/subcmds/sync.py
index b02fdd0..c0310c5 100644
--- a/subcmds/sync.py
+++ b/subcmds/sync.py
@@ -2269,51 +2269,57 @@
         checkout_finish = None
         checkout_stderr = ""
 
-        if fetch_success and not opt.network_only:
-            checkout_start = time.time()
-            stderr_capture = io.StringIO()
-            try:
-                with contextlib.redirect_stderr(stderr_capture):
-                    syncbuf = SyncBuffer(
-                        project.manifest.manifestProject.config,
-                        detach_head=opt.detach_head,
-                    )
-                    local_half_errors = []
-                    project.Sync_LocalHalf(
-                        syncbuf,
-                        force_sync=opt.force_sync,
-                        force_checkout=opt.force_checkout,
-                        force_rebase=opt.rebase,
-                        errors=local_half_errors,
-                        verbose=opt.verbose,
-                    )
-                    checkout_success = syncbuf.Finish()
-                    if local_half_errors:
-                        checkout_error = SyncError(
-                            aggregate_errors=local_half_errors
+        if fetch_success:
+            # We skip checkout if it's network-only or if the project has no
+            # working tree (e.g., a mirror).
+            if opt.network_only or not project.worktree:
+                checkout_success = True
+            else:
+                # This is a normal project that needs a checkout.
+                checkout_start = time.time()
+                stderr_capture = io.StringIO()
+                try:
+                    with contextlib.redirect_stderr(stderr_capture):
+                        syncbuf = SyncBuffer(
+                            project.manifest.manifestProject.config,
+                            detach_head=opt.detach_head,
                         )
-            except KeyboardInterrupt:
-                logger.error(
-                    "Keyboard interrupt while processing %s", project.name
-                )
-            except GitError as e:
-                checkout_error = e
-                logger.error(
-                    "error.GitError: Cannot checkout %s: %s", project.name, e
-                )
-            except Exception as e:
-                checkout_error = e
-                logger.error(
-                    "error: Cannot checkout %s: %s: %s",
-                    project.name,
-                    type(e).__name__,
-                    e,
-                )
-            finally:
-                checkout_finish = time.time()
-                checkout_stderr = stderr_capture.getvalue()
-        elif fetch_success:
-            checkout_success = True
+                        local_half_errors = []
+                        project.Sync_LocalHalf(
+                            syncbuf,
+                            force_sync=opt.force_sync,
+                            force_checkout=opt.force_checkout,
+                            force_rebase=opt.rebase,
+                            errors=local_half_errors,
+                            verbose=opt.verbose,
+                        )
+                        checkout_success = syncbuf.Finish()
+                        if local_half_errors:
+                            checkout_error = SyncError(
+                                aggregate_errors=local_half_errors
+                            )
+                except KeyboardInterrupt:
+                    logger.error(
+                        "Keyboard interrupt while processing %s", project.name
+                    )
+                except GitError as e:
+                    checkout_error = e
+                    logger.error(
+                        "error.GitError: Cannot checkout %s: %s",
+                        project.name,
+                        e,
+                    )
+                except Exception as e:
+                    checkout_error = e
+                    logger.error(
+                        "error: Cannot checkout %s: %s: %s",
+                        project.name,
+                        type(e).__name__,
+                        e,
+                    )
+                finally:
+                    checkout_finish = time.time()
+                    checkout_stderr = stderr_capture.getvalue()
 
         # Consolidate all captured output.
         captured_parts = []
diff --git a/tests/test_subcmds_sync.py b/tests/test_subcmds_sync.py
index 9cd19f1..5955e40 100644
--- a/tests/test_subcmds_sync.py
+++ b/tests/test_subcmds_sync.py
@@ -309,6 +309,7 @@
         self.relpath = relpath
         self.name = name or relpath
         self.objdir = objdir or relpath
+        self.worktree = relpath
 
         self.use_git_worktrees = False
         self.UseAlternates = False
@@ -836,6 +837,25 @@
         project.Sync_NetworkHalf.assert_called_once()
         project.Sync_LocalHalf.assert_not_called()
 
+    def test_worker_no_worktree(self):
+        """Test interleaved sync does not checkout with no worktree."""
+        opt = self._get_opts()
+        project = self.projA
+        project.worktree = None
+        project.Sync_NetworkHalf = mock.Mock(
+            return_value=SyncNetworkHalfResult(error=None, remote_fetched=True)
+        )
+        project.Sync_LocalHalf = mock.Mock()
+        self.mock_context["projects"] = [project]
+
+        result_obj = self.cmd._SyncProjectList(opt, [0])
+        result = result_obj.results[0]
+
+        self.assertTrue(result.fetch_success)
+        self.assertTrue(result.checkout_success)
+        project.Sync_NetworkHalf.assert_called_once()
+        project.Sync_LocalHalf.assert_not_called()
+
     def test_worker_fetch_fails_exception(self):
         """Test _SyncProjectList with an exception during fetch."""
         opt = self._get_opts()