# Инструменты Git # К этому времени вы уже изучили большинство повседневных команд и способы организации рабочего процесса, необходимые для того, чтобы поддерживать Git-репозиторий для контроля вашего исходного кода. Вы выполнили основные задания, связанные с добавлением файлов под версионный контроль и записью сделанных изменений, и вы вооружились мощью подготовительной области (staging area), легковесного ветвления и слияния. Сейчас вы познакомитесь с множеством весьма сильных возможностей Git'а. Вы совсем не обязательно будете использовать их каждый день, но, возможно, в какой-то момент они вам понадобятся. ## Выбор ревизии ## Git позволяет указывать конкретные коммиты или их последовательности несколькими способами. Они не всегда очевидны, но иногда их полезно знать. ### Одиночные ревизии ### Вы можете просто сослаться на коммит по его SHA-1 хешу, но также существуют более понятные для человека способы ссылаться на коммиты. В этом разделе кратко описаны различные способы обратиться к одному определённому коммиту. ### Сокращённый SHA ### Git достаточно умён для того, чтобы понять какой коммит вы имеете в виду по первым нескольким символам (частичному хешу), конечно, если их не меньше четырёх и они однозначны, то есть если хеш только одного объекта в вашем репозитории начинается с этих символов. Предположим, что вы хотите посмотреть содержимое какого-то конкретного коммита. Вы выполняете команду `git log` и находите этот коммит (например, тот, в котором вы добавили какую-то функциональность): $ git log commit 734713bc047d87bf7eac9674765ae793478c50d3 Author: Scott Chacon Date: Fri Jan 2 18:32:33 2009 -0800 fixed refs handling, added gc auto, updated tests commit d921970aadf03b3cf0e71becdaab3147ba71cdef Merge: 1c002dd... 35cfb2b... Author: Scott Chacon Date: Thu Dec 11 15:08:43 2008 -0800 Merge commit 'phedders/rdocs' commit 1c002dd4b536e7479fe34593e72e6c6c1819e53b Author: Scott Chacon Date: Thu Dec 11 14:58:32 2008 -0800 added some blame and merge stuff В нашем случае выберем коммит `1c002dd....`. При использовании `git show` для просмотра содержимого этого коммита следующие команды эквивалентны (предполагая, что сокращённые версии однозначны): $ git show 1c002dd4b536e7479fe34593e72e6c6c1819e53b $ git show 1c002dd4b536e7479f $ git show 1c002d Git может показать короткие уникальные сокращения ваших SHA-1 хешей. Если вы передадите опцию `--abbrev-commit` команде `git log`, то её вывод будет использовать сокращённые значения, сохраняя их уникальными; по умолчанию будут использоваться семь символов, но при необходимости длина будет увеличена для сохранения однозначности хешей: $ git log --abbrev-commit --pretty=oneline ca82a6d changed the version number 085bb3b removed unnecessary test code a11bef0 first commit В общем случае восемь-десять символов более чем достаточно для уникальности внутри проекта. В одном из самых больших проектов, использующих Git, ядре Linux только начинает появляться необходимость использовать 12 символов из 40 возможных для сохранения уникальности. ### Небольшое замечание о SHA-1 ### Многие люди интересуются, что произойдёт, если они в какой-то момент, по некоторой случайности, получат два объекта в репозитории, которые будут иметь два одинаковых значения SHA-1 хеша. Что тогда? Если вы вдруг закоммитите объект, SHA-1 хеш которого такой же, как у некоторого предыдущего объекта в вашем репозитории, Git обнаружит предыдущий объект в своей базе данных и посчитает, что он уже был записан. Если вы в какой-то момент попытаетесь получить этот объект опять, вы всегда будете получать данные первого объекта. Однако, вы должны осознавать то, как смехотворно маловероятен этот сценарий. Длина SHA-1 составляет 20 байт или 160 бит. Количество случайно хешированных объектов, необходимое для того, чтобы получить 50% вероятность одиночного совпадения составляет порядка 2^80 (формула для определения вероятности совпадения: `p = (n(n-1)/2) * (1/2^160)`)). 2^80 это 1.2 x 10^24 или один миллион миллиарда миллиардов. Это в 1200 раз больше количества песчинок на земле. Вот пример для того, чтобы вы поняли, что необходимо, чтобы получить SHA-1 коллизию. Если бы все 6.5 миллиардов людей на Земле программировали и каждую секунду каждый из них производил количество кода, эквивалентное всей истории ядра Linux (1 миллион Git-объектов) и отправлял его в один огромный Git-репозиторий, то потребовалось бы 5 лет для того, чтобы заполнить репозиторий достаточно для того, чтобы получить 50% вероятность единичной SHA-1 коллизии. Более вероятно, что каждый член вашей команды программистов будет атакован и убит волками в несвязанных друг с другом случаях в одну и ту же ночь. ### Ссылки на ветки ### Для самого прямого метода указать коммит необходимо, чтобы этот коммит имел ветку, ссылающуюся на него. Тогда вы можете использовать имя ветки в любой команде Git'а, которая ожидает коммит или значение SHA-1. Например, если вы хотите посмотреть последний коммит в ветке, следующие команды эквивалентны, предполагая, что ветка `topic1` ссылается на `ca82a6d`: $ git show ca82a6dff817ec66f44342007202690a93763949 $ git show topic1 Чтобы посмотреть, на какой именно SHA указывает ветка, или понять для какого-то из приведённых примеров, к каким SHA он сводится, можно использовать служебную (plumbing) утилиту Git'а, которая называется `rev-parse`. Вы можете заглянуть в главу 9 для получения более подробной информации о служебных утилитах; в основном `rev-parse` нужна для выполнения низкоуровневых операций и не предназначена для использования в повседневной работе. Однако, она может пригодиться, если вам необходимо разобраться, что происходит на самом деле. Сейчас вы можете попробовать применить `rev-parse` к своей ветке. $ git rev-parse topic1 ca82a6dff817ec66f44342007202690a93763949 ### RefLog-сокращения ### Одна из вещей, которую Git делает в фоновом режиме, пока вы работаете, это запоминание ссылочного лога — лога того, где находились HEAD и ветки в течение последних нескольких месяцев. Ссылочный лог можно просмотреть с помощью `git reflog`: $ git reflog 734713b... HEAD@{0}: commit: fixed refs handling, added gc auto, updated d921970... HEAD@{1}: merge phedders/rdocs: Merge made by recursive. 1c002dd... HEAD@{2}: commit: added some blame and merge stuff 1c36188... HEAD@{3}: rebase -i (squash): updating HEAD 95df984... HEAD@{4}: commit: # This is a combination of two commits. 1c36188... HEAD@{5}: rebase -i (squash): updating HEAD 7e05da5... HEAD@{6}: rebase -i (pick): updating HEAD Каждый раз, когда верхушка ветки обновляется по какой-либо причине, Git сохраняет информацию об этом в эту временную историю. И вы можете использовать и эти данные для задания старых коммитов. Если вы хотите посмотреть, какое значение было у HEAD в вашем репозитории пять шагов назад, используйте ссылку вида `@{n}` так же, как показано в выводе команды `reflog`: $ git show HEAD@{5} Также вы можете использовать эту команду, чтобы увидеть, где ветка была некоторое время назад. Например, чтобы увидеть, где была ветка `master` вчера, наберите $ git show master@{yesterday} Эта команда покажет, где верхушка ветки находилась вчера. Такой подход работает только для данных, которые всё ещё находятся в ссылочном логе. Так что вы не сможете использовать его для коммитов с давностью в несколько месяцев. Чтобы просмотреть информацию ссылочного лога в таком же формате, как вывод `git log`, можно выполнить `git log -g`: $ git log -g master commit 734713bc047d87bf7eac9674765ae793478c50d3 Reflog: master@{0} (Scott Chacon ) Reflog message: commit: fixed refs handling, added gc auto, updated Author: Scott Chacon Date: Fri Jan 2 18:32:33 2009 -0800 fixed refs handling, added gc auto, updated tests commit d921970aadf03b3cf0e71becdaab3147ba71cdef Reflog: master@{1} (Scott Chacon ) Reflog message: merge phedders/rdocs: Merge made by recursive. Author: Scott Chacon Date: Thu Dec 11 15:08:43 2008 -0800 Merge commit 'phedders/rdocs' Важно отметить, что информация в ссылочном логе строго локальная — это лог того, чем вы занимались со своим репозиторием. Ссылки не будут теми же самыми в чьей-то чужой копии репозитория; и после того как вы только что склонировали репозиторий, ссылочный лог будет пустым, так как вы ещё ничего не делали со своим репозиторием. Команда `git show HEAD@{2.months.ago}` сработает, только если вы склонировали свой проект как минимум два месяца назад. Если вы склонировали его пять минут назад, то вы ничего не получите. ### Ссылки на предков ### Ещё один основной способ указать коммит — указать коммит через его предков. Если поставить `^` в конце ссылки, для Git'а это будет означать родителя этого коммита. Допустим, история вашего проекта выглядит следующим образом: $ git log --pretty=format:'%h %s' --graph * 734713b fixed refs handling, added gc auto, updated tests * d921970 Merge commit 'phedders/rdocs' |\ | * 35cfb2b Some rdoc changes * | 1c002dd added some blame and merge stuff |/ * 1c36188 ignore *.gem * 9b29157 add open3_detach to gemspec file list В этом случае вы можете посмотреть предыдущий коммит, указав `HEAD^`, что означает "родитель HEAD": $ git show HEAD^ commit d921970aadf03b3cf0e71becdaab3147ba71cdef Merge: 1c002dd... 35cfb2b... Author: Scott Chacon Date: Thu Dec 11 15:08:43 2008 -0800 Merge commit 'phedders/rdocs' Вы также можете указать некоторое число после `^`. Например, `d921970^2` означает "второй родитель коммита d921970". Такой синтаксис полезен только для коммитов-слияний, которые имеют больше, чем одного родителя. Первый родитель — это ветка, на которой вы находились во время слияния, а второй — коммит на ветке, которая была слита: $ git show d921970^ commit 1c002dd4b536e7479fe34593e72e6c6c1819e53b Author: Scott Chacon Date: Thu Dec 11 14:58:32 2008 -0800 added some blame and merge stuff $ git show d921970^2 commit 35cfb2b795a55793d7cc56a6cc2060b4bb732548 Author: Paul Hedderly Date: Wed Dec 10 22:22:03 2008 +0000 Some rdoc changes Другое основное обозначение для указания на предков — это `~`. Это тоже ссылка на первого родителя, поэтому `HEAD~` и `HEAD^` эквивалентны. Различия становятся очевидными, только когда вы указываете число. `HEAD~2` означает первого родителя первого родителя HEAD или прародителя — это переход по первым родителям указанное количество раз. Например, для показанной выше истории, `HEAD~3` будет $ git show HEAD~3 commit 1c3618887afb5fbcbea25b7c013f4e2114448b8d Author: Tom Preston-Werner Date: Fri Nov 7 13:47:59 2008 -0500 ignore *.gem То же самое можно записать как `HEAD^^^`, что опять же означает первого родителя первого родителя первого родителя: $ git show HEAD^^^ commit 1c3618887afb5fbcbea25b7c013f4e2114448b8d Author: Tom Preston-Werner Date: Fri Nov 7 13:47:59 2008 -0500 ignore *.gem Кроме того, можно комбинировать эти обозначения. Например, можно получить второго родителя для предыдущей ссылки (мы предполагаем, что это коммит-слияние) написав `HEAD~3^2`, ну и так далее. ### Диапазон коммитов ### Теперь, когда вы умеете задавать отдельные коммиты, разберёмся, как указать диапазон коммитов. Это особенно полезно при управлении ветками — если у вас много веток, вы можете использовать обозначения диапазонов, чтобы ответить на вопросы типа "Какие в этой ветке есть коммиты, которые не были слиты в основную ветку?" #### Две точки #### Наиболее распространённый способ задать диапазон коммитов — это запись с двумя точками. По существу, таким образом вы просите Git взять набор коммитов, достижимых из одного коммита, но не достижимых из другого. Например, пускай ваша история коммитов выглядит так, как показано на рисунке 6-1. Insert 18333fig0601.png Рисунок 6-1. Пример истории для выбора набора коммитов. Допустим, вы хотите посмотреть, что в вашей ветке `experiment` ещё не было слито в ветку `master`. Можно попросить Git показать вам лог только таких коммитов с помощью `master..experiment` — эта запись означает "все коммиты, достижимые из experiment, которые недостижимы из master". Для краткости и большей понятности в примерах мы будем использовать буквы для обозначения коммитов на диаграмме вместо настоящего вывода лога в том порядке, в каком они будут отображены: $ git log master..experiment D C С другой стороны, если вы хотите получить обратное — все коммиты в `master`, которых нет в `experiment`, можно переставить имена веток. Запись `experiment..master` покажет всё, что есть в `master`, но недостижимо из `experiment`: $ git log experiment..master F E Такое полезно, если вы хотите, чтобы ветка `experiment` была обновлённой, и хотите посмотреть, что вы собираетесь в неё слить. Ещё один частый случай использования этого синтаксиса — посмотреть, что вы собираетесь отправить на удалённый сервер: $ git log origin/master..HEAD Эта команда покажет вам все коммиты в текущей ветке, которых нет в ветке `master` на сервере `origin`. Если бы вы выполнили `git push`, при условии, что текущая ветка отслеживает `origin/master`, то коммиты, которые перечислены в выводе `git log origin/master..HEAD` это те коммиты, которые были бы отправлены на сервер. Кроме того, можно опустить одну из сторон в такой записи — Git подставит туда HEAD. Например, вы можете получить такой же результат, как и в предыдущем примере, набрав `git log origin/master..` — Git подставит HEAD сам, если одна из сторон отсутствует. #### Множество вершин #### Запись с двумя точками полезна как сокращение, но, возможно, вы захотите указать больше двух веток, чтобы указать нужную ревизию. Например, чтобы посмотреть, какие коммиты находятся в одной из нескольких веток, но не в текущей. Git позволяет сделать это с помощью использования либо символа `^`, либо `--not` перед любыми ссылками, коммиты, достижимые из которых, вы не хотите видеть. Таким образом, следующие три команды эквивалентны: $ git log refA..refB $ git log ^refA refB $ git log refB --not refA Это удобно, потому что с помощью такого синтаксиса можно указать более двух ссылок в своём запросе, чего вы не сможете сделать с помощью двух точек. Например, если вы хотите увидеть все коммиты достижимые из `refA` или `refB`, но не из `refC`, можно набрать одну из таких команд: $ git log refA refB ^refC $ git log refA refB --not refC Всё это делает систему выбора ревизий очень мощной, что должно помочь вам определять, что содержится в ваших ветках. #### Три точки #### Последняя основная запись для выбора диапазона коммитов — это запись с тремя точками, которая означает те коммиты, которые достижимы по одной из двух ссылок, но не по обеим одновременно. Вернёмся к примеру истории коммитов на рисунке 6-1. Если вы хотите увидеть, что находится в `master` или `experiment`, но не в обоих сразу, выполните $ git log master...experiment F E D C Повторимся, что это даст вам стандартный `log`-вывод, но покажет только информацию об этих четырёх коммитах, упорядоченных по дате коммита, как и обычно. В этом случае вместе с командой `log` обычно используют параметр `--left-right`, который показывает, на какой стороне диапазона находится каждый коммит. Это помогает сделать данные полезнее: $ git log --left-right master...experiment < F < E > D > C С помощью этих инструментов вы можете намного легче объяснить Git'у, какой коммит или коммиты вы хотите изучить. ## Интерактивное индексирование ## Вместе с Git'ом поставляется пара сценариев (script), облегчающих выполнение некоторых задач в командной строке. Сейчас мы посмотрим на несколько интерактивных команд, которые помогут вам легко смастерить свои коммиты так, чтобы включить в них только определённые части файлов. Эти инструменты сильно помогают в случае, когда вы поменяли кучу файлов, а потом решили, что хотите, чтобы эти изменения были в нескольких сфокусированных коммитах, а не в одном большом путанном коммите. Так вы сможете убедиться, что ваши коммиты — это логически разделённые наборы изменений, которые будет легко просматривать другим разработчикам, работающим с вами. Если вы выполните `git add` с опцией `-i` или `--interactive`, Git перейдёт в режим интерактивной оболочки и покажет что-то похожее на это: $ git add -i staged unstaged path 1: unchanged +0/-1 TODO 2: unchanged +1/-1 index.html 3: unchanged +5/-1 lib/simplegit.rb *** Commands *** 1: status 2: update 3: revert 4: add untracked 5: patch 6: diff 7: quit 8: help What now> Как видите, эта команда показывает содержимое индекса, но в другом виде — по сути, ту же информацию вы получили бы при вызове `git status`, но здесь она в более сжатом и информативном виде. `git add -i` показывает проиндексированные изменения слева, а непроиндексированные — справа. Затем идёт раздел Commands (команды). Тут можно сделать многие вещи, включая добавление файлов в индекс, удаление файлов из индекса, индексирование файлов частями, добавление неотслеживаемых файлов и просмотр дельт (diff) проиндексированных изменений. ### Добавление и удаление файлов из индекса ### Если набрать `2` или `u` в приглашении `What now>`, сценарий спросит, какие файлы вы хотите добавить в индекс: What now> 2 staged unstaged path 1: unchanged +0/-1 TODO 2: unchanged +1/-1 index.html 3: unchanged +5/-1 lib/simplegit.rb Update>> Чтобы проиндексировать файлы TODO и index.html, нужно набрать их номера: Update>> 1,2 staged unstaged path * 1: unchanged +0/-1 TODO * 2: unchanged +1/-1 index.html 3: unchanged +5/-1 lib/simplegit.rb Update>> Символ `*` рядом с каждым файлом означает, что файл выбран для индексирования. Если вы сейчас ничего не будете вводить, а нажмёте Enter в приглашении `Update>>`, то Git возьмёт всё, что уже выбрано, и добавит в индекс: Update>> updated 2 paths *** Commands *** 1: status 2: update 3: revert 4: add untracked 5: patch 6: diff 7: quit 8: help What now> 1 staged unstaged path 1: +0/-1 nothing TODO 2: +1/-1 nothing index.html 3: unchanged +5/-1 lib/simplegit.rb Как видите, теперь файлы TODO и index.html проиндексированы (staged), а файл simplegit.rb — всё ещё нет. Если в этот момент вы хотите удалить файл TODO из индекса, используйте опцию `3` или `r` (revert): *** Commands *** 1: status 2: update 3: revert 4: add untracked 5: patch 6: diff 7: quit 8: help What now> 3 staged unstaged path 1: +0/-1 nothing TODO 2: +1/-1 nothing index.html 3: unchanged +5/-1 lib/simplegit.rb Revert>> 1 staged unstaged path * 1: +0/-1 nothing TODO 2: +1/-1 nothing index.html 3: unchanged +5/-1 lib/simplegit.rb Revert>> [enter] reverted one path Взглянув на статус снова, вы увидите, что файл TODO удалён из индекса: *** Commands *** 1: status 2: update 3: revert 4: add untracked 5: patch 6: diff 7: quit 8: help What now> 1 staged unstaged path 1: unchanged +0/-1 TODO 2: +1/-1 nothing index.html 3: unchanged +5/-1 lib/simplegit.rb Чтобы посмотреть дельту для проиндексированных изменений, используйте команду `6` или `d` (diff). Она покажет вам список проиндексированных файлов, и вы сможете выбрать те, для которых хотите посмотреть дельту. Это почти то же, что указать `git diff --cached` в командной строке: *** Commands *** 1: status 2: update 3: revert 4: add untracked 5: patch 6: diff 7: quit 8: help What now> 6 staged unstaged path 1: +1/-1 nothing index.html Review diff>> 1 diff --git a/index.html b/index.html index 4d07108..4335f49 100644 --- a/index.html +++ b/index.html @@ -16,7 +16,7 @@ Date Finder

...

- +