[Перевод] Как я использую git

Недавно я пытался объяснить коллеге, какие у меня критерии при формировании пул реквеста — когда стоит объединять что‑либо в один пул реквест, а когда нет. И я заметил за собой фразу «ну, кроме…» несколько раз и решил записать, как я использую git — чтобы разобраться в особенностях моего подхода, как я мог бы улучшить его и, возможно, поделиться чем‑то полезным.

Поскольку это интернет, давайте сразу обговорим: то, как я использую git основывается на последних 12 годах работы в компаниях с относительно небольшими (до 50 человек) командами. В каждой из них мы использовали только git и GitHub; изменения выполнялись в отдельных ветках, предлагались в виде пул реквестов и сливались в основную ветку. В последние несколько лет, после введения GitHub squash‑merging, мы использовали его.

Я никогда не использовал какую‑либо другую систему контроля версий. Я не могу и не буду сравнивать git с Mercurial, jj, Sapling, и т. д.

Итак, вот как я использую git.

Технические подробности

Все находится в git, все время. Любой сайд‑проект, большой или малый, завершенный или брошенный, все находится в репозитории. Выполнение git init это первое, что я делаю в новой директории. Я не вижу причин не использовать git.

git — самая наиболее интересная часть моего шелл промта. Без него я чувствую себя будто голым. Он показывает текущую ветку и состояние репозитория, т. е. есть ли несохраненные изменения:

b57bdd7a88faf737353da6c06fe2b6f0.png

Когда кто-то просит меня помочь им с чем-то связанным с git и я вижу, что у них нет информации о git в их промте, это первое, что я советую им сделать.

Я использую git в командной строке 99.9% времени. Я никогда не использовал GUI для git и не вижу смысла.

Единственное исключение: git blame. Для этого я всегда использую встроенный интерфейс текстового редактора или GitHub UI. Раньше на протяжении 10 лет я использовал функционал blame в vim‑fugitive. Сейчас поддержка git blame, добавленная в Zed.

Я использую git‑алиасы и shell‑алиасы так, словно возможный будущий артрит стоит за моей спиной, шепчет «скоро» мне на ухо и ждет каждого лишнего нажатия клавиши. Они хранятся в ~/.gitconfig и в моем.zshrc. Мои самые часто используемые алиасы, согласно atuin:

gst - for `git status`
gc — for `git commit`
co — for `git checkout`
gaa — for `git add -A`
gd — for `git diff`
gdc — for `git diff —cached`

Я их спамлю. Прямое соединение между мышечной памятью и клавиатурой, без участия мозга. Особенно gst для вывода git status — я постоянно использую его в качестве подтверждения, что то, что я делал сработало. Я добавляю файлы git add и выполняю gst, git add -p и снова gst и gdc, выполняю git restore и gst, git stash и gst.

К примеру, вот так я проверяю, какие изменения я только что внес, добавляю в стэйджинг и коммичу:

~/code/projects/tucanty fix-clippy X φ gst
# [...]
~/code/projects/tucanty fix-clippy X φ gd
# [...]
~/code/projects/tucanty fix-clippy X φ gaa
~/code/projects/tucanty fix-clippy X φ gst
# [...]
~/code/projects/tucanty fix-clippy X φ gdc
# [...]
~/code/projects/tucanty fix-clippy X φ gc -m "Fix clippy warnings"
~/code/projects/tucanty fix-clippy OK φ gst
# [...]

Почему? Я, честно говоря, не уверен — возможно, недостаток обратной связи от команд git, может быть потому что промт не говорит мне всю информацию, у меня нет UI и gst де‑факто и есть UI?

Я использую эту функцию pretty_git_log в~/.githelpers по сто раз за день. Я нашел ее в этом скринкасте Gary Bernhardt и не менял ее уже 12 лет. Она выглядит так:

Почему git lr, а не glr? Я ленивый и скорее всего никогда не привык бы к glr спустя годы использования git lr.

Почему git lr, а не glr? Я ленивый и скорее всего никогда не привык бы к glr спустя годы использования git lr.

Фиксация изменений

Что и как часто я коммичу основывается на том, что должно оказаться в основной ветке репозитория, над которым я работаю. Коммит? Сквош коммит? Серия коммитов? Вот под что я подстраиваюсь.

