Продвинутый функционал Git: хитрые приемы и команды

git  —  очень мощный инструмент, который практически каждый разработчик задействует ежедневно. Но для большинства его использование сводится всего к нескольким командам: pull, commit и push. Чтобы быть эффективным, продуктивным и задействовать всю мощь git, нужно знать еще несколько команд и хитрых приемов. Рассмотрим простые в использовании и настройке, легко запоминающиеся функции git, которые делают работу, связанную с контролем версий, намного более приятной.

Улучшение базового рабочего процесса

Прежде чем использовать даже самые простые команды, такие как pull, commit и push, нужно выяснить, что происходит с ветками и измененными файлами. Для этого задействуем git log. Все о нем знают, но не все умеют делать его вывод действительно красивым и удобным для восприятия человека:

git log --graph --abbrev-commit --decorate --all \
    --format=format:"%C(bold blue)%h%C(reset) - %C(bold cyan)%aD%C(dim white) \
    - %an%C(reset) %C(bold green)(%ar)%C(reset)%C(bold yellow)%d%C(reset)%n %C(white)%s%C(reset)"
граф git log

Такой граф дает неплохое общее представление о том, что происходит. Однако иногда бывает необходимо немного углубиться, например посмотреть историю/изменения конкретных файлов или даже отдельных функций. git log (с аргументом -L :<funcname>:<file>) помогает и в этом:

функция git log

Теперь, когда мы чуть лучшее представляем, что происходит в репозитории, проверим различия между обновленными файлами и последним коммитом. Для этого будем использовать git diff. Здесь, опять же, ничего нового, но у diff есть несколько параметров и флагов, о которых вы можете не знать. Например, с помощью git diff branch-a..branch-b сравниваются две ветки, а с помощью git diff <commit-a> <commit-b> -- <path(s)>  —  даже конкретные файлы в разных ветках.

Иногда вывод git diff становится довольно трудным для восприятия. Для решения этой проблемы пробуем использовать флаг -w, который игнорирует все пробелы, делая diff немного спамоподобным. Или же задействуем --word-diff --color-words, благодаря которому работа ведется со словами, а не с целыми строками.

Если нужно что-то получше базового статического вывода в оболочке, запускаем difftool с помощью git difftool=vimdiff, который откроет diff рядом в редакторе vim. Хотите другой редактор? Запустите git difftool --tool-help и выберите из списка пригодных для diff инструментов.

Мы видели, как просматривать историю конкретных частей/строк в файле с помощью git log. Столь же удобно просматривать историю и индексируемых частей файлов. Это довольно легко делается в IDE, например IntelliJ, правда с интерфейсом командной строки git не все так просто, но выручает git add --patch:

git add fib.py --patch
diff --git a/fib.py b/fib.py
index e95a02c..8ac1035 100644
--- a/fib.py
+++ b/fib.py
@@ -1,3 +1,5 @@
+from math import sqrt
+
 # Эта функция вычисляет последовательность Фибоначчи
 def fibonacci(n):
     if n <= 0:
(1/2) Stage this hunk [y,n,q,a,d,j,J,g,/,e,?]? y

В результате открывается редактор, показывающий один фрагмент, т. е. часть кода с несколькими различающимися строками. Что делают с этим куском кода? Есть много вариантов, но наиболее важные из них y  —  принять изменения (индексировать кусок кода), n  —  не принять (не индексировать кусок кода) и e  —  отредактировать кусок кода прежде его индексирования (полный список вариантов доступен здесь).

После завершения интерактивного индексирования запускаем git status и видим, что частично индексированный файл находится и в разделе Changes to be committed: («Изменения, подлежащие фиксации»), и в Changes not staged for commit: («Изменения, не индексированные для коммита»). Запускаем также git add -i (интерактивное индексирование), а затем задействуем команду состояния s (status), которая покажет, какие строки индексированы, а какие  —  нет.

Устранение типичных ошибок

После индексирования файлов часто (даже слишком часто) понимаешь: добавлено то, что добавлять было не нужно. Но в git нет команды для разындексирования файла или файлов. Проблема решается возвращением репозитория в исходное состояние с помощью git reset --soft HEAD somefile.txt. Добавьте параметр -p после git reset, и вы получите UI, аналогичный тому, что показан с git add -p. Только не забудьте добавить здесь флаг --soft, иначе вы сотрете локальные изменения!

Применение (чуть меньшей) силы

