Глава 8. Гроссмейстерство Git

Теперь вы уже должны уметь ориентироваться в страницах git help и понимать почти всё. Однако точный выбор команды, необходимой для решения конкретной проблемы, может быть утомительным. Возможно, я сберегу вам немного времени: ниже приведены рецепты, пригодившиеся мне в прошлом.

Релизы исходников

В моих проектах Git управляет в точности теми файлами, которые я собираюсь архивировать и пускать в релиз. Чтобы создать тарбол с исходниками, я выполняю:

$ git archive --format=tar --prefix=proj-1.2.3/ HEAD

Коммит изменений

В некоторых проектах может быть трудоемко оповещать Git о каждом добавлении, удалении и переименовании файла. Вместо этого вы можете выполнить команды

$ git add .
$ git add -u

Git просмотрит файлы в текущем каталоге и сам позаботится о деталях. Вместо второй команды add, выполните git commit -a, если вы собираетесь сразу сделать коммит. Смотрите git help ignore, чтобы узнать как указать файлы, которые должны игнорироваться.

Вы можете выполнить все это одним махом:

$ git ls-files -d -m -o -z | xargs -0 git update-index --add --remove

Опции -z и -0 предотвращают неверную обработку файловых имен, содержащих специальные символы. Поскольку эта команда добавляет игнорируемые файлы, вы возможно захотите использовать опции -x или -X.

Мой коммит слишком велик

Вы пренебрегали коммитами слишком долго? Яростно писали код и вспомнили об управлении исходниками только сейчас? Внесли ряд несвязанных изменений, потому что это ваш стиль?

Нет поводов для беспокойства. Выполните

$ git add -p

Для каждой сделанной вами правки Git покажет измененный участок кода и спросит, должно ли это изменение попасть в следующий коммит. Отвечайте «y» (да) или «n» (нет). У вас есть и другие варианты, например отложить выбор; введите «?» чтобы узнать больше.

Когда закончите, выполните

$ git commit

для внесения именно тех правок, что вы выбрали («буферизованных» изменений). Убедитесь, что вы не указали опцию -a, иначе Git закоммитит все правки.

Что делать, если вы изменили множество файлов во многих местах? Проверка каждого отдельного изменения становится удручающей рутиной. В этом случае используйте git add -i. Ее интерфейс не так прост, но более гибок. В несколько нажатий кнопок можно добавить или убрать из буфера несколько файлов одновременно, либо просмотреть и выбрать изменения лишь в отдельных файлах. Как вариант, запустите git commit --interactive, которая автоматически сделает коммит когда вы закончите.

Индекс — буферная зона Git

До сих пор мы избегали знаменитого «индекса» Git, но теперь мы должны рассмотреть его, для пояснения вышесказанного. Индекс это временный буфер. Git редко перемещает данные непосредственно между вашим проектом и его историей. Вместо этого Git сначала записывает данные в индекс, а уж затем копирует их из индекса по месту назначения.

Например, commit -a на самом деле двухэтапный процесс. Сначала слепок текущего состояния каждого из отслеживаемых файлов помещается в индекс. Затем слепок, находящийся в индексе, записывается в историю. Коммит без опции -a выполняет только второй шаг, и имеет смысл только после выполнения команд, изменяющих индекс, таких как git add.

Обычно мы можем не обращать внимания на индекс и делать вид, что взаимодействуем напрямую с историей. Но в данном случае мы хотим более тонкого контроля, поэтому управляем индексом. Мы помещаем слепок некоторых (но не всех) наших изменений в индекс, после чего окончательно записываем этот аккуратно сформированный слепок.

Не теряй «головы»

Тег HEAD (англ. «голова», прим. пер.) — как курсор, который обычно указывает на последний коммит, продвигаясь с каждым новым коммитом. Некоторые команды Git позволяют перемещать этот курсор. Например,

$ git reset HEAD~3

переместит HEAD на три коммита назад. Теперь все команды Git будут работать так, как будто вы не делали последних трех коммитов, хотя файлы останутся в текущем состоянии. В справке описано несколько способов использования этого приема.

Но как вернуться назад в будущее? Ведь предыдущие коммиты о нем ничего не знают.

Если у вас есть SHA1 изначальной «головы», то:

$ git reset 1b6d

Но допустим, вы его не записывали. Не беспокойтесь: для комнад такого рода Git сохраняет оригинальную «голову» как тег под названием ORIG_HEAD, и вы можете вернуться надежно и безопасно:

$ git reset ORIG_HEAD

Охота за «головами»

Предположим ORIG_HEAD недостаточно. К примеру, вы только что осознали, что допустили громадную ошибку, и вам нужно вернуться к древнему коммиту в давно забытой ветке.

По умолчанию Git хранит коммиты не меньше двух недель, даже если вы приказали уничтожить содержащую их ветку. Проблема в нахождении соответствующего хеша. Вы можете просмотреть все значения хешей в .git/objects и методом проб и ошибок найти нужный. Но есть путь значительно легче.

Git записывает каждый подсчитанный им хеш коммита в .git/logs. В подкатлоге refs содержится полная история активности на всех ветках, а файл HEAD содержит каждое значение хеша, которое когда-либо принимал HEAD. Последнее можно использовать чтобы найти хеши коммитов на случайно обрубленных ветках.

Команда reflog предоставляет удобный интерфейс работы с этими журналами. Используйте

$ git reflog

Вместо копирования хешей из reflog, попробуйте

$ git checkout "@{10 minutes ago}" # 10 минут назад, прим. пер.

Или сделайте чекаут пятого с конца из посещенных коммитов с помощью

$ git checkout "@{5}"

Смотрите раздел «Specifying Revisions» в git help rev-parse для дополнительной информации.

Вы можете захотеть удлинить отсрочку для коммитов, обреченных на удаление. Например,

$ git config gc.pruneexpire "30 days"

означает, что удаляемые коммиты будут окончательно исчезать только по прошествии 30 дней и после запуска git gc.

Также вы можете захотеть отключить автоматический вызов git gc:

$ git config gc.auto 0

В этом случае коммиты будут удаляться только когда вы будете запускать git gc вручную.

Git как основа

Дизайн Git, в истинном духе UNIX, позволяет легко использовать его как низкоуровневый компонент других программ: графических и веб-интерфейсов; альтернативных интерфейсов командной строки; инструментов управления патчами; средств импорта или конвертации, и так далее. Многие команды Git на самом деле — скрипты, стоящие на плечах гигантов. Небольшой доработкой вы можете переделать Git на свой вкус.

Простейший трюк — использование алиасов Git для сокращения часто используемых команд:

$ git config --global alias.co checkout
$ git config --global --get-regexp alias       # отображает текущие алиасы
alias.co checkout
$ git co foo # то-же, что и «git checkout foo»

Другой пример: можно выводить текущую ветку в приглашении командной строки или заголовке окна терминала. Запуск

$ git symbolic-ref HEAD

выводит название текущей ветки. На практике вы скорее всего захотите убрать «refs/heads/» и сообщения об ошибках:

$ git symbolic-ref HEAD 2> /dev/null | cut -b 12-

Подкаталог contrib это целая сокровищница инструментов, построенных на Git. Со временем некоторые из них могут становиться официальными командами. В Debian и Ubuntu этот каталог находится в /usr/share/doc/git-core/contrib.

Один популярный инструмент из этого каталога — workdir/git-new-workdir. Этот скрипт создает с помощью символических ссылок новый рабочий каталог, имеющий общую историю с оригинальным хранилищем:

$ git-new-workdir существующее/хранилище новый/каталог

Новый каталог и файлы в нем можно воспринимать как клон, с той разницей, что два дерева автоматически остаются синхронизированными ввиду общей истории. Нет необходимости в merge, push и pull.

Рискованные трюки

Нынешний Git делает случайное уничтожение данных очень сложным. Но если вы знаете, что делаете, вы можете обойти защиту для распространенных команд.

Checkout: Наличие незакоммиченных изменений прерывает выполнение checkout. Чтобы перейти к нужному коммиту, даже уничтожив свои изменения, используйте «принуждающий» (force, прим. пер.) флаг -f:

$ git checkout -f HEAD^

С другой стороны, если вы укажете checkout конкретные пути, проверки на безопасность не будет: указанные файлы молча перезапишутся. Будьте осторожны при таком использовании checkout.

Reset: сброс также прерывается при наличии незакоммиченных изменений. Чтобы заставить его сработать, запустите

$ git reset --hard 1b6d

Branch: Удаление ветки прервётся, если оно привело бы к потере изменений. Для принудительного удаления введите

$ git branch -D мертвая_ветка # вместо -d

Аналогично, попытка перезаписи ветки путем перемещения будет прервана, если может привести к потере данных. Для принудительного перемещений ветки введите

$ git branch -M источник цель # вместо -m

В отличии от checkout и reset, эти две команды дают отсрочку в удалении данных. Изменения остаются в каталоге .git и могут быть возвращены восстановлением нужного хеша из .git/logs (смотрите выше раздел «Охота за „головами“»). По умолчанию они будут храниться по крайней мере две недели.

Clean: Некоторые команды могут не сработать из опасений повредить неотслеживаемые файлы. Если вы уверены, что все неотслеживаемые файлы и каталоги не нужны, то безжалостно удаляйте их командой

$ git clean -f -d

В следующий раз эта досадная команда сработает!

Предотвращаем плохие коммиты

Глупые ошибки загрязняют мои хранилища. Самое ужасное это проблема недостающих файлов, вызванная забытым git add.

Примеры менее серьезных проступков: завершающие пробелы и неразрешённые конфликты слияния. Несмотря на безвредность, я не хотел бы, чтобы это появлялось в публичных записях.

Если бы я только поставил защиту от дурака, используя хук, который бы предупреждал меня об этих проблемах:

$ cd .git/hooks
$ cp pre-commit.sample pre-commit # В старых версиях Git: chmod +x pre-commit

Теперь Git отменит коммит, если обнаружит лишние пробелы или неразрешенные конфликты.

Для этого руководства я в конце концов добавил следующее в начало хука pre-commit, чтобы защититься от своей рассеянности:

if git ls-files -o | grep \.txt$; then echo ПРЕРВАНО! Неотслеживаемые .txt файлы. exit 1 fi

Хуки поддерживаются несколькими различными операциями Git, смотрите git help hooks. Мы использовали пример хука post-update раньше, при обсуждении использования Git через http. Он запускался при каждом перемещении «головы». Пример скрипта post-update обновляет файлы, которые нужны Git для связи через не считающиеся с ним средства сообщения, такие как HTTP.