Продвинутое использование Гита или как выйти на пенсию на полгода раньше?

g_v7lgawugfumbtsi3edevsencc.png

Не знаю, на каком языке программирования вы пишете, но уверен, что используете Гит при разработке. Инструментов для сопровождения разработки становится всё больше, но даже самый маленький тестовый проект, я неизменно начинаю с команды git init. А в течение рабочего дня набираю в среднем ещё 80 команд, обращаясь к этой системе контроля версий.

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

На Хабр написано много статей о Гите, но они не уходят дальше официальной документации, а упрощать работу авторы предлагают самописными костылями. Я уверен, что изучать Гит нужно на конкретных примерах задач, а повышать эффективность работы с ним — стандартизированными средствами.


Кому будет полезна эта статья?

Вы уже освоили джентльменский набор Гита и готовы двигаться дальше? Существует 2 пути:


  1. Освоить сокращённые команды — алиасы. Они почти всегда составлены мнемонически и легко запоминаются. Забыть оригиналы команд проблематично, я легко их набираю, когда это требуется. Плюс не сбиваюсь с мысли, проверяя что-то в Гите в процессе написания кода.
  2. Узнать о дополнительных флагах к командам, а также их объединении между собой. Я понимаю, что кто-то ненавидит сокращения. Для вас тоже есть интересный материал в статье — как повысить полезность и удобство вывода команд, а также как решать не самые тривиальные, но часто встречающиеся на практике задачи.

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

Добро пожаловать под кат!


Подготовка

Среди разработчиков стандартом альтернативы Bash является Zsh — продвинутая программная оболочка, поддерживающая тонкую настройку. А среди пользователей Zsh стандартом является использование Oh My Zsh — набора готовых настроек для Zsh. Таким образом, установив этот комплект, мы из коробки получим набор хаков, которые годами собирало и нарабатывало для нас сообщество.

Очень важно отметить, что Zsh есть и для Linux, и для Mac, и даже для Windows.

Установка Zsh и Oh My Zsh

Устанавливаем Zsh и Oh My Zsh по инструкции одной командой:

# macOS
brew install zsh zsh-completions && sh -c "$(curl -fsSL https://raw.githubusercontent.com/robbyrussell/oh-my-zsh/master/tools/install.sh)"

# Ubuntu, Debian, ...
apt install zsh && sh -c "$(curl -fsSL https://raw.githubusercontent.com/robbyrussell/oh-my-zsh/master/tools/install.sh)"

Поскольку задача — оптимизировать взаимодействие с Гитом, добавим к Zsh пару плагинов. Откройте файл ~/.zshrc и добавьте к списку plugins:

plugins=(git gitfast)

Итого:


  • git — набор алиасов и вспомогательных функций;
  • gitfast — улучшенное автодополнение для Гита.

Установка tig

И последний штрих — установка консольной утилиты tig:

# macOS
brew install tig

# Ubuntu, Debian, ...
# https://jonas.github.io/tig/INSTALL.html

О ней поговорим дальше.


Гит на практике

Разбираться с Гитом лучше всего на примерах решения конкретных задач. Далее рассмотрим задачи из ежедневной практики и варианты их удобного решения. Для этого рассмотрим некий репозиторий с текстовыми файлами.


В жёлтых блоках указан основной алиас для решения задачи из раздела. Выучите только его, а всё остальное оставьте для общего развития.


Проверяем состояние рабочей директории

Начнём с самой базовой вещи. Мы немного поработали и теперь давайте посмотрим, что происходит в рабочей директории:

$ git status

On branch master
Changes to be committed:
  (use "git reset HEAD ..." to unstage)

    new file:   e.md

Changes not staged for commit:
  (use "git add ..." to update what will be committed)
  (use "git checkout -- ..." to discard changes in working directory)

    modified:   b.md

Untracked files:
  (use "git add ..." to include in what will be committed)

    d.md

Текущее состояние всех файлов описано очень подробно, даны дополнительные руководства к действию. Очень полезно на первых порах использования Гита, но для ежедневной работы очень много лишнего. Давайте понизим уровень шума дополнительными ключами:

