@@ -286,6 +286,28 @@ def _run_capture(command: list[str], *, cwd: Path, timeout: float = 10.0) -> sub
286286 )
287287
288288
289+ def _list_meaningful_worktree_changes (project_root : Path ) -> list [str ] | None :
290+ diff = _run_capture (["git" , "diff" , "--name-only" ], cwd = project_root )
291+ if diff .returncode != 0 :
292+ return None
293+
294+ changes : list [str ] = []
295+ for raw_path in diff .stdout .splitlines ():
296+ path = raw_path .strip ()
297+ if not path :
298+ continue
299+
300+ stats = _run_capture (["git" , "diff" , "--numstat" , "--" , path ], cwd = project_root )
301+ if stats .returncode != 0 :
302+ return None
303+
304+ # `git diff --numstat` is empty for mode-only changes, so treat those as safe.
305+ if stats .stdout .strip ():
306+ changes .append (path )
307+
308+ return changes
309+
310+
289311def _git_checkout_ready_for_update (project_root : Path ) -> tuple [bool , str | None ]:
290312 if not (project_root / ".git" ).exists ():
291313 return False , "This install is not a git checkout."
@@ -299,7 +321,14 @@ def _git_checkout_ready_for_update(project_root: Path) -> tuple[bool, str | None
299321 return False , f"Could not inspect git status: { detail } "
300322
301323 if status .stdout .strip ():
302- return False , "Working tree has local changes. Commit or stash them first."
324+ meaningful_changes = _list_meaningful_worktree_changes (project_root )
325+ if meaningful_changes is None :
326+ return False , "Could not determine whether local changes are content changes or mode-only changes."
327+ if meaningful_changes :
328+ names = ", " .join (meaningful_changes [:3 ])
329+ if len (meaningful_changes ) > 3 :
330+ names += ", ..."
331+ return False , f"Working tree has local content changes: { names } . Commit or stash them first."
303332
304333 return True , None
305334
0 commit comments