Теперь, когда с индексированием покончили, остается сделать коммит и добавить его. Но что, если мы забыли что-то добавить или допустили ошибку и хотим внести исправления в уже добавленные коммиты? Есть простое решение: использовать git commit -a и git push --force. Но это очень опасно, если мы работаем с общей веткой, такой как master. Поэтому во избежание риска перезаписи чужой работы этим нашим force-push будем задействовать флаг --force-with-lease. В отличие от --force, этот флаг добавляет изменения только в том случае, если в ветку не внесено никаких других изменений. Если ветка изменена, добавление будет отклонено. То есть придется вытащить последние изменения, прежде чем добавлять что-то еще.

Как проводить слияния правильно

Если вы работаете в репозитории бок о бок с другими разработчиками, то наверняка все манипуляции производите в отдельной ветке, а не на master. Но рано или поздно приходится включать свой код в кодовую базу (в ветку master). И часто случается так, что к этому времени кто-то другой уже успевает добавить туда собственный код. Так что ветка вашей функции остается на несколько коммитов позади, а вам остается только слить свой код в master с помощью git merge. Но в этом случае будет создан дополнительный merge commit, делающий историю излишне сложной и менее удобной для восприятия:

История изменений веток

Гораздо лучше здесь было бы сначала переместить изменения из ветки функции в ветку master, а затем выполнить так называемое слияние с перемоткой вперед (git merge --ff). Благодаря этому поддерживается линейность истории, облегчающая ее чтение и последующий поиск коммитов, которые привели к появлению конкретной функции или бага.

Но как выполнить это самое перемещение изменений? Выполним в наиболее базовой его форме с помощью git rebase master feature/branch и с последующим force-push: этого обычно бывает достаточно. Чтобы задействовать git rebase по максимуму, включим также параметр -i, запустив сеанс интерактивного перемещения изменений  —  удобный инструмент, например для переписывания, объединения или вообще очистки коммитов и всей ветки. Попробуем даже переместить изменения ветки в ней самой:

git rebase -i @~4  # @ — это то же, что и HEAD

Эта команда фактически позволяет повторно применить последние 4 коммита, внося в них изменения. Например, объединить одни и переписать другие:

# git log перед перемещением 

* 48a68b1 - Mon, 18 Jan 2021 17:43:24 +0100 - MartinHeinz (64 minutes ago) (HEAD -> feature/issue-02, origin/feature/issue-02)
|  Refactoring.
* 898d430 - Mon, 18 Jan 2021 17:39:14 +0100 - MartinHeinz (68 minutes ago)
|  Update docs again.
* 31201a9 - Mon, 18 Jan 2021 15:01:43 +0100 - Martin Heinz (4 hours ago)
|  Update docs.
* 715c46a - Mon, 18 Jan 2021 14:48:56 +0100 - Martin Heinz (4 hours ago)
|  Fix bug.
| ...

# Начало интерактивного перемещения

pick 715c46a Fix bug.
pick 31201a9 Update docs.
pick 898d430 Update docs again.
pick 48a68b1 Refactoring.
# Перемещение c5eb07e..48a68b1 в 48a68b1 (4 команды)
...

# Перемещение изменений

reword 31201a9 Update docs.      # Предлагается переписать это
fixup 898d430 Update docs again.
pick 48a68b1 Refactoring.        # Предлагается переписать это
squash 715c46a Fix bug.
# Перемещение c5eb07e..48a68b1 в 48a68b1 (4 команды)
...

# git log после перемещения (и переписывания)

* 11495c3 - Mon, 18 Jan 2021 17:43:24 +0100 - Martin Heinz (84 minutes ago) (HEAD -> feature/issue-02)
|  Refactoring and fixes.
* 561c327 - Mon, 18 Jan 2021 15:01:43 +0100 - Martin Heinz (4 hours ago)
|  Update documenatation.
|  ...

В приведенном выше примере демонстрируется, как происходит сеанс перемещения. Наверху показано то, как выглядела ветка до перемещения. Во второй части фрагмента кода  —  список коммитов, представленных нам после запуска git rebase ..., каждый из которых предваряется командой pick. Поменяем эту команду для трех из четырех коммитов и полностью изменим их очередность. В третьей части примера показаны новые команды: reword, которая дает git указание открыть редактор описания коммита; squash, которая объединяет коммит с предшествующим ему; и fixup, отличающаяся от squash тем, что после объединения отбрасывает описание коммита. Применив эти изменения и переписав измененные коммиты, получаем историю, показанную в нижней части примера.