$ git status -sb

## master
 M b.md
A  e.md
?? d.md

Ага, мы находимся в ветке master, изменили файл b.md (M-odified) и создали два файла, добавив первый в индекс Гита (A-dded), а второй оставив вне индекса (??). Коротко и ясно.

Осталось оптимизировать бесконечный ввод этой команды алиасом »git status with branch»:


Показать сокращённый статус рабочей директории
 

$ gsb # git status -sb


Создаём коммит

Продолжаем.

Конечно, вы умеете создавать коммиты. Но давайте попробуем оптимизировать решение и этой простой задачи. Добавляем все изменения в индекс алиасом »git add all»:

$ gaa # git add --all

Проверяем, что в индекс попало именно то, что нам нужно с помощью алиаса »git diff cached»:

$ gdca # git diff --cached

diff --git a/b.md b/b.md
index 698d533..cf20072 100644
--- a/b.md
+++ b/b.md
@@ -1,3 +1,3 @@
 # Beta

-Next step.
+Next step really hard.
diff --git a/d.md b/d.md
new file mode 100644
index 0000000..9e3752e
--- /dev/null
+++ b/d.md
@@ -0,0 +1,3 @@
+# Delta
+
+Body of article.

Хм, в один коммит должны попадать изменения, решающие единственную задачу. Здесь же изменения обоих файлов никак не связаны между собой. Давайте пока исключим файл d.md из индекса алиасом »git reset undo»:

$ gru d.md # git reset -- d.md

И создадим коммит алиасом »git commit»:

$ gc # git commit

Пишем название коммита и сохраняем. А следом создаём ещё один коммит для файла d.md более привычной командой с помощью алиаса »git commit messag:

$ gaa # Уже знакомый алиас
$ gcmsg "Add new file" # git commit -m "Add new file"

А ещё мы можем…

… коммитить изменённые файлы из индекса одной командой:

$ gcam "Add changes" # git commit -a -m "Add changes"

… смотреть изменения по словам вместо строк (очень полезно при работе с текстом):

$ gdw # git diff --word-diff

… добавлять файлы по частям (очень полезно, когда нужно добавить в коммит только часть изменений из файла):

$ gapa # git add --patch

… добавлять в индекс только файлы, уже находящиеся под наблюдением Гита:

$ gau # git add --update

Итого:


Добавить в индекс / Создать коммит
 

$ ga # git add
$ gc # git commit


Исправляем коммит

Название последнего коммита не объясняет сделанных нами изменений. Давайте переформулируем:

$ gc! # git commit -v --amend

И в открывшемся текстовом редакторе назовём его более понятно: "Add Delta article". Уверен, вы никогда не используете ключ -v, хотя при редактировании описания коммита он показывает все сделанные изменения, что помогает лучше сориентироваться.

А ещё мы можем…

… внести в коммит изменения файлов, но не трогать описание:

$ gcn! # git commit -v --no-edit --amend

… внести все изменения файлов сразу в коммит, без предварительного добавления в индекс:

$ gca! # git commit -v -a --amend

… скомбинировать две предыдущие команды:

$ gcan! # git commit -v -a --no-edit --amend

Ну и важно ещё раз отметить, что вместо набора полной регулярно используемой команды git commit -v --amend, мы пишем всего три символа:


Изменить последний коммит
 

$ gc! # git commit -v --amend


Начинаем работать над новой фичей

Создаём новую ветку от текущей алиасом »git checkout branch»:

$ gcb erlang # git checkout --branch erlang

Хотя нет, лучше напишем статью про более современный язык Эликсир алиасом »git branch с ключом move» (переименовывание в Гите делается через move):

$ gb -m elixir # git branch -m elixir

Здесь логично было бы использовать алиас gbmv, но его, к сожалению, ещё не придумали. Хороший вариант для контрибьюта.

Вносим изменения в репозиторий и создаём коммит, как уже умеем:

