*"ублюдок" вольный перевод слова "git" "an unpleasant or contemptible person", "неприятный или презренный человек".
В комментариях к статье 15 базовых советов по Git для
эффективной работы каждый день развернулась дискуссия на тему
эффективности использования тех или иных команд и опций. Надо
признать, что git
предоставляет столько различного
функционала, что во-первых, за всем становится невозможно уследить,
а во-вторых, его можно совершенно по-разному вписывать в рабочий
процесс.
Давайте посмотрим, что можно использовать, чтобы улучшить себе
жизнь. Статья предполагает, что читатель умеет пользоваться
основными возможностями git
и понимает что делает,
когда, скажем, вводит в консоль git rebase --merge
--autostash
.
1. Использовуйте консольный и графический интерфейсы
git
одновременно
Начнём с того, как именно Вы пользуетесь возможностями git? Многие работают строго из консоли или из приложения вроде SourceTree, и с первого взгляда может показаться, что эти варианты взаимоисключают друг друга.
Многие продвинутые редакторы, в частности, Ваш любимый
мой любимый VS Code, предоставляют как
удобный доступ к консоли, так и приятный графический интерфейс для
систем контроля версий, что позволяет в равной мере использовать
плюсы обоих вариантов.vim
Удобства графического интерфейса очевидны невооружённым взглядом:
- Вы наглядно видите изменения в файле сразу в своём любимом редакторе.
- Вы можете контролировать стейджинг (добавление файлов для
коммита) текущих изменений практически в реальном времени, не
обращаясь к
git status
. - Вы получаете быстрый доступ к истории файла.
- и многое другое, что зависит от конкретных редакторов и/или используемых плагинов.
Удобства использования git
из консоли не всегда
очевидно для тех, кто привык пользоваться графическим
интерфейсом.
- В первую очередь, это поддержка всех возможных команд и опций,
поскольку
git
в первую очередь консольная команда, а любой GUI это посредник между ним и Вами. - Подсказки по этим "всем возможным командам и опциям".
- Возможность применить эти команды и опции в любой комбинации. В
графических интерфейсах часто "выведен наружу" только базовый набор
команд, а всё что сложнее спрятано где-то внутри. Консоль
предоставляет одинаково
неудобный интерфейс для всех команд. - Подробный лог выполнения команд, описание ошибок и способы как
их исправить. Банальный вывод неудачного
git pull --ff-only
при наличии входящих изменений в отредактированных файлах сразу можно увидеть в каком файле есть несовместимые изменения и заняться мержем веток вручную:> git pull origin master --ff-onlyFrom ../habr2* branch master -> FETCH_HEADerror: Your local changes to the following files would be overwritten by merge: .gitignorePlease commit your changes or stash them before you merge.AbortingUpdating 6d1c088..a113bf7
Соответственно, когда у вас есть доступ и к консоли, и к боковой панели, можете успешно использовать лучшее из обоих миров так, как будет удобно именно Вам.
Применительно к VS Code с установленным плагином GitLens хочу поделиться парочкой специфичных лайфхаков.
- Привяжите хоткей к команде
gitlens.diffWithBranch
эта команда позволяет быстро сравнить текущий файл с его версией в любой ветке. - Если хотите пометить для себя место в каком-либо файле, чтобы иметь возможность быстро к нему вернуться в процессе работы над текущей веткой просто добавьте в нужное место пустую строчку. Таким образом файл всегда будет под рукой в боковой панели. Да, для закладок существует богатый ассортимент плагинов, но обычно списки закладок быстро захламляются и в них бывает сложно ориентироваться, когда смешиваются закладки из разных веток.
2. Конфиги, конфиги, конфиги. Разделяйте и властвуйте
Да, именно три штуки: системный (--system
),
пользовательский (--global
) и локальный
(--local
). Соответственно, они применяются в порядке
иерархии, каждый последующий оверрайдит предыдущий системный
применяется для всех пользователей, пользовательский конкретно для
Вас, локальный для конкретного репозитория. Ловко лавируя между
ними, можно гибко адаптировать свой рабочий процесс под условия
окружающей среды.
(Upd. Как оказалось, есть ещё четвёртый уровень конфигов, специфичный для отдельных рабочих копий одного репозитория worktree. Подробнее про worktree см. ниже.)
Когда какой стоит применять? В большинстве случаев случаев Вы
предпочтёте воспользоваться глобальным, чтобы вынести в него общие
для всех настройки core.eol
, алиасы, а также
user.name
и user.email
, переопределяя
только специфические вещи для конкретного репозитория. Однако в
некоторых случаях, например когда несколько разработчиков по
очереди отлаживают встраиваемое ПО, часть общих настроек имеет
смысл вынести на системный уровень, переопределяя в глобальном (==
пользовательском) только
user.name
/email
.
Также в моей практике был случай, когда в рабочих репозиториях
надо было пользоваться строго рабочей почтой, при этом в своих
локальных репозиториях я продолжал пользоваться личной. Чтобы даже
случайно нельзя было перепутать где что, я удалил
user.name
/email
из глобального конфига,
каждый раз указывая их заново в локальном, держа процесс под
контролем.
3. Используйте временные коммиты вместо stash при переходе между ветками
Скорее всего, Вы сталкивались хотя бы раз с ситуацией, когда
надо срочно переключиться с одной ветки на другую, бросив всё в
разобранном состоянии. Очень вероятно, что Вы знаете про 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.exclude "!f() { git stash apply stash^{/$*}; }; f"# сохранитьgit stash save "hack"# посмотретьgit sshow "hack"# применитьgit sapply "hack"
4.
Используйте "-
" для возврата к предыдущей ветке
После того, как вы сделали свои грязные дела в другой
ветке и хотите вернуться к предыдущей, вместо того чтобы вспоминать
и вводить её полное имя, можно просто передать
"-
":
git checkout -
Пользователям Windows этот трюк иногда совершенно незнаком, а
для пользователей Linux он может быть привычен по аналогичному
использованию в bash
:
cd /some/long/path...cd -
5. Не клонируйте репозиторий, когда в этом нет нужды
Предположим, у вас есть две принципиально несовместимые друг с другом ветки например, когда создаётся множество временных неотслеживаемых файлов кэша, при переходе между ветками можно замучиться их вычищать (передаю привет 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
Клонирования не происходит, по сути просто чекаут в другую папку, которую потом можно оставить или не жалко удалить.
6. Применяйте
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
. - Успех, когда всё прекрасно.
7. Скрывайте лишнее
через 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>`.
8. Скрывайте локальные изменения, когда не хотите их "вливать"
А теперь про то, когда Вы не хотите чтобы отслеживались
изменённые файлы. Яркий пример: очень многие, особенно
долгоживущие репозитории, хранят в себе ряд конфигов. Часто они
служат для обеспечения единообразия настроек (к примеру,
.editorconfig
) или тасков сборки/линтинга
(.vscode/tasks.json
). И иногда так случается, что
хочется их как-то изменить, но возможность разделения конфигов на
"общие" и "пользовательские" отсутствует.
Есть административный вариант решения проблемы: вынести все
конфиги в отдельную папку, из которой каждый будет сам копировать
конфиги в нужные места. И есть путь одиночки возможность
заоверрайдить на месте и пометить файл как
неизменённый:
git update-index --assume-unchanged <path to file>
С этих пор он "пропадает с радаров" даже если Вы продолжите его
изменять. Если во время pull
-а приходят новые
изменения в этом же файле в этом случае он будет продолжать
считаться неизменённым, но легко смержиться Вам не даст. Чтобы
вернуть всё как было, надо снять флаг, добавив no
:
git update-index --no-assume-unchanged <path to file>
9.
Отслеживайте хаки локальные изменения в обход
репозитория
Теперь немного чёрной магии. Предположим, что Вы не только
изменили конфиги, но и хотите сохранить их в истории, чтобы
помнить, почему Вы так сделали, и иметь возможность переключаться
между разными версиями. Или же хотите отслеживать файлы, которые в
принципе игнорируются главным репозиторием. Суть одна, в основной
репозиторий заливать их нельзя. 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%\
).
10. Используйте хуки
Про хуки много рассказано в других статьях, например и
вот, но не упомянуть их нельзя. Благодаря git
bash
они одинаково работают как в Unix-like системах так и в
Windows, правда, если они при этом запускают что-то ещё, можно
огрести приключений. Полезны, например, хуки:
- прогоняющие код через линтер/автоформаттер перед коммитом;
- вычленяющие номер задачи из текущей ветки и добавляющие её в сообщение коммита;
- пересобирающие вспомогательные библиотеки после чекаута и мержа.
Из любопытного, когда-то я себе ставил хук на чекаут, который
писал название ветки в Hamster,
что позволяло достаточно точно отслеживать когда и над чем я
работал. А при использовании .git_dev
из предыдущего
пункта можно настроить его автоматический чекаут после чекаута
основного репозитория, чтобы всегда держать у себя "правильные"
локальные версии конфигов.
11. Требуйте автодополнение
Напоследок хочу сказать довольно банальную вещь автодополнение существенно улучшает качество жизни. В большинстве 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" # переход на следующий коммит - операция, обратная `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"