Если во время перемещения возникает какой-то конфликт, то для его разрешения запускаем git mergetool --tool=vimdiff, после чего продолжаем перемещение с помощью git rebase --continue. Если вам еще не знаком git mergetool, не пугайтесь этого зверя. На самом деле он тот же, что и в IDE типа IntelliJ, только в стиле Vim. Как и с любым инструментом, использующим vim, в нем довольно трудно ориентироваться и задействовать его, если не знаешь хотя бы несколько клавиш быстрого доступа. Иногда трудно даже понять, что перед тобой. Так что, если вам здесь понадобится помощь, рекомендую это подробное руководство.

Если все изложенное выше кажется слишком трудным или вам просто страшно использовать rebase, тогда создайте pull request (запрос на включение изменений в репозиторий) в GitHub и задействуйте кнопку Rebase and merge («Переместить и объединить»), чтобы выполнять хотя бы простые перемещения/слияния с перемоткой вперед.

Главное  —  эффективность

В предыдущих примерах мы показали несколько хитрых приемов и особенностей, но всех их запомнить не так просто. Особенно это касается таких команд, как git log. К счастью, у нас есть глобальная конфигурация и псевдонимы git, делающие использование упомянутых приемов гораздо более удобным. Глобальная конфигурация git находится в ~/.gitconfig и обновляется при каждом запуске git config --global .... Даже если вы не пробовали настроить этот файл, наверняка знаете, что в нем находится обычное для таких файлов содержимое, например раздел [user] и т. д. Но никто не мешает добавить в файл гораздо больше данных:

# .gitconfig
[user]
	name = MartinHeinz
	email = [email protected]
	signingkey = 7FBRA885E6354BC3E489CAF3D8B87B8N91F7538Q
[core]
	autocrlf = input
    editor = vim  # git config --global core.editor vim
[alias]
    graph = log --graph --abbrev-commit --decorate --all \
    --format=format:"%C(bold blue)%h%C(reset) - %C(bold cyan)%aD%C(dim white) \
    - %an%C(reset) %C(bold green)(%ar)%C(reset)%C(bold yellow)%d%C(reset)%n %C(white)%s%C(reset)"
[help]
	autocorrect = 10  # git config --global help.autocorrect 10
[commit]
	gpgsign = true

В этом примере добавлен ряд доступных параметров конфигурации. Здесь для длинной команды git log был назначен псевдоним git graph. Значение автозамены установлено на 10. То есть проходит одна секунда, прежде чем выполняется правильная команда вместо той, которая была введена неправильно. И в конце мы видим конфигурацию для подписи коммита с помощью GPG (подробнее об этом ниже).

Настройка .gitconfig со множеством псевдонимов  —  уже немало. А кроме них, есть довольно много хороших ресурсов с примерами того, что еще включают в .gitconfig. Поэтому вместо того, чтобы приводить здесь всеобъемлющий список параметров и псевдонимов, ограничимся несколькими ресурсами, которые все это содержат:

Аналогичное повышение производительности, как и в случае с псевдонимами, достигается с помощью автоматического завершения команд. Устанавливается оно просто с помощью:

cd ~
curl https://github.com/git/git/blob/master/contrib/completion/git-completion.bash

# Добавляем к .bash_profile или .bashrc следующее:
if [ -f ~/.git-completion.bash ]; then
  . ~/.git-completion.bash
fi

Дополнительные возможности

Помимо этих удобных псевдонимов, есть еще плагин git-extras со множеством полезных команд, которые тоже способны немного облегчить нам жизнь. Не будем подробно расписывать здесь весь функционал этого плагина. Желающие ознакомятся со списком его команд здесь, мы же быстренько пробежимся по тому, что там внутри:

  • git delta выводит список файлов, отличающихся от другой ветки.
  • git show-tree отображает декорированное графовое представление коммитов из всех веток аналогично показанному ранее.
  • git pull-request создает pull request (запрос на включение изменений в репозиторий) через командную строку.
  • git changelog генерирует журнал изменений из описаний коммитов и тегов.

Кроме этого плагина, конечно, есть и другие замечательные инструменты, например тот, который позволяет открывать репозиторий в браузере прямо из интерфейса командной строки. А статус репозитория в терминале настраивается с помощью zsh или bash-it.