$ echo "# Эликсир — мощь Эрланга с изяществом Руби." > e.md
$ gaa && gcmsg "Add article about Elixir"

И запоминаем:


Создать новую ветку
 

$ gcb # git checkout --branch


Сливаем изменения

Теперь добавляем нашу новую статью об Эликсире в master. Сначала переключимся на основную ветку алиасом »git checkout master»:

$ gcm # git checkout master

Нет, серьёзно. Одна из самых часто используемых команд в три легко запоминающихся символа. Теперь мерджим изменения алиасом »git merge»:

$ gm elixir # git merge elixir

Упс, а в master кто-то уже успел внести свои изменения. И вместо красивой линейной истории, которая принята у нас в проекте, создался ненавистный мердж-коммит.


Слить ветки
 

$ gm # git merge


Удаляем последний коммит

Ничего страшного! Нужно просто удалить последний коммит и попробовать слить изменения ещё раз »git reset hhard»:


Удалить последний коммит
 

$ grhh HEAD~ # git reset --hard HEAD~


Решаем конфликты

Стандартная последовательность действий checkout – rebase – merge для подготовки линейной истории изменений выполняется следующей последовательностью алиасов:

gco elixir # git checkout elixir
grbm # git rebase master
gcm # git checkout master
gm elixir # git merge elixir

Все они так часто используются что уже отлетают от пальцев, и проделывая подобные операции, нет необходимости задумываться о том, какой набор букв нужно набирать. И не забывайте, что в Zsh можно дополнять названия веток клавишей Tab.


Сделать ребейз
 

$ grb # git rebase


Отправка изменений на сервер

Сначала добавляем origin алиасом »git remote add»:

$ gra origin git@github.com/... # git remote add origin git@github.com/...

А затем отправляем изменения напрямую в текущую ветку репозитория («gg» — удвоенное g в начале команды указывает на выполнение действия в текущую ветку):

$ ggpush # git push origin git_current_branch

Вы также можете…

… отправить изменения на сервер с установкой upstream алиасом »git push set upstream»:

$ gpsup # git push --set-upstream origin $(git_current_branch)


Отправить изменения на сервер
 

$ gp # git push


Получаем изменения с сервера

Работа кипит. Мы успели добавить новую статью f.md в master, а наши коллеги изменить статью a.md и отправить это изменение на сервер. Эта ситуация тоже решается очень просто:

$ gup # git pull --rebase

После чего можно спокойно отправлять изменения на сервер. Конфликт исчерпан.


Получить изменения с сервера
 

$ gl # git pull


Удаляем слитые ветки

Итак, мы успешно влили в master несколько веток, в том числе и ветку elixir из предшествующего примера. Они нам больше не нужны. Можно удалять алиасом »git branch delete another»:

$ gbda # git branch --no-color --merged | command grep -vE "^(\*|\s*(master|develop|dev)\s*$)" | command xargs -n 1 git branch -d

Очень красивая и хитрая команда. Обычно я забываю очищать потерявшие актуальность ветки и эта изящная команда — настоящее спасение. Если не хотите использовать алиас, просто скопируйте полный вариант команды себе в заметки, и выполняйте его по мере необходимости.


Создаём временный коммит

Работа над новой статьёй h.md про Haskell идёт полным ходом. Написана половина и нужно получить отзыв от коллеги. Недолго думая, набираем алиас »git work in progress»:

$ gwip # git add -A; git rm $(git ls-files --deleted) 2> /dev/null; git commit --no-verify -m "--wip-- [skip ci]"

И тут же создаётся коммит с названием Work in Progress, пропускающим CI и удаляющим «лишние» файлы. Отправляем ветку на сервер, говорим об этом коллеге и ждём ревью.

Затем этот коммит можно отменить и вернуть файлы в исходное состояние:

$ gunwip # git log -n 1 | grep -q -c "\-\-wip\-\-" && git reset HEAD~1

А проверить, есть ли в вашей ветке WIP-коммиты можно командой:

$ work_in_progress

Команда gwip — довольно надёжный аналог stash, когда нужно переключиться на соседнюю ветку. Но в Zsh есть много алиасов и для самого stash.