То, что оказывается в основной ветке должно быть:

  1. Легко понятно другим как самостоятельное изменение.

  2. Откатываемо. Если я ошибусь в процессе внесения изменений и пойму это уже после слияния, могу ли я отменить изменение с помощью git revert или это также отменит 12 других не связанных с этим изменений, которые скорее всего не относятся к проблеме?

  3. Bisectable. Если мы заметим, что регрессия проскочила в основную ветку на прошлой неделе, будет ли легко ее найти, если мы пройдемся по коммитам и протестируем их? Или нам придется сказать «это появилось в этом коммите», а сам коммит — 3 тысячи измененных строк, в которых обновили зависимость OpenSSL, изменили рекламный текст, подправили настройки таймаута в стандартном клиенте HTTP, добавили миграцию базы данных, изменили бизнес‑логику и обновили стандартный логгер? Это то, чего я хотел бы избежать.

Я не думаю, что все три могут быть достигнуты в 100% случаев, но общий посыл — легко ли это отменить? легко ли это дебажить в случае регрессии? — это то, что я стараюсь держать в голове, решая, добавить ли что‑либо в отдельный пул реквест или отдельный коммит.

Я делаю коммиты рано и часто. Мой подход:  быстрое сохранение в игре. Пережил тех трех зомби, спрятанных за углом? Сохранился. Починил тот неприятный баг, который потребовал изменений, которые ты не особо понимаешь, но оно сработало? Сохранился. Сохранился и потом думаешь, как сделать это правильно.

Коммиты и их историю в моей ветке я вижу гибко. Я всегда могу переформулировать их, объединить, переместить — до тех пор, пока я не отдал их на ревью, до тех пор, пока они «мои».

Почему? Потому что почти в каждом репозитории, в котором я работал (кроме open‑source репозиториев, в которые я вносил вклад), объединенный пул реквест это то, что оказывается в основной ветке, а не коммит.

Так что я делаю коммиты так часто и много, как захочу и затем убеждаюсь, что объединенный пул реквест соответствует моим трем критериям. Что закономерно приводит нас к…

Pull Request’ы

Объединенный пул реквест более важен, чем коммит в ветке, поскольку именно он оказывается в основной ветке и именно его стоит оптимизировать.

Если мы используем сжатые коммиты при объединении, в результате слияния получится один коммит и я буду думать о том, как должен выглядеть этот коммит и легок ли он для понимания, отката и поиска.

Если сжатые коммиты не используются и мержатся все коммиты из ветки в основную, тогда я уже буду думать о том, как они должны выглядеть. В таком случае я могу воспользоваться интерактивным rebase в своей ветке и объединить коммиты так, чтобы они соответствовали частям проделанной работы и их было легко найти, понять и при необходимости откатить.

Ревью создают исключения из данных правил, поскольку требования коллег или ревьюеров стоят выше моих. К примеру, если ревью PR проводится по каждому коммиту, я потрачу больше времени на их оформление. Если PR ревьюится как одно изменение, где изменилось три строки в двух файлах, я не вижу проблем добавить коммит «исправил форматирование» и проигнорировать сообщение.

Общее правило между тем остается: для меня действительно важен конечный PR, как его будут ревьюить и во что он превратится после слияния, а не индивидуальные коммиты на пути к ревью и слиянию.

Я открываю PR очень рано. Прямо с первого коммита. Раньше я помечал их как «WIP», добавляя это как префикс в названии, но теперь у нас есть статус черновика в GitHub. Я открываю их рано, потому что после отправки изменений, пока я продолжаю работать, CI также начинает работать. Я получаю результаты долго‑выполняющихся наборов тестов,  линтеров, проверок стиля и других вещей, которые выполняются в CI, пока я продолжаю работать.

Мой подход: небольшие PR — быстро принимают. Иногда они на 3 строчки кода. Иногда на 300. Практически никогда на 3000. Если они открыты больше недели, это уже звоночек.

