Есть два непримиримых лагеря. Одни говорят: ты должен качественно и грамотно описывать свои коммиты. Каждый коммит — это законченный, осмысленный кусочек работы. Если ты не можешь сделать понятное и простое описания коммита, значит у тебя неправильные коммиты.
Другие считают — ты делаешь коммиты как хочешь, это часть твоего личного рабочего процесса. А вот пул реквесты описываются детально: что сделано, как сделано, зачем сделано. Как протестировано, какую проблему решает, на что стоит обратить внимание.
Я убежденный сторонник второго подхода — мне неудобно бить свою работу на микрокусочки. Я беру небольшую задачу и хаотично мечусь по кодовой базе, экспериментируя и внося изменения в том порядке, в котором получается. Если бы я мог нажать на кнопку, и моя работа переструктурировалась бы на хорошие коммиты — я бы на неё нажал. Но кнопки нет, а ломать себя мне не хочется. В то же время я достиг определенного мастерства в описании пул реквестов. Мне довелось фигачить код в майкрософт (через аутстаф, не считается), и там я почерпнул топовые стандарты оформления пул реквестов. С годами работы я только развивал эту практику. Обычно мне удавалось убеждать команду использовать именно такой подход.
Но на последней работе мне достался девлид — убежденный сторонник детальных коммитов. О, мы долго спорили. Моё подчиненное положение сыграло свою роль, совковскую привычку соглашаться с главным нелегко заткнуть. Я не был так категоричен, как обычно бываю, и был уложен на обе лопатки. Уложен, но не убежден.
Если вдруг разрабы объединятся в единое правительство, установят правила и начнут объявлять определенные подходы вне закона — гражданская война начнется на следующий же день. Все потому что в мире разработчиков существует вера в объективное и измеримое. В не зависящую от человеческого восприятия истину. И если все это существует, значит кто-то из спорящих точно объективно не прав.
Мой первый серьёзный рабочий спор произошёл довольно рано. Я тогда ещё был зелёным джуном, который думает что он аппер мидл, да и вообще очень крутой и умный разраб. Один настоящий разраб из моей команды закинул ПР. У нас была практика, что все члены команды делают код ревью. Я открыл код, и почти сразу увидел проблему. Человек написал тест к какой-то функции, которая принимала число. Так вот он скормил ей 0, 1000, и random(0, 1000).
Я в то время очень остро чувствовал, что другие тиммейты видят во мне глупого новичка. И выжидал момента, чтобы размазать их своим видением. Мне повезло — рандом в тестах!
Я не очень-то разбирался в теории юнит-тестирования, но я почитал пару книжек, и твёрдо запомнил — один и тот же юнит-тест на одной и той же кодовой базе должен давать один и тот же результат. Я потратил около часа на продумывание комментария, чтобы он не выглядел токсично, но явно давал понять — только мартышка, которую буквально вчера научили считать, сможет додуматься до такого решения. В итоге он конечно же смотрелся предельно глупо, как и всё, что из себя выдавливают вчерашние стажёры, возомнившие себя разработчиками.
На следующий день я понял, что открыл портал в ад. Под моим комментарием было ещё сто. Весь остаток дня вместо команды из шести разработчиков мы были командой из шести обезьян. Мы спорили на английском в самом ПРе, хотя все были русскими. Когда запаса слов не хватало — созванивались. Кидались друг в друга ссылками, цитатами из книг, исследованиями, переходили на личности. На один мой коммент коллега ответил скриншотом из английского словаря, чтобы высмеять мой английский. Но мы так и не пришли к компромиссу.
Вопрос был сложный. С одной стороны, мы все вообще не знали, за что отвечает этот параметр, наша функция передавала его в другую, из интёрнал либы, к которой не было доки. Поэтому, теоретически, функция могла свалится на любом значении, а значит тест мог это поймать, и тогда мы бы смогли предвосхитить ошибку на проде. С другой стороны, у нас и так была проблема рандомно валящихся билдов. Из-за этого, если проект билдился локально и валился на сиае, мы тупо заказывали новый билд, и не смотрели никакие логи. Ведь билд итак занимал четыре часа, а пулл реквест твой никто и не посмотрит, пока он не прошёл CI.
Кроме того тачки часто были заняты — гораздо прагматичнее было заказать билд, пока он ещё заказывается, чем искать потенциальную «проблему» (которая почти всегда оказывалась отлетевшим таймаутом в интеграционном тесте). Это означало, что даже если бы рандомный тест свалился — мы бы не обратили внимания.
Всё разрешилось на следующем дейлике. Мы созвонились всей командой, и полчаса продолжали спорить — на русском. Мы не заметили, как наш американский менеджер-разработчик присоединился к звонку, и продолжали на повышенных тонах громить друг друга аргументами. Пока не услышали «Hey guys, it is absolutely not important. Just merge it as is». Я не знаю, сколько времени он слушал нас до этого, и как понял, о чём мы говорим, но спорить мы резко перестали. Вмержили и забыли об этом. Разошлись с миром, но не переубежденными.
С тех пор я набрался опыта, чтобы понимать — да насрать вообще, рандом, фор, или граничные кейсы. Это чёртов юнит тест, будет мешать — перепишем. Всё лучше чем спорить весь день. Но если представить, что я в идеальном мире, где необходимо всегда принимать только более верные решения — я не знаю, что делать. Мы вмёржили тест с рандомом, и сейчас я это понимаю как выбор в пользу надёжности, но в ущерб удобству разработки.
На днях я столкнулся с похожей дилеммой. Я игрался с новым тайпскриптом, и написал штуку, которую можно использовать вот так
// LessThan<T> и MoreThan<T> - это и есть то, что я написал.
// Типы, с помощью которых можно ограничивать входной параметр функции
// вот тут я объявляю некую фн,
// которая принимает число, меньшее чем 100
const consumeNumberLessThan100 = (v: LessThan<100>) =>
doWork(v);
// а тут фн принимает число, большее чем 100
const consumeNumberMoreThan100 = (v: MoreThan<100>) => doWork(v);
const sample = (x: number) => {
// тут с помощью функции check я проверяю,
// меньше ли x чем 100
// кстати "<" и ">" чекаются компилятором,
// скормить вместо этих символов что-то другое не получится
const lessThan100 = check(x,'<', 100);
if (lessThan100) {
// здесь я точно знаю - да меньше
// потому что в этой ветке исполнения
// компилятор тайпскрипта знает что, у lessThan100 тип - LessThan<100>
// вот этот код работает
consumeNumberLessThan100(lessThan100);
// а здесь - ошибка компиляции, функция ждёт число,
//для которого доказано, что оно > 100.
consumeNumberMoreThan100(lessThan100);
}
const moreThan100 = check(x, '>', 100);
if (moreThan100) {
consumeNumberMoreThan100(moreThan100); // работает
consumeNumberLessThan100(moreThan100); // ошибка
}
consumeNumberLessThan100(x); // ошибка
consumeNumberMoreThan100(x); // ошибка
}
Я подумал, блин, это очень круто — так можно ловить большое количество ошибок на более ранней стадии. Сам дизайн моих инструментов ограничения значений ужасен, но пока это просто концепт. В дальнейшем его можно легко расширить, построить мощный DSL и формулировать действительно сложные условия соответствия параметра, гарантированные на стадии компиляции. Я прикинул, как оно может увеличить надёжность любой кодовой базы, воспрял духом, и разослал сниппет разным знакомым разрабам.
Мнения опять разделились, причём сильно не в мою сторону. Переусложнение, оверинжиниринг, неподдерживаемо, все будут глушить твой гард с помощью каста к эни. Не читается. Хорошо, как эксперимент — плохо в настоящем проекте. Примеры использования высоссаны из пальца. Займись уже делом, Фил.
Сторонники подхода говорили, да, так надёжней. Чем раньше ловишь ошибку, тем лучше. Кроме того, если ты пишешь функцию, которая работает с ограниченным числом, проверку всё равно придётся писать, но она будет работать только в рантайме, и увеличит количество кода в теле функции.
Сейчас я немного умнее, чем раньше, и научился слышать аргументы. Я представил себе, как в реальном проекте пишу функцию, защищённую моим типом. Как все, кто её использует спрашивают меня, что это за херня. Как, прошарив фишку, они начинают обмазывать кодовую базу кастомным, тухло выглядящим DSL-ем. Как мы по триста раз проверяем значения, которые на самом деле никогда не будут превышать допустимое. Подход действительно ужасен в использовании, какие-то проблемы можно решить или сгладить, но например вот такой кейс
consumeNumberLessThan100(90);
не сгладить никак. Мне придётся доказывать компилятору, что 90 меньше 100. Я снабжу либу асертами, и получится
consumeNumberLessThan100(assert(90, '<', 100));
Выглядит не очень-то круто. Все аргументы «против» — в кассу, но аргументам «за» они не противоречат. Получается дилемма — удобство разработки, или надёжность. Вот тут мы попадаем в ловушку, мы начинаем думать, что надо посчитать, что там за удобство и что там за надёжность. Но удобство и надёжность в разработке, очень, очень сложные вещи. Они состоят из тысяч параметров.
Например удобство, это когда IDE комплитит за тебя код по паре введённых символов, когда код легко читается, когда не заглядывая в доку ты можешь поменять функциональность метода. Когда компилятор не нагружается статическим анализом, билд проходит быстро, текстовый редактор мгновенно рендерит символы. Но когда компилятор обнаруживает и подсвечивает тебе ошибку — это тоже удобство. А ещё это надёжность. Которая в свою очередь собирается из огромного числа совершенно разных вещей.
Ты должен сесть, и посчитать, как долго просуществует проект, в какую сторону он пойдёт, сколько раз в истории человечества кто-то вызовет тот или иной метод твоей кодовой базы, какие разработчики будут здесь работать. Это бесконечный список вопросов, на добрую половину которых невозможно просчитать правильный ответ. Только угадать.
Однажды друг попросил меня подготовить технические вопросы для интервью с Андреем Бреславом. Я пошёл в доку котлина, чтобы найти спорные моменты в дизайне. Их там, как и везде, тьма. Но больше всего меня заинтересовал подход к обработке исключений. Обработчик исключений в Котлине — это выражение, вполне себе функциональный и надёжный подход. Вот только обрабатывать ошибки не обязательно. Что роняет всю надёжность на ноль. А интересно это потому, что прямо в доке разработчики не поленились объяснить свой выбор: «есть исследования, что обязательная обработка ошибок сильно снижает производительность разработчиков при незначительном снижении ошибок».
Я охренел, как ты можешь писать код, если понятия не имеешь, что делать в случае, когда он сработает неправильно!? Если ты не знаешь, как обработать исключение, то не обрабатывать — не решение проблемы, а откладывание. В какой-то момент код свалится, и проблема, которая существует всегда, нанесёт ущерб, который можно было предвосхитить.
Но логичный аргумент против не нужен, можно обойтись просто статистикой. Для разработчиков котлина, исследования бьют логику, потому что у них есть философия. Их философия — прагматизм. Железный, непрошибаемый прагматизм, последовательно встроенный во все особенности этого языка программирования. Точно так же поступают идеалисты, которые пилят хаскели/идрисы, и говнокодеры, которые пишут голанг. Философия разумного компромисса царит в кодовой базе F#. И нет ощущения, что кто-то из них прав, а остальные — дураки.
Все эти люди значительно умнее и опытнее таких как я, и они, похоже давно поняли — ты вырабатываешь себе философию, а потом просто следуешь ей. Потому что киллер фича любой философии — резолвить дилеммы.
И так философия появилась во всем в ИТ, а философия — это полная противоположность идее единой и истинной объективности, потому что философия предлагает выбрать для себя истину субъективную.
У каждого из нас есть своя философия — она не описывается одним словом, это сложный набор паттернов для принятия решений. И мы часто их меняем или расширяем. На практике получается, что у тебя есть проект со своей философией, который пишется на языке программирования со своей философией, с помощью фреймворков и инструментов, в каждом из которых есть своя философия. И пишут этот проект разработчики, каждый со своей философией. И каждый раз, когда надо принять какое то решение, только стечение обстоятельств влияет на то, какое именно будет принято. Не управление сложностью, не опыт, не подход, не знания, а просто случайность.
И все эти проекты работают. Львиная доля всех задач, которые решают разработчики — это ликвидация последствий ошибок других разработчиков, но проекты работают, и решают проблемы людей. Умные люди придумывают практики и архитектурные паттерны. Языки программирования с мощным контролем типов и контрактами — всё, лишь бы разработчики поменьше ошибались. А я каждый день открываю эджайл борд на работе, и вижу там больше багов, чем задач. Где каждый баг, это очередной, управляющий сложностью, обосравшийся разраб.
Философия разработки — это просто выбор способа, которым ты в финале обосрёшься. Но если не будет философии, а только чистая объективная логика, то любой спор сведется к проблеме всемогущего творца и неподнимаемого камня.
Я уже давно в разработке, и хорошо научился делать как говорят, даже когда в корне не согласен, и стал писать сверхдетальные коммиты. Мой девлид — настоящий вампир. Он требует не просто описание — он хочет, чтобы я написал, что делает каждый файл, зачем делает, как делает. Какую проблему решает коммит.
Я нафигачил восемь экстраподробных коммитов, такое же подробное описание пул реквеста — и мне это капец как понравилось. С тех пор мы почти не вели работу именно над этим проектом, но я время от времени захожу туда полюбоваться на эти коммиты. Серьезно, очень, очень круто. И во всех остальных проектах, кроме своих личных, я теперь применяю этот подход.
Мне было очень трудно перестроить рабочий процесс. Прошло полтора месяца, и мне всё ещё тяжело писать код так. Но, похоже, оно того стоит.
Или нет. Я, если честно, не знаю. И я не знаю, что если оно того точно стоит, делает ли это меня двухмесячной давности круглым идиотом. В мире точно где-то ходит человек десять, которых я отучил делать такие коммиты. А они идиоты? Я так не думаю.
Мой подкаст
apro
Так есть же вроде. Конечно не из разряда "сделай мне все хорошо", но довольно удобно. По крайней мере в том GUI для git который я использую. Пара нажатий и какие-то коммиты объедены и для них переписан комментарий, какие-то изменены местами и т.д. и т.п.
fillpackart Автор
Так тоже ведь неудобно. Что бы хорошо описать, что сделано в конкретном коммите, это надо делать сразу, как его сделал. Или, если описываешь потом, ходить смотреть по коду. Что совсем не сочетается с моим подходом «меняю что хочу и где хочу»
apro
Так GUI для git все время показывает контекст, никуда ходить не надо.
Смотришь список коммитов для "interactive rebase", когда выбираешь что объединить, разъеденить и т.д. и т.п., нажимаешь Enter и видишь сбоку "diff". Переписываешь комментарий к коммиту, в правой половине экрана отображается "diff" и т.п.
Всегда доступен контекст, никуда ходить и смотреть код не надо.
fillpackart Автор
А если у тебя 40 файлов изменено в каждом коммите?
apro
А в чем сложность с 40 файлами?
Буфер/окно с "diff" конечно прокручивается.
fillpackart Автор
Вникать надо. Для меня это напряжно. Понятно, что в итоге вникать всё равно придётся — но это когда пуллреквест будет готов, и никаких промежуточных решений там не осталось. А если сделал кусочек работы, сразу описал коммит — вникать не надо. Все в голове есть
dvenum
Хотите метаться по кодовой базе и вносить измения случайным образом, не постаравшись сформулировать, чем же вы занимаетесь, пожалуйста. Это ваш выбор. Но вот писать статью о том, как вы спорили с лидером команды и доказывали, что так должны делать все, неприлично. Как и сам спор.
Вполне можно быть хорошо оплачиваемым специалистом на таком уровне. И даже уважаемым, если вы в конечном итоге решаете поставленные задачи и ваш код не создает проблем остальным. Но не самым лучшим, никакими аргументами вы этот разрыв не компенсируете.
schrodenkatzen
А самый лучший программист это кто?
Торвальдс что-ли систематично работал с четко очерченными и детально описанными коммитами?
symbix
У этого "надо вникать" есть польза: смотришь на собственный код свежим взглядом, и получается self code review.
Я, делая interactive rebase фичеветки, группирую коммиты так, чтобы было удобно делать ревью по одному коммиту. В процессе часто замечаю свои собственные косяки, которые тут же исправляю.
fillpackart Автор
Неее, тут же какой момент. Комиты, которые совершаются до пулл реквеста — это промежуточные результаты. Скажем сделал я реализацию бизнес логики через отдельную сборку, а потом, через два коммита отказался от этого подхода. И в итоговом решении оно представлено не будет. Так зачем я тратил силы на его повторное осознание при документировании? Селф ревью я так или иначе сделаю, когда буду пулл реквест оформлять.
donRumatta
К чему они? Если не отвлекаться на другую ветку, что бывает очень редко, то один талон — один коммит. И девлид не придерется(=
fillpackart Автор
Я с нескольких машин работаю
m0sk1t
эмм, зачем? --amend не? ноут?
symbix
А зачем тратить силы ревьюера на понимание ваших промежуточных шагов?
Если изменения в ветке небольшие, можно, конечно, и итоговый дифф смотреть, но маленькие ветки можно просто сквошить при мерже и не запариваться, какие там коммиты, тут вообще вопрос не стоит.
А большую фичеветку без атомарных коммитов ревьюить довольно адово.
fillpackart Автор
Большую ветку проще ревьювить, не видя промежуточных шагов, если эти шаги были в итоге отменены. Если бьешь свою работу на логичные шаги — тогда да, ревьювить по комитам проще. Но я свою не бью.
symbix
В процессе работы шаги могут перемешаться как угодно, это естественное состояние фичеветки в процессе работы над ней, с первого раза идеальный код никто не пишет.
Interactive rebase позволяет разбить на логичные шаги постфактум — так, чтобы ревьюер видел PR в таком состоянии, как будто я изначально идеально все спланировал, и мог последовательно идти по коммитам.
Я это делаю прежде всего из уважения ко времени коллег, которые точно не порадуются необходимости ревьюить PR на несколько тысяч строк за один заход. История коммитов для будущих git blame — тоже хорошо, но вторично, скорее полезный побочный эффект.
fillpackart Автор
Ну звучит кстати довольно разумно, попробую
Druu
Самый главный вопрос в том — зачем это нужно, когда есть вещи вроде git lense.
Какую именно задачу позволит вам решить подобное разбиение на коммиты, которую нельзя было бы столь же удобно решить без разбиения?
sedyh
В cli делается аналогично через:rebase -i и add patch
Shatun
Я обычно делаю amend до тех пор пока не получу работабщий кусок кода, и именно то что заработало/изменилось я и пишу в загаловке.
У меня получается коммит атомарный, и главное — всегда работающий состояние, если у меня что-то пошло не так я к нему могу откатиться. Если что-то по итогу изменено что никак не касается этого коммита то эту часть я не включу/выкину из него.
VolCh
У меня с таким подходом часто возникает проблема: в процессе разработки какой-то фичи, решаю улучшить какой-то модуль, часто с изменением сигнатур/контрактов, чтобы проще было решить задачу. В итоге довольно много изменений к задаче напрямую не относятся, но отделить их нельзя, потому что задача решена с новыми улучшениями.
fillpackart Автор
Ну вот да. Не получается у меня так: комит решает одну проблему. Всегда что-то по ходу зацепляешь, меняешь, рефакторишь. И задействуешь в решении той проблемы
m0sk1t
не надо так, а если без этого никак — делай в таске подзадачи типа «рефакторинг *подставить нужное* для обеспечения корректной работы *чего-то там ещё*»
VolCh
Да подзадачи чделать не проблема, проблема разделить эту работу с кодом, чтобы сделать отдельный коммит для этого "рефакторинга"
m0sk1t
так в чём проблема то? =) проделал работу — закоммить и опиши что сделал, так и история блужданий сохранится и лиду будет понятно что тебе пришлось перерыть и что затронуть ради выполнения таски
VolCh
Я про ситуации типа:
Если коммитить всё сразу, то изменений из п.3, неотносящихся к фиче может быть очень много. А цельная рабочая систем атолько после шага 3, на шаге 1 она разломана
m0sk1t
Из недавнего:
делал фичу, предварительно проанализировал задачу и понял что нужно будет сделать много подготовительных изменений — поговорил с лидом — и сразу же оформили таску на изменения, я её быстренько сделал, переписал тесты, влили в мастер — продолжил предыдущую таску на основе сделанных изменений. в итоге пришлось поменять 35-40 файлов по изменениям и 5 на фичу, даже в изначальные сроки почти убрался, потратил лишние пару суток на анализ и подготовку, но всё это было согласовано и одобрено.
Но это как пример нашего процесса разработки, конечно же у вас может быть устроено по иному
fillpackart Автор
Ага, тебя забыл спросить
m0sk1t
ага
chapuza
Это уже очень серьезный звоночек, но если так надо, то вот рецепт:
Все можно туда-сюда раскатывать, ветка на подзадачу, история коммитов нахрен не нужна.
VolCh
Ну как-то так да приходится делать, если требуют такой атомарности
Shatun
Когда я изменил контракт и проверил что работает-я сделал коммит. Следующий коммит будет дальше про основную фичу. Но я продолжу работу только когда убежусь что рефакторинг верный и ничего не сломал, чтобы если что-то пойдет не так не откатываться к началу, а вернуться именно к этому коммиту с рефакторингом.
Для меня коммит это как сейв в игре, лучше больше сейвов и в те моменты когда я при загрузке не умираю через секунду.
VolCh
Это хорошо работает, если изменение контракта плановое. Его даже в отдельный MR можно отправить с просьбой провести регресс. А вот если понимание необходимости провести изменение контракта приходит во время разработки фичи, когда что-то уже сломано, да и окончательный контракт ещё не ясен, а время ограничено, то простой алгоритм сложно придумать.