Не знаю, на каком языке программирования вы пишете, но уверен, что используете Гит при разработке. Инструментов для сопровождения разработки становится всё больше, но даже самый маленький тестовый проект, я неизменно начинаю с команды 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 <file>..." to unstage)

    new file:   e.md

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

    modified:   b.md

Untracked files:
  (use "git add <file>..." 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 <path-to-your-file>' --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. Посмотрите полный список алиасов по ссылке. Пытаться запомнить их все – безумие, но использовать список в качестве сборника набора интересных команд и ключей к ним – хорошая идея.

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


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