Пример: предположим, я работаю над фичей, которая изменяет отображение интерфейса пользовательских настроек. Пока я работаю, я замечаю, что необходимо изменить механизм парсинга параметров. Это изменение на две строчки. Я возьму это изменение на две строчки и помещу его в отдельный от изменений UI PR, даже если оно потребовалось в рамках работы над UI. Почему? Потому что если два дня спустя кто‑то скажет «что‑то не так с нашим парсером настроек», я хочу иметь возможность быстро определить, что дело в изменениях UI или в изменениях парсера и откатить то или иное изменение.

Вместо мерджа основной ветки в мою, я делаю rebase моих PR к основной ветке. Почему? Потому что когда я использую git lr (алиас для показа git log) я хочу видеть коммиты, сделанные в моей ветке. Я думаю, что чище делать rebase на последнюю версию main. Мне не нравятся мерж‑коммиты в моей ветки. Интерактивный rebase также позволяет просмотреть все мои коммиты и понять, что происходит в ветке.

Не волнует ли меня уничтожение изначальной истории коммитов, когда я делаю rebase? Опять же: единица работы это объединенный PR и меня не волнует, отражают ли коммиты в моей ветке то, что происходило во время работы. Важно то, что окажется в основной ветке и если мы используем squashed commit, вся эта чистая история будет в любом случае утеряна.

Но, опять же, возникают исключения из‑за ревью и требований ревьюеров — иногда я делаю интерактивный rebase в моей ветке, чтобы объединить или отредактировать коммиты, чтобы их было легче ревьюить, несмотря на то, что они будут объединены два часа спустя.

Я также использую PR в моих пет‑проектах даже если я единственный, кто работает над ним и даже если я всегда буду единственным участником. Я не делаю это для каждого изменения, но иногда да, поскольку мне нравится отслеживать более крупные изменения в UI GitHub. Похоже, какой‑то UI я все‑таки использую?

Commit Messages & Pull Request Messages

Я уделяю внимание сообщениям коммитов, но не слишком много. Меня не волнуют префиксы, формулы и т. д. Меня волнуют хорошо написанные сообщения. Я прочитал A Note About Git Commit Messages от Tim Pope в 2011 и с тех пор не забывал.

Если мы объединяем коммиты перед слиянием, тогда описание PR и будет сообщением для этого PR и я трачу больше времени на его написание.

Самое важное в сообщении коммита или PR — »почему»Что» мы можем увидеть в diff (хотя иногда короткое описание может быть полезно), но когда я читаю ваше сообщение коммита я хочу увидеть почему вы внесли это изменение. Потому что обычно, сообщения коммитов читают когда что‑то случилось.

Я думаю, что такие вещи как Conventional Commits это по большей мере пустая трата времени. Команды впустую тратят время на выбор правильных префиксов коммитов с крайне небольшой пользой. Когда я пытаюсь найти источник регрессии по истории коммитов, я в любом случае буду проверять каждый коммит, так как мы знаем, что да, регрессия может быть даже в коммите [chore]: fix formatting.

Иногда я добавляю префиксы к сообщениям коммитов или названиям PR, например «lsp:» или «cli:» или «migrations:». Но по большей мере это делается для сокращения сообщения. «lsp: Ensure process is cleaned up» короче «Ensure language server process is cleaned up» и передает по сути тот же смысл.

По возможности я стараюсь включить демо‑видео или скриншот в PR. Скриншот дороже тысячи слов и десяти тысяч ссылок на другие тикеты. Скриншот это доказательство. Доказательство, что он действительно исправляет то, что было заявлено исправить, подтверждение, что ты действительно запускал этот код. И это тратит гораздо меньше времени, чем многие думают. Вот пример:

0985949da8fa7e0bb7e1fec80869deab.png

Если нужно, я ссылаюсь на другие коммиты или PR в сообщениях. Идея: оставлять крошки. Вместо «Исправляет неработающий парсинг» я стараюсь писать «Исправляет неработающий парсинг, после того как изменения в 3bac3ed ввели новое ключевое слово».

В Zed, когда работаем попарно, мы добавляем Co-authored-by: firstname к сообщениям коммитов, чтобы коммит был привязан к нескольким людям. Вот так:

b218f8368f4f1221c472bcd703170890.png

Особенно когда дело касается сообщений коммитов, самое важное — контекст, контекст, контекст. Когда я работаю один, я использую другие сообщения коммитов чем когда я работаю с командой. Когда мы делаем ревью, это отличается от работы в паре.

