Pull to refresh

В одной лодке с «ублюдком»: 11 продвинутых советов по использованию Git

Reading time11 min
Views51K

*"ублюдок" — вольный перевод слова "git" — "an unpleasant or contemptible person", "неприятный или презренный человек".



В комментариях к статье 15 базовых советов по Git для эффективной работы каждый день развернулась дискуссия на тему эффективности использования тех или иных команд и опций. Надо признать, что git предоставляет столько различного функционала, что во-первых, за всем становится невозможно уследить, а во-вторых, его можно совершенно по-разному вписывать в рабочий процесс.


Давайте посмотрим, что можно использовать, чтобы улучшить себе жизнь. Статья предполагает, что читатель умеет пользоваться основными возможностями git и понимает что делает, когда, скажем, вводит в консоль git rebase --merge --autostash.


1. Используйте консольный и графический интерфейсы git одновременно


Начнём с того, как именно Вы пользуетесь возможностями git? Многие работают строго из консоли или из приложения вроде SourceTree, и с первого взгляда может показаться, что эти варианты взаимоисключают друг друга.


Многие продвинутые редакторы, в частности, Ваш любимый vim мой любимый VS Code, предоставляют как удобный доступ к консоли, так и приятный графический интерфейс для систем контроля версий, что позволяет в равной мере использовать плюсы обоих вариантов.


Удобства графического интерфейса очевидны невооружённым взглядом:



  • Вы наглядно видите изменения в файле сразу в своём любимом редакторе.
  • Вы можете контролировать стейджинг (добавление файлов для коммита) текущих изменений практически в реальном времени, не обращаясь к 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 — эта команда позволяет быстро сравнить текущий файл с его версией в любой ветке.
  • Если хотите пометить для себя место в каком-либо файле, чтобы иметь возможность быстро к нему вернуться в процессе работы над текущей веткой — просто добавьте в нужное место пустую строчку. Таким образом файл всегда будет под рукой в боковой панели. Да, для закладок существует богатый ассортимент плагинов, но обычно списки закладок быстро захламляются и в них бывает сложно ориентироваться, когда смешиваются закладки из разных веток.

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.sapply "!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"

    # вывести список файлов, скрытых через `--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"
Tags:
Hubs:
+95
Comments26

Articles

Change theme settings