День, когда git отказался принимать мой push
Однажды git просто перестал давать мне сохранять мою работу. Мой push провалился с серверной ошибкой, которой я раньше никогда не видел, — не конфликт слияния, не проблема с правами, а просто категорический отказ с той стороны. Я неделями спокойно делал коммиты, и вот теперь самая рутинная команда моего дня не желала завершаться.
Причина была не в баге git. Дело в том, что все эти недели я тихо набивал свой репозиторий тысячами вещей, которым там было не место, и в конце концов он стал слишком тяжёлым, чтобы его поднять.
Push, который не проходил
Сначала я решил, что это случайность — сбой сети, сервер, у которого выдался плохой момент. Но это повторялось, стабильно, каждый раз. Накопленная масса репозитория, похоже, и была причиной: push раз за разом проваливался одинаково, и никакое количество повторных попыток не могло исправить проблему, которая была структурной, а не временной.
Когда я наконец посмотрел, что репозиторий на самом деле содержит, ответ оказался стыдным. Код — то, ради чего репозиторий и существует, — составлял малую его часть. Подавляющая масса была данными: тысячи сгенерированных файлов, кэшей и скомпилированных артефактов, каждый из которых добросовестно отслеживался, версионировался и таскался за собой в каждой операции.
Как репозиторий тихо превращается в склад
Никто не принимает решения положить гигабайты данных под контроль версий. Это происходит шаг за шагом, и каждый шаг кажется разумным. Ты генерируешь несколько файлов, и они оказываются в папке проекта. Ты запускаешь команду add, может быть немного небрежно, и они идут следом. Это работает. Никто не возмущается. Так что ты делаешь это снова, и снова, и репозиторий обрастает данными так же, как ящик обрастает хламом — незаметно, до того дня, когда он перестанет закрываться.
К тому моменту, когда git отказался принимать мой push, я отслеживал тысячи файлов данных. Я никогда не делал осознанного выбора поместить их под контроль версий. Я просто ни разу не сделал выбора держать их вне его.
Данные и код хотят противоположного
Причина, по которой их смешивание вредит, в том, что они версионируются по совершенно разным причинам. Код мал, осмыслен и меняется так, что ты хочешь видеть это построчно, — git создан именно для этого. Данные велики, непрозрачны и склонны меняться целиком; построчный diff по ним не говорит тебе почти ничего, а хранить каждую их прошлую версию вечно — по большей части напрасная трата. Git великолепен в первой задаче; его стандартная история часто плохо подходит для второй, по крайней мере когда данных становится много или они меняются постоянно.
Сложи их вместе — и получишь худшее из обоих. Твоя история раздувается огромными изменениями, которые ты никогда не сможешь осмысленно прочитать, каждый clone и push таскает всё это за собой, а маленькая, драгоценная вещь — собственно исходник — оказывается погребённой в репозитории, который состоит в основном из балласта.
Решение: проведи границу, которую ты пропустил
Починка была концептуально тривиальной и слегка нудной: сказать git, явно, что не отслеживать. Сгенерированные файлы, кэши, скомпилированные артефакты — всё это было исключено, и репозиторий вернулся к тому, чем он всегда и должен был быть: домом для вещей, которые я намеренно создал, — кода, конфигурации, документации, тестов, — а не для массы сгенерированного вывода. Данные переехали туда, где и положено быть большим, воссоздаваемым вещам — производимым по запросу или хранимым где-то, что построено для больших файлов, но никогда больше не версионируемым построчно, как будто это исходник.
Push прошёл с первой попытки. Решение не было хитрым. Это была просто граница, которую мне следовало провести в первый же день и которой у меня так и не было.
Привычка под всем этим: исходник против производного
Прочный урок на самом деле не про git. Это вопрос, который я теперь задаю про каждый файл, который производит проект: я создал это, или это сгенерировал мой код? Это исходник — незаменимая вещь, которую написал человек, — или это производное, вывод, который можно воссоздать, запустив исходник заново?
Исходник, как правило, место под контролем версий. Большим или громоздким производным файлам там обычно не место — их следует коммитить только тогда, когда воспроизводимость или рабочий процесс действительно этого требуют, и в идеале они должны оставаться воссоздаваемыми из исходника в любой момент. Многие из репозиториев, что сгнивают в неуправляемое месиво, делают это, теряя из виду это различие, — относясь к чему-то производному так, будто оно драгоценно, и позволяя вещам, которые всегда можно перегенерировать, накапливаться рядом с вещами, которые никогда не заменить.
Git в тот день не сломался. Он просто наконец заставил меня заметить, что я перестал держать исходник и вывод по своим раздельным местам и просил один инструмент делать работу, для которой он никогда не был создан.
— Никаких сигналов, никакой доходности, не инвестиционный совет.