Start Debugging

Исправление: flutter upgrade сообщает "Your flutter checkout has local changes that would be erased by upgrading"

flutter upgrade прерывается, если git status в папке SDK что-то выводит. Посмотрите, что там, восстановите pubspec.lock или выполните git stash -u, а --force используйте только в крайнем случае.

flutter upgrade запускает git status -s внутри папки Flutter SDK и отказывается продолжать, если выведена хотя бы одна строка, в том числе про неотслеживаемые файлы, которые вы не собирались хранить. Найдите SDK командой flutter --version --machine (поле flutterRoot), выполните там git status --short, а затем либо восстановите файл (обычно pubspec.lock), либо отложите свои правки через git stash push -u, либо, если в SDK для вас ничего не важно, запустите flutter upgrade --force. Всё ниже проверено на Flutter 3.44.8 и 3.47.7/3.47.8 (stable, Dart 3.12.2) 2026-10-29.

Ошибка в контексте

Вы запускаете flutter upgrade на машине, где точно не трогали сам Flutter, и получаете вот это:

$ flutter upgrade
Your flutter checkout has local changes that would be erased by upgrading. If you want to keep these changes, it is recommended that you stash them via "git stash" or else commit the changes to a local branch. If it is okay to remove local changes, then re-run this command with "--force".

Код выхода равен 1, ничего не изменилось. Сообщение относится к git-репозиторию SDK, а не к репозиторию вашего приложения. git status в вашем приложении может быть совершенно чистым, а ошибка всё равно появится.

Почему это происходит

Flutter SDK представляет собой git-клон flutter/flutter, а flutter upgrade по сути выполняет git fetch и git reset --hard <new revision> с несколькими проверками безопасности вокруг. Нужный код находится в packages/flutter_tools/lib/src/commands/upgrade.dart, и порядок действий имеет значение:

  1. git fetch --tags, затем определяется upstream-ревизия вашей ветки (stable, beta, main).
  2. Если вы уже на этой ревизии, выводится “Flutter is already up to date” и команда завершается с кодом 0. Проверка локальных изменений не выполняется.
  3. Если передан --verify-only, выводится доступная версия и команда завершается.
  4. Если у текущего коммита нет узнаваемого тега релиза и вы не передали --force, команда прерывается с сообщением “Unknown flutter tag”.
  5. Если вы не передали --force и hasUncommittedChanges() возвращает true, команда прерывается с ошибкой выше.
  6. git reset --hard <upstream revision>, затем новый инструмент перезапускается через flutter upgrade --continue, чтобы заранее загрузить артефакты, выполнить pub upgrade в вашем проекте и запустить flutter doctor.

