upload: Suggest full sync if hooks fail with partially synced tree

Pre-upload hooks may fail because of partial syncs.

Bug: b/271507654
Change-Id: I124cd386c5af2c34e1dcaa3e86916624e235b1e3
Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/383474
Reviewed-by: Mike Frysinger <vapier@google.com>
Commit-Queue: Gavin Mak <gavinmak@google.com>
Tested-by: Gavin Mak <gavinmak@google.com>
diff --git a/subcmds/sync.py b/subcmds/sync.py
index bbe0372..159771e 100644
--- a/subcmds/sync.py
+++ b/subcmds/sync.py
@@ -1696,7 +1696,7 @@
         )
 
         self._fetch_times = _FetchTimes(manifest)
-        self._local_sync_state = _LocalSyncState(manifest)
+        self._local_sync_state = LocalSyncState(manifest)
         if not opt.local_only:
             with multiprocessing.Manager() as manager:
                 with ssh.ProxyManager(manager) as ssh_proxy:
@@ -1932,7 +1932,7 @@
             platform_utils.remove(self._path, missing_ok=True)
 
 
-class _LocalSyncState(object):
+class LocalSyncState(object):
     _LAST_FETCH = "last_fetch"
     _LAST_CHECKOUT = "last_checkout"
 
diff --git a/subcmds/upload.py b/subcmds/upload.py
index 040eaeb..b89525c 100644
--- a/subcmds/upload.py
+++ b/subcmds/upload.py
@@ -26,6 +26,7 @@
 from git_refs import R_HEADS
 from hooks import RepoHook
 from project import ReviewableBranch
+from subcmds.sync import LocalSyncState
 
 
 _DEFAULT_UNUSUAL_COMMIT_THRESHOLD = 5
@@ -804,6 +805,12 @@
             if not hook.Run(
                 project_list=pending_proj_names, worktree_list=pending_worktrees
             ):
+                if LocalSyncState(manifest).IsPartiallySynced():
+                    print(
+                        "Partially synced tree detected. Syncing all projects "
+                        "may resolve issues you're seeing.",
+                        file=sys.stderr,
+                    )
                 ret = 1
         if ret:
             return ret
diff --git a/tests/test_subcmds_sync.py b/tests/test_subcmds_sync.py
index b43fe18..71b0f8e 100644
--- a/tests/test_subcmds_sync.py
+++ b/tests/test_subcmds_sync.py
@@ -110,7 +110,7 @@
 
 
 class LocalSyncState(unittest.TestCase):
-    """Tests for _LocalSyncState."""
+    """Tests for LocalSyncState."""
 
     _TIME = 10
 
@@ -129,7 +129,7 @@
 
     def _new_state(self, time=_TIME):
         with mock.patch("time.time", return_value=time):
-            return sync._LocalSyncState(self.manifest)
+            return sync.LocalSyncState(self.manifest)
 
     def test_set(self):
         """Times are set."""