С кем ты общаешься своим сообщением, когда и почему? Это те вопросы, которые должны формировать сообщение.

Когда я работаю один в личном репозитории, пытаюсь заставить CI работать, вы почти точно увидите сообщения коммитов из одной буквы в main. Но даже если я работаю один, если я пофиксил неприятный баг, я напишу красивое сообщение. Когда работаю с другими, я стараюсь писать сообщения, которые объяснят им, что я пытался сделать и зачем.

Ревью

Перед тем, как попросить кого-то о ревью моего PR, я сам читаю diff на странице pull request. Почему-то чтение вне своего редактора позволяет лучше замечать баги и оставшиеся вызовы print.

Я стараюсь не просить ревью, пока CI не отработал успешно. Исключение: я знаю, как поправить CI и мы можем параллельно работать над ревью и исправлением CI.

Когда я делаю ревью чужого кода, я всегда стараюсь скопировать код к себе, запустить его и убедиться, что он делает то, о чем заявлено в PR. Вы бы удивились, как часто это не так.

Workflows

Основной воркфлоу всегда одинаков, когда я работаю с кем‑то: открываю свою ветку, начинаю работать, делаю коммиты рано и часто, отправляю рано и часто, открываю PR как черновик как можно раньше, завершаю работу, убеждаюсь, что коммиты в ветке более‑менее имеют смысл, запрашиваю ревью, объединяю с main.

Когда я работаю один, 99% коммитов происходят в основной ветке и отправляются сразу.

Иногда, работая над веткой, я замечаю, что мне нужно сделать новый коммит в отдельной ветке, чтобы превратить его в отдельный PR. Есть несколько подходов, которые я использую в таком случае.

  • git add -p && git stash то, что я хочу закоммитить в ветке A потом, переключаюсь на новую ветку B с основной, делаю коммит в ней, отправляю.

  • git add ‑p && git commit то, что я хочу оставить в этой ветке. git stash то, что я хочу поместить в другую ветку, переключаюсь между ветками, git stash pop, делаю коммит.

  • git add -p && git commit -m "WIP” того, что я хочу оставить в этой ветке. Затем, опять же, убираю в stash то, что хочу перенести в новую ветку, перехожу туда, делаю коммит. Затем возвращаюсь в первоначальную ветку, отменяю коммит «WIP» делая git reset —soft HEAD~1 и возвращаюсь к работе.

  • git add -p то, что я хочу переместить в другую ветку, затем git stash и git reset --hard HEAD, чтобы выбросить то, что не стоит того, чтобы оставить. Меняю ветки, git stash pop, делаю коммит.

  • Иногда я превращаю изменения в два разных коммита в одной ветке, переключаюсь на новую и перемещаю один из них в нее с помощью git cherry-pick. Затем возвращаюсь на старую ветку, делаю git rebase -i и убираю перемещенный коммит.

Когда я выбираю ту или иную стратегию? Это зависит от размера изменений, которые я хочу перенести в другую ветку и как много не включенных в коммит изменений сейчас в рабочей директории.

Я не особо уделяю внимание названиям веток, пока они несут какой-то смысл. Я использую GitHub UI чтобы получить обзор открытых мной pull request«ов (этот URL — быстрая ссылка в Raycast, так что я могу просто ввести «prs» в Raycast и открыть URL). Это помогает мне понимать, какие PR сейчас в процессе работы и какие готовы к слиянию.

Я создаю PR либо переходя по ссылке, показанной после выполнения git push на GitHub, либо выполняя команду gh pr create -w. Это, пожалуй, основное, для чего я использую GitHub CLI.

Еще одна вещь, для которой я использую gh это переключение между ветками открытых PR. Особенно когда я смотрю PR от сторонних участников, которые находятся в их форках.

У меня также есть эти два удобных алиаса, позволяющих переключаться между PR с помощью fzf и я бы хотел вспоминать о них чаще.

Прошло много лет с тех пор, как мне приходилось в последний раз удалять и повторно клонировать репозиторий из-за проблем с git. Сейчас я могу решить большую часть проблем, используя git reflog, немного git reset и синей изоленты.

Вот и все — вот так я использую git!

© Habrahabr.ru