Допустим, в репозиторий по ошибке попал лишний блоб из сборки или файл с ключами, паролями или какая-нибудь еще чувствительная информация. Удалим его насовсем:
cd /path/to/repo
git filter-branch \
-d /tmp/filter-branch-xxx \
--original vanilla \
--index-filter "git rm -r --cached --ignore-unmatch unwanted_file.dat" \
--prune-empty \
HEAD
На стандартном выводе получим нечто подобное:
Rewrite 588408a0f4c7e3aa272ed58d1ebc4adc432add68 (1/3)rm 'unwanted_file.dat'
Rewrite 261533807bc8226804b9c7d7c8a67f62a8f861ac (2/3)rm 'unwanted_file.dat'
Rewrite 6c59972f8f7c2f9a60a730c38c187d6fcb1c38b8 (3/3)rm 'unwanted_file.dat'
...
Ref 'refs/heads/master' was rewritten
И master будет указывать на другой коммит. Старая история все еще будет доступна, но скорее всего, про нее нужно забыть. Поэтому начнем историю с чистого листа:
git clone file:///path/to/repo /path/to/clean/repo
А если код был на гитхабе, битбакете или гитлабе, то будет нужно завести новый репозиторий и запушить в него наш новый и чистый, предварительно предупредив всех заинтересованных.
Как это работает и почему можно всё сломать
Если вы еще не знаете, то переписывать историю нужно очень осторожно. Чем больше людей работает с кодом, тем больше нужно усилий, чтобы не сломать им рабочий процесс. Если вы не знаете, почему это так, то добро пожаловать дальше.
История в гите -- это направленный граф коммитов, и каждый коммит, начиная со второго, содержит изменения относительно предыдущего. От каждого коммита вычисляется контрольная сумма, а точнее, хэш SHA1 от состояния дерева файловой системы, хэша родительского коммита, имени и адреса автора коммита, даты авторства, имени и адреса коммитера, даты коммита и текста сообщения коммита (
краткое введение в тему).
Когда мы изменяем историю, мы точно изменяем что-то из списка выше, а значит, изменяем и хэши коммитов (случаи коллизий здесь не рассматриваем). Начиная с первого коммита, которого коснулись изменения, изменятся все хэши, и, в частности, это означает, что если мы получили репозиторий с помощью клона из гитхаба и переписали его историю, наши ветки, в том числе master, перестанут совпадать с ветками на гитхабе и в репозиториях у других участников. Попытка запушить такой master обратно в гитхаб в лучшем случае не сработает, но если быть настойчивым и переписать удаленный master, то это сломает pull-ы, fetch-и, rebase-ы и прочие сценарии всем остальным.
Что делать в этом случае? Предупредить всех заранее, чтобы можно было запушить все готовые ветки в гитхаб. Затем переименовать или удалить репозиторий и создать на его месте пустой, в который запушить переписанный. Если удаление репозитория -- не вариант, то нужно вспомнить, что прошлая история никуда не исчезает, она живет параллельно (возможно, garbage collector когда-то ее сотрет, но сейчас не об этом). Значит, ничто не мешает аккуратно запушить нашу новую, параллельную историю в гитхаб, переписать указатели веток и аккуратно же продолжить работу.
Что можно сделать еще
Файлы можно не только удалять, но и добавлять и изменять. Можно изменять структуру директорий (если это вам зачем-то нужно делать именно таким способом), можно изменять сообщения к коммитам, можно изменять авторов, словом -- изменять можно всё, что входит в коммит. Распространенный случай в такой узкой области, как переписывание истории, это избавление от метаинформации git-svn при завершении миграции из SVN в Git:
cd /tmp/large-file-test
git filter-branch \
-d /tmp/filter-branch-xxx \
--original vanilla \
--msg-filter 'sed -e "/^git-svn-id:/d"' \
--prune-empty \
HEAD
Здесь используется команда --msg-filter, которая является произвольным скриптом, принимающим на стандартный ввод исходное сообщение и кое-какие переменные окружения и выдающим на стандартный вывод новый текст сообщения.
Можно разделять проекты на отдельные репозитории, например, когда в разных поддиректориях в ходе развития проекта оказываются несвязанные кодом части. Тогда аккуратным использованием --index-filter сделать по репозиторию на каждый проект, чтобы история не содержала коммитов, не относящихся к делу.
Самый элегантный способ использования истории, не связанный непосредственно с переписыванием, но часто идущий рядом, это слияние проектов. Например, можно внести код библиотеки в репозиторий проекта или добавить подпроект, и всё это с сохранением истории. Допустим, мы добавляем проект B в проект A. Делается такая операция в три счета:
- Заводим ветку subdir в репозитории B, создаем в этой ветке директорию с подходящим названием и переносим в нее все файлы и директории при помощи git mv. Коммитим.
- Добавляем B как origin в репозиторий A.
- В репозитории A делаем git merge b/subdir. И вуаля, у нас в A есть директория с проектом B, для которой сохранилась вся история. Можно делать немного иначе и иметь возможность обновлять историю подпроекта.
Про git submodule я знаю, а также знаю, какая это неудобная поделка. При первой же возможности рекомендую от них
избавляться.
Что почитать
- Git изнутри -- глава книги Pro Git, в которой описывается, что происходит "за кулисами", когда мы выполняем "простые" команды commit, diff и т.п., и как сделать это на низком уровне. Например, чтобы реализовать git-flow, нужно активно использовать именно такие знания.
- man git-filter-branch. Самый незаменимый и самый непонятный источник информации о переписывании истории. Кажется, этот мануал невозможно понять, если не иметь представления о материале из [1] и не гуглить по StackOverflow. Именно здесь описываются команды --index-filter, --msg-filter и другие.