Просунуте використання Гіта або як вийти на пенсію на півроку раніше?
Не знаю, якою мовою програмування ви пишете, але впевнений, що використовуєте Гіт при розробці. Інструментів для супроводу розробки стає все більше, але навіть самий маленький тестовий проект, я незмінно починаю з команди 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
(Modified
) і створили два файли, додавши перший в індекс Гіта (Added
), а другий залишивши поза індексом (??
). Коротко і ясно.
Залишилося оптимізувати нескінченний введення цієї команди аліасом «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 -v "^(*|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
. - Подивіться повний список аліасів по посиланню. Намагатися запам’ятати їх все – безумство, але використовувати перелік в якості збірки набору цікавих команд та ключів до них – гарна ідея.
Деякі аліаси зроблені нетривіально, але виявляються дуже корисними на практиці. Багато з представлених аліасів є не просто скороченнями, а невеликими функціями, які ще більше оптимізують роботу. Користуватися Гітом стало приємніше, якість комітів підвищилося.
Сподіваюся, матеріал виявився корисним, і ви змогли дізнатися для себе щось нове. А може бути вже почали активно впроваджувати новий підхід. Удачі!