Добавить временный коммит / Сбросить временный коммит
 

$ gwip
$ gunwip


Прячем изменения

С этой командой нужно быть осторожным. Файлы можно спрятать, а затем неаккуратным действием удалить насовсем, благо есть reflog, в котором можно попытаться найти потерянные наработки.

Давайте спрячем файлы, над которыми работаем, алиасом »git stash all»:

$ gsta # git stash save

А затем вернём их обратно алиасом »git stash pop»:

$ gstp # git stash pop

Или более безопасным методом »git stash all apply»:

$ gstaa # git stash apply

Вы также можете …

… посмотреть, что конкретно мы припрятали:

gsts # git stash show --text

… воспользоваться сокращениями для связанных команд:

gstc # git stash clear
gstd # git stash drop
gstl # git stash list


Спрятать изменения / Достать изменения
 

$ gsta
$ gstaa


Ищем баг

Инструмент git-bisect, который неоднократно спасал мне жизнь, тоже имеет свои алиасы. Начинаем с запуска процедуры «двоичного поиска ошибки» алиасом »git bisect start»:

$ gbss # git bisect start

Отмечаем, что текущий, последний в ветке, коммит содержит ошибку, алиасом »git bisect bad»:

$ gbsb # git bisect bad

Теперь помечаем коммит, гарантирующий нам рабочее состояние приложения »git bisect good»:

$ gbsg HEAD~20 # git bisect good HEAD~20

А теперь остаётся продолжать отвечать на вопросы Гита фразами gbsb или gbsg, а после нахождения виновника сбросить процедуру:

$ gbsr # git bisect reset

И я действительно пишу эти сокращения при использовании этого инструмента.


Поиск коммита с ошибкой
 

$ gbss # git bisect start
$ gbsb # git bisect bad
$ gbsg # git bisect good
$ gbsr # git bisect reset


Ищем зачинщика беспредела

Даже с высоким процентом покрытия кода тестами, никто не застрахован от ситуации, когда приложение падает и любезно указывает на конкретную строчку с ошибкой. Или, например, в нашем случае мы хотим узнать, кто допустил ошибку во второй строчке файла a.md. Для этого выполните команду:

$ gbl a.md -L 2 # git blame -b -w a.md -L 2

Видите, контрибьютеры Oh My Zsh сделали алиас не просто на команду git blame, а добавили в него ключи, которые упрощают поиск непосредственно зачинщика.


Bonus


Просмотр списка коммитов

Для просмотра списка коммитов используется команда git log с дополнительными ключами форматирования вывода. Обычно эту команду вместе с ключами заносят в кастомные алиасы Гита. Нам с вами повезло больше, у нас уже есть готовый алиас из коробки: glog. А если вы установили утилиту tig по совету из начала статьи, то вы абсолютный чемпион.

Теперь, чтобы поизучать историю коммитов в консоли в очень удобном виде, нужно набрать слово git наоборот:

$ tig

Утилита также даёт пару полезных дополнений, которых нет в Гите из коробки.

Во-первых, команда для поиска по содержимому истории:

$ tig grep

Во-вторых, просмотр списка всех источников, веток, тегов вместе с их историей:

$ tig refs

В-третьих, может быть найдёте что-то интересное для себя сами:

$ tig --help


Случайно сделал git reset --hard

Вы работали над веткой elixir весь день:

$ glog

* 17cb385 (HEAD -> elixir) Refine Elixir article
* c14b4dc Add article about Elixir
* db84d54 (master) Initial commit

И под конец случайно всё удалили:

$ grhh HEAD~2
HEAD is now at db84d54 Initial commit

Не нужно паниковать. Самое главное правило — перестаньте выполнять какие-либо команды в Гите и выдохните. Все действия с локальным репозиторием записываются в специальный журнал — reflog. Из него можно достать хеш нужного коммита и восстановить его в рабочем дереве.

Давайте заглянем в рефлог, но не обычным способом через git reflog, а более интересным с подробной расшифровкой записей:

