Недавно я пытался объяснить коллеге, какие у меня критерии при формировании пул реквеста — когда стоит объединять что‑либо в один пул реквест, а когда нет. И я заметил за собой фразу «ну, кроме…» несколько раз и решил записать, как я использую git — чтобы разобраться в особенностях моего подхода, как я мог бы улучшить его и, возможно, поделиться чем‑то полезным.
Поскольку это интернет, давайте сразу обговорим: то, как я использую git основывается на последних 12 годах работы в компаниях с относительно небольшими (до 50 человек) командами. В каждой из них мы использовали только git и GitHub; изменения выполнялись в отдельных ветках, предлагались в виде пул реквестов и сливались в основную ветку. В последние несколько лет, после введения GitHub squash‑merging, мы использовали его.
Я никогда не использовал какую‑либо другую систему контроля версий. Я не могу и не буду сравнивать git с Mercurial, jj, Sapling, и т. д.
Итак, вот как я использую git.
Технические подробности
Все находится в git, все время. Любой сайд‑проект, большой или малый, завершенный или брошенный, все находится в репозитории. Выполнение git init это первое, что я делаю в новой директории. Я не вижу причин не использовать git.
git — самая наиболее интересная часть моего шелл промта. Без него я чувствую себя будто голым. Он показывает текущую ветку и состояние репозитория, т. е. есть ли несохраненные изменения:
Когда кто-то просит меня помочь им с чем-то связанным с 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 revert или это также отменит 12 других не связанных с этим изменений, которые скорее всего не относятся к проблеме?
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. Скриншот дороже тысячи слов и десяти тысяч ссылок на другие тикеты. Скриншот это доказательство. Доказательство, что он действительно исправляет то, что было заявлено исправить, подтверждение, что ты действительно запускал этот код. И это тратит гораздо меньше времени, чем многие думают. Вот пример:
Если нужно, я ссылаюсь на другие коммиты или PR в сообщениях. Идея: оставлять крошки. Вместо «Исправляет неработающий парсинг» я стараюсь писать «Исправляет неработающий парсинг, после того как изменения в 3bac3ed ввели новое ключевое слово».
В Zed, когда работаем попарно, мы добавляем Co-authored-by: firstname <email>
к сообщениям коммитов, чтобы коммит был привязан к нескольким людям. Вот так:
Особенно когда дело касается сообщений коммитов, самое важное — контекст, контекст, контекст. Когда я работаю один, я использую другие сообщения коммитов чем когда я работаю с командой. Когда мы делаем ревью, это отличается от работы в паре.
С кем ты общаешься своим сообщением, когда и почему? Это те вопросы, которые должны формировать сообщение.
Когда я работаю один в личном репозитории, пытаюсь заставить 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!
Комментарии (7)
ganqqwerty
22.10.2024 16:45что насчет коммитов, код в которых не запускаются? Избегаешь?
eee
22.10.2024 16:45Боюсь, что автор не ответит, т.к. это перевод )
Лично мне самому пофиг на работоспособность проекта между коммитами, пуллреквест важнее.
ganqqwerty
22.10.2024 16:45о, неплохой перевод. Я чувствовал что-то в стиле, но подумал, что может автор просто много на английском говорит.
Politura
А на git fetch, git push, git pull алиасов нету? :) Эти три, плюс git commit, git checkout, git clone, git init, git remote, а все остальное все-таки удобнее и нагляднее делать через UI.
А когда через UI, то уже и мерж основной ветки вместо ребейса нормально работает. Ребейс хорошо чего-то маленького, а чем больше изменений, тем больше шансов, что твои изменения затрагивают файлы меняемые другими, в этом случае ребейс может быть головной болью, особенно когда основная ветка ушла на много коммитов.
Metotron0
git checkout же заменили на git switch, нет?
Politura
Ну, не заменили, добавили альтернативу с более очевидным именем. Кстати, ветки переключать тоже удобно в UI, через командную строку хорошо, когда в репе много веток и лень листать UI в поисках нужной.
ivvi
Зачем листать, если можно поиском фильтрануть