Хабрахабр

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

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

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

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

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

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

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

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

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

Подготовка

А среди пользователей Zsh стандартом является использование Oh My Zsh – набора готовых настроек для Zsh. Среди разработчиков стандартом альтернативы Bash является 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)"

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

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

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

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

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

$ 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

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

Сначала переключимся на основную ветку алиасом «git checkout master»: Теперь добавляем нашу новую статью об Эликсире в 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

Но в Zsh есть много алиасов и для самого stash. Команда gwip – довольно надёжный аналог 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 start»: Инструмент git-bisect, который неоднократно спасал мне жизнь, тоже имеет свои алиасы.

$ 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 с дополнительными ключами форматирования вывода. А если вы установили утилиту tig по совету из начала статьи, то вы абсолютный чемпион. Нам с вами повезло больше, у нас уже есть готовый алиас из коробки: glog.

Теперь, чтобы поизучать историю коммитов в консоли в очень удобном виде, нужно набрать слово 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@. Здесь нам снова на помощь приходит рефлог. Следом делаем мягкий сброс, индекс при этом не сбрасывается:

# Мягкий сброс на оригинальный коммит
$ 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

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

$ gclean -X # git clean -Xfd

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

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

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

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

Заключение

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

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

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

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

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

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

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть