В одной лодке с «ублюдком»: 11 продвинутых советов по использованию Git
- суббота, 1 августа 2020 г. в 00:31:41
*"ублюдок" — вольный перевод слова "git" — "an unpleasant or contemptible person", "неприятный или презренный человек".
В комментариях к статье 15 базовых советов по Git для эффективной работы каждый день развернулась дискуссия на тему эффективности использования тех или иных команд и опций. Надо признать, что git
предоставляет столько различного функционала, что во-первых, за всем становится невозможно уследить, а во-вторых, его можно совершенно по-разному вписывать в рабочий процесс.
Давайте посмотрим, что можно использовать, чтобы улучшить себе жизнь. Статья предполагает, что читатель умеет пользоваться основными возможностями git
и понимает что делает, когда, скажем, вводит в консоль git rebase --merge --autostash
.
git
одновременноНачнём с того, как именно Вы пользуетесь возможностями git? Многие работают строго из консоли или из приложения вроде SourceTree, и с первого взгляда может показаться, что эти варианты взаимоисключают друг друга.
Многие продвинутые редакторы, в частности, Ваш любимый мой любимый VS Code, предоставляют как удобный доступ к консоли, так и приятный графический интерфейс для систем контроля версий, что позволяет в равной мере использовать плюсы обоих вариантов.vim
Удобства графического интерфейса очевидны невооружённым взглядом:
git status
.Удобства использования git
из консоли не всегда очевидно для тех, кто привык пользоваться графическим интерфейсом.
git
в первую очередь консольная команда, а любой GUI — это посредник между ним и Вами.git pull --ff-only
при наличии входящих изменений в отредактированных файлах — сразу можно увидеть в каком файле есть несовместимые изменения и заняться мержем веток вручную:> git pull origin master --ff-only
From ../habr2
* branch master -> FETCH_HEAD
error: Your local changes to the following files would be overwritten by merge:
.gitignore
Please commit your changes or stash them before you merge.
Aborting
Updating 6d1c088..a113bf7
Соответственно, когда у вас есть доступ и к консоли, и к боковой панели, можете успешно использовать лучшее из обоих миров так, как будет удобно именно Вам.
Применительно к VS Code с установленным плагином GitLens хочу поделиться парочкой специфичных лайфхаков.
gitlens.diffWithBranch
— эта команда позволяет быстро сравнить текущий файл с его версией в любой ветке.Да, именно три штуки: системный (--system
), пользовательский (--global
) и локальный (--local
). Соответственно, они применяются в порядке иерархии, каждый последующий оверрайдит предыдущий — системный применяется для всех пользователей, пользовательский — конкретно для Вас, локальный — для конкретного репозитория. Ловко лавируя между ними, можно гибко адаптировать свой рабочий процесс под условия окружающей среды.
(Upd. Как оказалось, есть ещё четвёртый уровень конфигов, специфичный для отдельных рабочих копий одного репозитория worktree. Подробнее про worktree см. ниже.)
Когда какой стоит применять? В большинстве случаев случаев Вы предпочтёте воспользоваться глобальным, чтобы вынести в него общие для всех настройки core.eol
, алиасы, а также user.name
и user.email
, переопределяя только специфические вещи для конкретного репозитория. Однако в некоторых случаях, например когда несколько разработчиков по очереди отлаживают встраиваемое ПО, часть общих настроек имеет смысл вынести на системный уровень, переопределяя в глобальном (== пользовательском) только user.name
/email
.
Также в моей практике был случай, когда в рабочих репозиториях надо было пользоваться строго рабочей почтой, при этом в своих локальных репозиториях я продолжал пользоваться личной. Чтобы даже случайно нельзя было перепутать где что, я удалил user.name
/email
из глобального конфига, каждый раз указывая их заново в локальном, держа процесс под контролем.
Скорее всего, Вы сталкивались хотя бы раз с ситуацией, когда надо срочно переключиться с одной ветки на другую, бросив всё в разобранном состоянии. Очень вероятно, что Вы знаете про git stash
(от англ. "тайник"), который позволяет "спрятать" Ваши текущие изменения. Однако во время его использования Вы можете столкнуться со следующими вещами:
--amend
можете написать с закрытыми глазами, stash
имеет несколько перпендикулярный интерфейс. Чтобы сохранить его надо сделать git stash save
(при этом save
может быть опущен). А чтобы восстановить — есть git stash apply
(применяет последний стеш из всех) и git stash pop
(применяет стеш и удаляет его из стека). Соответственно, когда придёт внезапная необходимость переключиться, Вы можете не сразу вспомнить, а что собственно надо вводить и что от команды ожидать.stash
-ить буквально пару строчек, то можно вообще не вспомнить, что делал stash
, и потом сидишь и удивляешься, куда делись изменения.stash
по умолчанию распространяется только на изменённые (modified) файлы и не включает в себя неотслеживаемые (untracked). Соответственно, не зная этого, при переключении веток можно потерять их, если, например, они авто-генерируемые.Что же делать, если не stash
? Наиболее простое решение — взять и закоммитить всё с комментарием WIP
(распространённая аббревиатура от "Work In Progress"). Не надо морочить себе голову, вспоминать названия команд и искать потом, в который из стешей сохранены изменения.
А зачем тогда stash
вообще нужен? Я предпочитаю их использовать для хранения мелких фиксов, которые нужны только для отладки и не должны быть закоммичены вообще. Есть возможность применять не только последний из стешей, но и вообще любой, ссылаясь на его имя. Самое большое удобство в том, что стеши хоть и "помнят" на какой ветке были сделаны, но ни к чему не обязывают и могут быть применены на любой ветке. Я где-то когда-то нашёл очень удобные алиасы для этого:
git config --global alias.sshow "!f() { git stash show stash^{/$*} -p; }; f"
git config --global alias.sapply "!f() { git stash apply stash^{/$*}; }; f"
# сохранить
git stash save "hack"
# посмотреть
git sshow "hack"
# применить
git sapply "hack"
-
" для возврата к предыдущей веткеПосле того, как вы сделали свои грязные дела в другой ветке и хотите вернуться к предыдущей, вместо того чтобы вспоминать и вводить её полное имя, можно просто передать "-
":
git checkout -
Пользователям Windows этот трюк иногда совершенно незнаком, а для пользователей Linux он может быть привычен по аналогичному использованию в bash
:
cd /some/long/path
...
cd -
Предположим, у вас есть две принципиально несовместимые друг с другом ветки — например, когда создаётся множество временных неотслеживаемых файлов кэша, при переходе между ветками можно замучиться их вычищать (передаю привет Unity).
Пришло задание срочно переключиться с одной на другую. Клонировать репозиторий вариант, но может занять уйму времени и места. Вычищать лишние файлы не вариант. На помощь приходит worktree
: возможность держать несколько рабочих копий для одного репозитория. Из документации:
$ git worktree add -b emergency-fix ../temp master
$ pushd ../temp
# ... hack hack hack ...
$ git commit -a -m 'emergency fix for boss'
$ popd
$ git worktree remove ../temp
Клонирования не происходит, по сути просто чекаут в другую папку, которую потом можно оставить или не жалко удалить.
pull
только как fast-forward
На всякий случай, напоминаю, что pull
по умолчанию делает fetch
(выкачивание ветки с удалённого репозитория) и merge
(слияние локальной и удалённой веток), а fast-forward
— это режим слияния, когда нет никаких изменений в локальной ветке и происходит "перемотка" её на последний коммит из удалённой. Если изменения есть, то происходит классический мерж с ручным разрешением конфликтов и мерж-коммитом.
Некоторые предпочитают использовать git pull --rebase
, но не всегда это возможно, например, когда вы локально смержили другую ветку из origin
в master
и перед пушем делаете pull
(надеюсь, не надо напоминать, чем в данном случае может грозить rebase
).
Соответственно, чтобы не попасть случайно в ситуацию, когда Вы неудачным pull
-ом смержили не то и не туда, можно использовать параметр --ff-only
или вписать соответствую опцию в конфиг:
git config --global pull.ff only
Что мы получаем?
pull
не в ту ветку — например, на автомате вписали git pull origin
master
upstream
вместо my_feature
.git exclude
Обычно для скрытия файлов используется .gitignore
, но он практически всегда отслеживается в самом репозитории и любое его изменение приведёт к тому, что он будет считаться изменённым на нашей стороне или может привести к неожиданному скрытию новых файлов у всех остальных.
Для решения этого вопроса есть чудесная возможность добавить соответствующий паттерн в файл .git/info/exclude
. А для удобства редактирования этого файла можно использовать алиас:
git config --global alias.exclude '!f() { vim .git/info/exclude; }; f'
(Не забудьте подставить Ваш любимый редактор.)
.git/info/exclude
использует тот же синтаксис, что и .gitignore
..gitignore
распространяется только на неотслеживаемые (untracked) файлы. Уже отслеживаемые изменённые файлы будут "подсвечиваться" как и раньше. Если Вы добавили файл случайно и теперь хотите его скрыть (такое иногда бывает с локальными конфигами IDE, например, .vscode/settings.json
), используйте git rm <path> --cached
— команда удалит файл из отслеживаемых, но оставит его локальную копию нетронутой, и вот теперь её можно будет скрыть через exclude.git config --global core.excludesfile <path to global .gitignore>
А теперь про то, когда Вы не хотите чтобы отслеживались изменённые файлы. Яркий пример: очень многие, особенно долгоживущие репозитории, хранят в себе ряд конфигов. Часто они служат для обеспечения единообразия настроек (к примеру, .editorconfig
) или тасков сборки/линтинга (.vscode/tasks.json
). И иногда так случается, что хочется их как-то изменить, но возможность разделения конфигов на "общие" и "пользовательские" отсутствует.
Есть административный вариант решения проблемы: вынести все конфиги в отдельную папку, из которой каждый будет сам копировать конфиги в нужные места. И есть путь одиночки возможность заоверрайдить на месте и пометить файл как неизменённый:
git update-index --assume-unchanged <path to file>
С этих пор он "пропадает с радаров" даже если Вы продолжите его изменять. Если во время pull
-а приходят новые изменения в этом же файле — в этом случае он будет продолжать считаться неизменённым, но легко смержиться Вам не даст. Чтобы вернуть всё как было, надо снять флаг, добавив no
:
git update-index --no-assume-unchanged <path to file>
Теперь немного чёрной магии. Предположим, что Вы не только изменили конфиги, но и хотите сохранить их в истории, чтобы помнить, почему Вы так сделали, и иметь возможность переключаться между разными версиями. Или же хотите отслеживать файлы, которые в принципе игнорируются главным репозиторием. Суть одна, в основной репозиторий заливать их нельзя. stash
в этом может помочь, но когда разных изменений накапливается много, в них можно прострелить сломать ногу.
В моей практике было время, когда сборка проекта приводила к автогенерации части рабочих конфигов. Подставлять вручную такие, какие нужны для отладки, — замучаешься. Хотелось получить возможность быстро их чекаутить. Был вариант сделать репозиторий в папке ./out
— но оказалось, что постоянно переходить из папки в папку тоже неудобно.
Долго ли, коротко ли, узнал я о том, что вовсе необязательно, чтобы папка с репозиторием называлась .git
. Её можно назвать как угодно ещё на этапе создания репозитория и работать с ней, передавая в команды параметр gitdir
. А значит… Просто выполнить git init --separate-git-dir=.git_dev
в существующей папке нам не дадут, произойдёт переименование каталога. Поэтому делаем хитрее: выполняем команду в новой папке, и кладём свежесозданный репозиторий рядом с существующим.
Что только что сейчас произошло? Мистическим образом у нас оказалось два репозитория в одной папке, а значит и возможность вести параллельную историю файлов! Почему .git_dev
? Да для единообразия. Давайте заведём себе алиас, чтобы упростить работу со вторым репозиторием:
git config --global alias.dev '!git --git-dir=\"./.git_dev\"'
Пробуем:
> git status -s
?? .git_dev/
> git dev status -s
?? .git_dev/
?? .gitignore
?? Program.cs
?? habr.csproj
Со вторым репозиторием Вы теперь вольны делать всё что захотите. Можете отслеживать только отдельные конфиги, а можете вести полностью параллельную историю, добавляя в неё что-то своё и делая checkout
то одного, то другого (правда, не знаю зачем это может пригодиться, но вдруг Вы шизофреник сложный человек).
Игнорировать .git/
у гита заложено в генах, а вот всё остальное, как мы видим, отображается как есть. Наибольшая проблема — .gitignore
у них будет один на двоих, так что практически всё, что может потребоваться во втором репозитории, придётся добавлять через -f
, а всё что не требуется — не забываем игнорировать через .git_dev/info/exclude
. По умолчанию можно добавить следующие строчки:
# ignore all files
/*
# ignore all folders
*/
В качестве бонуса, саму идею использования git
для отслеживания конфигов можно использовать в том числе для того, чтобы хранить все свои заботливо собранные .vimrc
, .bashrc
, создавая репозиторий прямо в ~
(для Windows это C:\Users\%USERNAME%\
).
Про хуки много рассказано в других статьях, например и вот, но не упомянуть их нельзя. Благодаря git bash
они одинаково работают как в Unix-like системах так и в Windows, правда, если они при этом запускают что-то ещё, можно огрести приключений. Полезны, например, хуки:
Из любопытного, когда-то я себе ставил хук на чекаут, который писал название ветки в Hamster, что позволяло достаточно точно отслеживать когда и над чем я работал. А при использовании .git_dev
из предыдущего пункта можно настроить его автоматический чекаут после чекаута основного репозитория, чтобы всегда держать у себя "правильные" локальные версии конфигов.
Напоследок хочу сказать довольно банальную вещь — автодополнение существенно улучшает качество жизни. В большинстве Unix-систем оно идёт из коробки, но если Вас угораздило оказаться в инфраструктуре Windows — настоятельно рекомендую перейти на Powershell (если ещё не) и установить posh-git, который обеспечивает автодополнение большинства команд и даёт минималистичную сводку в prompt:
Спасибо за внимание; желаю всем приятной и эффективной каждодневной работы.
Бонус для внимательных. Упомянутые выше и несколько неупомянутых алиасов из конфига:
[alias]
# `git sshow hack` - показать содержимое стеша с названием, включающим "hack". Строка может быть неточной
sshow = "!f() { git stash show stash^{/$*} -p; }; f"
# `git sapply hack` - применить стеш "hack"
sapply = "!f() { git stash apply stash^{/$*}; }; f"
# работа с `.git_dev`
dev = !git --git-dir=\"./.git_dev\"
# отображение статуса одновременно `.git/` и `.git_dev/`
statys = "!f() { git status ; echo \"\n\" ; git dev status ; }; f"
# поиск ветки по части названия
findb = "!f(){ git branch -ra | grep $1; }; f"
# последние пять коммитов в ветке. Если вызвать как `git hist -n 10`, отобразит 10
hist = log --pretty=format:\"%ad | %h | %an: \t %s%d\" --date=short -n5
# `git dist branch-name` отображает разницу в списке коммитов между текущей веткой и branch-name
dist = "!git log --pretty=format:\"%ad | %h | %an: \t %s%d\" --date=short \"$(git rev-parse --abbrev-ref HEAD)\" --not "
# редактирование локального `exclude`
exclude = "!f() { vim .git/info/exclude; }; f"
# вывести список файлов, скрытых через `--assume-unchanged`
ignored = !git ls-files -v | grep "^[[:lower:]]"
# переход на следующий коммит - операция, обратная `git reset HEAD~1`
forward = "!f() { git log --pretty=oneline --all | grep -B1 `git rev-parse HEAD` | head -n1 | egrep -o '[a-f0-9]{20,}' | xargs git checkout ; }; f"