Не знаю, на каком языке программирования вы пишете, но уверен, что используете Гит при разработке. Инструментов для сопровождения разработки становится всё больше, но даже самый маленький тестовый проект, я неизменно начинаю с команды git init
. А в течение рабочего дня набираю в среднем ещё 80 команд, обращаясь к этой системе контроля версий.
Я потратил кучу нервов, когда стал переучиваться на десятипальцевый метод печати. В итоге это стало самым правильным решением по улучшению личного рабочего процесса. В числе следующих по важности оптимизаций стоит углубленное освоение Гита.
На Хабр написано много статей о Гите, но они не уходят дальше официальной документации, а упрощать работу авторы предлагают самописными костылями. Я уверен, что изучать Гит нужно на конкретных примерах задач, а повышать эффективность работы с ним – стандартизированными средствами.
Кому будет полезна эта статья?
Вы уже освоили джентльменский набор Гита и готовы двигаться дальше? Существует 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 message»:
$ 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
, вы сможете работать с теми же скоростью и комфортом, даже при парном программировании.
Куда двигаться дальше?
Предлагаю следующие варианты:
- Наконец-то разберитесь, как Гит устроен внутри. Очень помогает понимать, что ты делаешь и почему то, что ты хочешь сделать не получается.
- Не ленитесь лишний раз заглянуть в документацию к командам:
git --help
илиghh
. - Посмотрите полный список алиасов по ссылке. Пытаться запомнить их все – безумие, но использовать список в качестве сборника набора интересных команд и ключей к ним – хорошая идея.
Некоторые алиасы сделаны нетривиально, но оказываются очень полезными на практике. Многие из представленных алиасов являются не просто сокращениями, а небольшими функциями, которые ещё больше оптимизируют работу. Пользоваться Гитом стало приятнее, качество коммитов повысилось.
Надеюсь, материал оказался полезным, и вы смогли узнать для себя что-то новое. А может быть уже начали активно внедрять новый подход. Удачи!
Автор: Ярослав