== Гроссмейстерство 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.