hasUncommittedChanges() является тонкой обёрткой над git status -s. В канале stable любой непустой вывод считается изменением. Начиная с Flutter 3.47.0 (PR #184997) каналы beta и main пропускают строки, оканчивающиеся на pubspec.lock, потому что main сам записывает в этот файл ограничения dev SDK. Для stable такого исключения нет.

Из использования git status -s напрямую следуют два вывода:

Поэтому настоящий вопрос звучит не как “есть ли у меня локальные изменения”, а как “что выводит git status --short в папке SDK и кто это туда положил”.

Минимальное воспроизведение

Я воспроизвёл каждый случай на отдельном клоне flutter/flutter, сброшенном на тег 3.47.7, то есть на один релиз позади stable (3.47.8). Чтобы не запускать только что клонированный код, я вызывал логику обновления из установленного Flutter 3.44.8 через скрытый флаг инструмента --working-directory, который направляет команду на другой чекаут и пропускает повторно входящую вторую половину. Проверка для канала stable побайтово совпадает в 3.44.8 и 3.47.8.

# Flutter 3.44.8 tool, scratch checkout at 3.47.7, upstream stable = 3.47.8
C=~/scratch/flutter
git -C "$C" reset -q --hard 3.47.7

echo hi > "$C/notes.txt"                       # untracked file only
flutter upgrade --working-directory="$C"
# Your flutter checkout has local changes that would be erased by upgrading. ...
# exit=1

Вот что получилось в каждом сценарии:

Состояние чекаута SDKgit status -sРезультат flutter upgrade
Чистый, на один релиз позади(пусто)Обновляется
Неотслеживаемый notes.txt?? notes.txtЗаблокировано
Файлы в .idea/, .pub-cache/, debug.log(пусто, видно только с --ignored)Не заблокировано
Изменённый корневой pubspec.lock M pubspec.lockЗаблокировано
Изменён исходный код фреймворка + неотслеживаемый файл, --force M packages/flutter/..., ?? notes.txtОбновляется; правка фреймворка пропала, notes.txt остался
Уже на 3.47.8 и есть ?? notes.txt?? notes.txt”Flutter is already up to date”, код выхода 0

Последняя строка объясняет запутанную картину: лишний файл месяцами лежит без жалоб, а ошибка появляется только в день релиза, когда наконец есть куда обновляться.

Самая частая причина: собственный pubspec.lock в SDK

Если git status --short показывает M pubspec.lock и больше ничего, значит Flutter изменил собственный lock-файл. Репозиторий Flutter является рабочим пространством pub (pub workspace), и его корневой pubspec.lock остаётся единственным отслеживаемым lock-файлом в дереве. Всё, что запускает pub upgrade с корнем SDK в качестве проекта, перезаписывает его, потому что пакеты, опубликованные после релиза (например, более новый objective_c), удовлетворяют тем же ограничениям.

Вплоть до Flutter 3.44.x включительно flutter upgrade делал это сам с собой. Повторно входящий flutter upgrade --continue запускался с корнем SDK в качестве рабочего каталога, поэтому шаг “обновить пакеты вашего проекта” вызывал findProjectRoot(), который поднимается вверх от текущего каталога в поисках pubspec.yaml, находил pubspec.yaml самого SDK и запускал на нём pub upgrade. Обновление проходило успешно, оставляло изменённый lock-файл, и следующее обновление падало. Это issue #187977, и оно совпадает с сообщениями вида “это происходит только на установках, которые уже обновлялись один раз”.

Дрейф можно увидеть самостоятельно. На чистом чекауте тега 3.44.0 запуск pub upgrade в корне SDK с Dart 3.12.2 (линейка Dart, которая поставляется с 3.44) перезаписал lock-файл (14 добавлений, 6 удалений):

# Flutter 3.44.0 checkout, Dart 3.12.2, 2026-10-29
cd ~/scratch/flutter && dart pub upgrade
git diff --stat
#  pubspec.lock | 20 ++++++++++++++------
       name: jni
-    version: "1.0.0"
+    version: "1.1.1"
       name: jni_flutter
-    version: "1.0.1"
+    version: "1.0.4+1"
       name: objective_c
-    version: "9.2.3"
+    version: "9.3.0"

objective_c с 9.2.3 до 9.3.0 это точно тот же diff, что приведён в отчёте об ошибке. PR #188794 исправил проблему: вторая половина теперь запускается в вашем исходном рабочем каталоге, и исправление вошло во Flutter 3.47.0. Так что если у вас 3.44.x, ожидайте эту ошибку ещё один раз на пути к 3.47, а затем она должна прекратиться.

У этого исправления есть подвох. Вторая половина теперь запускает pub upgrade для того проекта, который findProjectRoot() находит от вашего текущего каталога, а эта функция поднимается вверх. Если запустить flutter upgrade изнутри дерева SDK, например из ~/development/flutter/bin, она снова найдёт pubspec.yaml SDK и испортит lock-файл и на 3.47. Запускайте обновления из домашнего каталога или из папки приложения, но никогда изнутри SDK. (Запуск из папки приложения обновляет зависимости этого приложения, это отдельная тема, она разобрана в статье почему pubspec.lock меняется после flutter upgrade.)

Исправление подробно

1. Найдите SDK и посмотрите, что там, прежде чем применять force

Не хватайтесь за --force первым делом. Сначала выясните, что именно там лежит.

# Flutter 3.44+ on macOS / Linux (needs jq)
SDK="$(flutter --version --machine | jq -r .flutterRoot)"
git -C "$SDK" status --short
git -C "$SDK" diff --stat
# Flutter 3.44+ on Windows PowerShell
$sdk = (flutter --version --machine | ConvertFrom-Json).flutterRoot
git -C $sdk status --short
git -C $sdk diff --stat

flutter doctor -v выводит тот же путь в строке “Flutter version X on channel Y at ”, если вы не хотите разбирать JSON. Для установки через Homebrew cask flutterRoot указывает на реальный чекаут (/opt/homebrew/share/flutter на моей машине), а не на символическую ссылку bin/flutter.

2a. Изменён только pubspec.lock: восстановите его

Это описанный выше случай, когда файл записал сам Flutter. Файл всё равно будет сгенерирован заново, поэтому отмените изменение и обновитесь как обычно:

# Flutter 3.44.x -> 3.47.x, stable channel
git -C "$SDK" checkout -- pubspec.lock
cd ~ && flutter upgrade

Запуск обновления из ~ не даёт второй половине найти pubspec.yaml внутри SDK.

2b. Вы намеренно изменили файлы фреймворка: отложите их в stash

Возможно, вы поправили виджет, гоняясь за багом, или добавили debugPrint внутри packages/flutter. Если эти правки нужны вам после обновления, отложите их в stash вместе с неотслеживаемыми файлами:

# Flutter 3.47.7 -> 3.47.8, verified 2026-10-29
git -C "$SDK" stash push -u -m "before flutter upgrade"
flutter upgrade
git -C "$SDK" stash pop

В моём воспроизведении обновление до 3.47.8 прошло чисто, а stash pop вернул и изменённый text.dart, и неотслеживаемый notes.txt. Если файлы, которые вы трогали, изменились в upstream между двумя версиями, stash pop может дать конфликт, и это честный результат: ваш патч писался под другую версию фреймворка.

Если у вас долгоживущие патчи к фреймворку, stash не подходит. Закоммитьте их в локальную ветку или, ещё лучше, ведите форк и подключайте к нему SDK конкретного проекта через менеджер версий, чтобы обновляемый SDK не совпадал с тем, который вы патчите.

2c. Ничего из лежащего там не важно: --force

Когда git status --short показывает только мусор, который вам не нужен, используйте force:

# Flutter 3.47.x, stable channel
flutter upgrade --force    # or: flutter upgrade -f

Нужно понимать, что делает --force, потому что это не то, что многие думают:

Чтобы окончательно убрать неотслеживаемые файлы, сначала посмотрите, что будет удалено, а затем удалите:

# Run inside the SDK folder; -n is a dry run
git -C "$SDK" clean -n -d
git -C "$SDK" clean -f -d

git clean удаляет файлы безвозвратно и учитывает .gitignore, поэтому bin/cache/ и .pub-cache/ он не тронет. Прочитайте вывод с -n, прежде чем запускать настоящую команду.

3. Как не допустить повторения

Подводные камни и похожие ошибки

“Unknown flutter tag. Abandoning upgrade to avoid destroying local changes.” Другая проверка, то же семейство. Срабатывает, когда git describe не находит тег релиза рядом с вашим коммитом, обычно после поверхностного клона без тегов или после перехода на произвольный коммит. В официальном канале сообщение советует использовать git напрямую, в остальных случаях предлагает --force. Перед force загрузите теги (git -C "$SDK" fetch --tags).

“Your Flutter checkout is currently not on a release branch.” Вы находитесь в состоянии detached HEAD, обычно потому, что последовали совету из документации выполнить git checkout <version> для закрепления версии. Выполните flutter channel stable, чтобы вернуться на ветку, и затем обновляйтесь.

“The current Flutter branch/channel is not tracking any remote repository.” У вашей локальной ветки нет upstream. Документированное решение это переустановка; также подойдёт git -C "$SDK" branch -u origin/stable, если origin является официальным удалённым репозиторием.

Шум из-за режимов файлов или переводов строк. Если git diff в SDK показывает только строки old mode 100755 / new mode 100644, что-то (chmod -R, копирование между файловыми системами, синхронизируемая папка) изменило права доступа. Если в Windows каждый файл отображается как изменённый, чекаут скопировали с другими окончаниями строк. В обоих случаях ничего важного не изменилось; правильный выход это git -C "$SDK" checkout -- . или --force. В .gitattributes SDK используется * text=auto, поэтому свежий клон остаётся самым чистым выходом, если копия сильно испорчена.

Ошибка появляется в CI. Если образ CI кеширует SDK между запусками и предыдущее задание выполнило flutter upgrade на SDK 3.44.x (или изнутри дерева SDK), кешированный чекаут несёт изменённый lock-файл, и каждый последующий шаг обновления падает. Фиксируйте версию вместо обновления в CI; см. как нацелиться на несколько версий Flutter из одного конвейера CI.

flutter upgrade отработал, но ваш проект теперь не разрешает зависимости. Это вторая половина запустила pub upgrade для вашего приложения с новыми закреплёнными версиями SDK, а не эта ошибка. Начните со статьи исправление “version solving failed” в pubspec.yaml.

Связанные материалы

Источники

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< Назад