Подписывание коммитов

Вам наверняка доводилось просматривать историю коммитов какого-нибудь проекта разработки ПО с открытым исходным кодом, даже если вы никогда не участвовали ни в одном из таких проектов. И в этой истории коммитов вы, скорее всего, видели значок завершения работы с коммитом и/или подписанный/проверенный значок коммита. Но что они означают и для чего используются?

Первый из них  —  значок завершения работы с коммитом  —  используется в некоторых проектах, чтобы показать, что его автор подтверждает, что он создал соответствующий код, или подтверждает, что (насколько ему известно) код был создан под соответствующей open-source лицензией. Это делается по юридическим причинам, связанным с авторским статусом кода. Использовать этот значок необязательно, но если однажды вы захотите поделиться своим кодом в каком-нибудь проекте, где будет требоваться такой значок, то вот как это делать:

~ $ git commit -m "Update docs." --signoff
[feature/issue-02 a2385f4] Update docs.
 1 file changed, 1 insertion(+)

~ $ git log
commit 31201a9a91983641897ac1e6c2ee0217a4952d7c
Author: Martin Heinz <[email protected]>
Date:   Mon Jan 18 15:01:43 2021 +0100

    Update docs.
    
    Signed-off-by: Martin Heinz <[email protected]>
...

В этом примере при запуске git commit с параметром --sign-off в конце описания коммита от имени пользователя из конфигурации git автоматически добавилась строка Signed-off-by: ....

Что касается подписанного/проверенного значка коммита, который вы наверняка замечали в некоторых репозиториях  —  используется он для того, чтобы противодействовать такому распространенному на GitHub явлению, как имперсонализация (т. е. когда одни пользователи выдают себя за других пользователей). Все, что нужно таким пользователям для осуществления своих злонамерений,  —  это поменять имя автора коммита и адрес электронной почты в конфигурации и добавить изменения кода. Для предотвращения имперсонализации подписывайте коммиты, задействуя GPG-ключи. Так проверяется, действительно ли человек, сделавший коммит и добавивший в него код, является тем, за кого себя выдает. Подписанный/проверенный значок коммита более распространен, чем значок завершения работы с коммитом, ведь часто бывает важно знать, кто на самом деле автор кода.

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

gpg --list-keys  # проверяем наличие уже имеющихся ключей
gpg --gen-key    # генерируем ключ

...
Real name: Martin Heinz
Email address: [email protected]
You selected this USER-ID:
    "Martin Heinz <[email protected]>"

pub   rsa3072 2021-01-18 [SC] [expires: 2023-01-18]
      <SOME_VALUE>
uid                      Martin Heinz <[email protected]>
sub   rsa3072 2021-01-18 [E] [expires: 2023-01-18]

git config --global user.signingkey <SOME_VALUE>

git commit -m "Signed commit." -S  # здесь будет предложено ввести пароль...
git push

Сначала будет сгенерирована пара ключей GPG (если ее еще нет), затем с помощью git config ... установлен ключ подписи и, наконец, при написании коммита кода добавлен параметр -S. Потом при просмотре информации о коммитах в GitHub вы увидите такой же значок, как на этом рисунке:

Подписанный непроверенный коммит

Но на рисунке подпись не проверена, ведь GitHub не знает, что GPG-ключ принадлежит вам. Чтобы это исправить, нужно добавить открытый ключ из пары ключей на GitHub. Для этого экспортируем ключ с помощью gpg --export:

gpg --armor --export <SOME_VALUE>
-----BEGIN PGP PUBLIC KEY BLOCK-----
...
-----END PGP PUBLIC KEY BLOCK-----

Затем возьмем этот ключ и вставим его в поле по адресу https://github.com/settings/gpg/new. После добавления ключа вернемся к ранее подписанному коммиту. Теперь он проверен, при условии что добавлен тот же ключ в GitHub, что был использован для подписи:

Подписанный подтвержденный коммит

Заключение

git  —  очень мощный инструмент со слишком большим количеством подкоманд и параметров, чтобы охватить их всех в одной статье.

Читайте также:

Читайте нас в Telegram, VK и Яндекс.Дзен


Перевод статьи Martin Heinz: Advanced Git Features You Didn’t Know You Needed

Предыдущая статьяThonny - идеальная IDE для новичков Python
Следующая статьяМеньше образы Docker => быстрее CI-конвейер