$ glg -g

Находим хеш нужного коммита 17cb385 и восстанавливаем его:

# Создаём новую ветку с нашим коммитом и переключаемся на неё
$ gcb elixir-recover 17cb385

# Удаляем старую ветку 
$ gbd elixir

# Переименовываем восстановленную ветку обратно
$ gb -m elixir


Случайно вместо создания нового коммита внёс изменения в предыдущий

Здесь нам снова на помощь приходит рефлог. Находим хеш оригинального коммита 17cb385, если мы производим отмену коммита сразу же, то вместо поиска хеша можем воспользоваться быстрой ссылкой на него HEAD@{1}. Следом делаем мягкий сброс, индекс при этом не сбрасывается:

# Мягкий сброс на оригинальный коммит
$ grh --soft HEAD@{1} # git reset -soft

# Коммитим правильно
$ gcmsg "Commit description"


Ветка слишком сильно устарела

Бывает начинаешь работать над фичей, но её релиз откладывается на неопределённый срок. Делаешь коммит и переключаешься на другие задачи. Вместе с командой вносишь кучу изменений в мастер и спустя время возвращаешься к ветке с фичей. Пробуешь сделать ребейз, но он предлагает разобрать конфликты в десятке коммитов. Можно попробовать решить их все либо сделать проще.

Давайте рассмотрим на примере ветки с фичей под названием elixir:

# Переключаемся на master
$ gcm # git checkout master

# Создаём новую актуальную ветку для оригинальной фичи
$ gcb elixir-new # git checkout --branch elixir-new

# Переносим единственный коммит с фичей из устаревшей ветки в новую
$ gcp elixir@{0} # git cherry-pick elixir@{0}

Вот так, вместо попытки обновления ветки, мы берём и без проблем переносим один единственный коммит.


Удаление важных данных из репозитория

Для удаления важных данных из репозитория, у меня сохранён такой сниппет:

$ git filter-branch --force --index-filter 'git rm --cached --ignore-unmatch ' --prune-empty --tag-name-filter cat -- --all && git push origin --force --all

Выполнение этой команды поломает ваш stash. Перед её исполнением рекомендуется достать все спрятанные изменения. Подробнее об этом приёме по ссылке.


Обращение к предыдущей ветке

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

$ gco - # git checkout -
$ gm - # git merge -
$ grb - # git rebase -


Удаление всех файлов, отмеченных в .gitignore

Ещё одна нередкая неудача — слишком поздно добавить в .gitignore какие-то нежелательные файлы или директории. Для того, чтобы вычистить их из репозитория (и удалить с диска) уже есть готовые ключи для команды git clean:

$ gclean -X # git clean -Xfd

Будьте осторожны!

Как правильно перебдеть читайте дальше.


Зачем многим командам нужен ключ --dry-run?

Ключ --dry-run нужен как раз в качестве осторожности при задачах удаления и обновления. Например, в предыдущем разделе описан способ удаления всего, что указано в файле .gitignore. Лучше проявиться осторожность и воспользоваться ключом --dry-run, отсмотреть список всех файлов к удалению, и только затем выполнить команду без --dry-run.


Заключение

В статье показывается точка для оптимизации трудовой деятельности программиста. Запомнить 10–20 мнемонических сокращений не составляет труда, забыть оригинальные команды практически невозможно. Алиасы стандартизированы, так что при переходе всей команды на Zsh + Oh My Zsh, вы сможете работать с теми же скоростью и комфортом, даже при парном программировании.


Куда двигаться дальше?

Предлагаю следующие варианты:


  1. Наконец-то разберитесь, как Гит устроен внутри. Очень помогает понимать, что ты делаешь и почему то, что ты хочешь сделать не получается.
  2. Не ленитесь лишний раз заглянуть в документацию к командам: git --help или ghh.
  3. Посмотрите полный список алиасов по ссылке. Пытаться запомнить их все — безумие, но использовать список в качестве сборника набора интересных команд и ключей к ним — хорошая идея.

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

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

© Habrahabr.ru