O Dia em que o Git Recusou Meu Push

Um dia, o git simplesmente parou de me deixar salvar meu trabalho. Meu push falhou com um erro de servidor que eu nunca tinha visto antes — não era um conflito de merge, não era um problema de permissão, apenas uma recusa categórica do outro lado. Eu vinha fazendo commits alegremente havia semanas, e agora o comando mais rotineiro do meu dia não terminava.

A causa não era um bug no git. Era que eu tinha passado aquelas semanas enchendo discretamente meu repositório com milhares de coisas que não tinham nada que estar ali, e ele finalmente tinha ficado pesado demais para erguer.

O push que não ia

No começo eu presumi que fosse um acaso — um soluço de rede, um servidor passando por um mau momento. Mas continuou acontecendo, de forma confiável, toda vez. O volume acumulado do repositório parecia ser a causa: o push continuava falhando da mesma maneira toda vez, e nenhuma quantidade de novas tentativas ia consertar um problema que era estrutural em vez de passageiro.

Quando finalmente olhei o que o repositório de fato continha, a resposta foi constrangedora. O código — a coisa para a qual um repositório existe — era uma pequena minoria dele. A esmagadora maioria era dados: milhares de arquivos gerados, caches e artefatos compilados, cada um fielmente rastreado, versionado e carregado junto em cada operação.

Como um repo discretamente vira um depósito

Ninguém decide colocar gigabytes de dados sob controle de versão. Isso acontece um passo aparentemente razoável de cada vez. Você gera alguns arquivos e eles aterrissam na pasta do seu projeto. Você roda um comando add, talvez um pouco descuidadamente, e eles vêm junto. Funciona. Nada reclama. Então você faz de novo, e de novo, e o repositório acumula dados do jeito que uma gaveta acumula bagunça — de forma invisível, até o dia em que ela não fecha mais.

Quando o git recusou meu push, eu estava rastreando milhares de arquivos de dados. Eu nunca tinha feito a escolha consciente de colocá-los sob controle de versão. Eu simplesmente nunca tinha feito a escolha de mantê-los de fora.

Dados e código querem coisas opostas

A razão pela qual misturá-los machuca é que eles são versionados por razões completamente diferentes. Código é pequeno, significativo, e muda de maneiras que você quer ver linha por linha — o git foi construído precisamente para isso. Dados são grandes, opacos, e tendem a mudar por inteiro; um diff linha por linha deles não te diz quase nada, e armazenar cada versão passada deles para sempre é, em sua maior parte, desperdício. O git é maravilhoso na primeira tarefa; seu histórico padrão muitas vezes se encaixa mal na segunda, pelo menos quando os dados ficam grandes ou mudam constantemente.

Junte os dois e você tem o pior de ambos. Seu histórico incha com mudanças enormes que você nunca consegue ler de forma significativa, cada clone e push arrasta tudo isso por aí, e a coisa pequena e preciosa — o código-fonte de fato — acaba enterrada num repositório que é, em sua maior parte, lastro.

A correção: trace a fronteira que você pulou

O reparo era conceitualmente trivial e ligeiramente tedioso: dizer ao git, explicitamente, o que não rastrear. Os arquivos gerados, os caches, os artefatos compilados — tudo isso foi excluído, e o repositório voltou a ser o que sempre deveria ter sido: um lar para as coisas que eu tinha intencionalmente autorado — código, configuração, docs, testes — em vez de saída gerada a granel. Os dados se mudaram para onde coisas grandes e regeneráveis pertencem — produzidas sob demanda, ou armazenadas em algum lugar construído para arquivos grandes, mas nunca mais versionadas linha por linha como se fossem código-fonte.

O push passou na primeira tentativa. A correção não foi engenhosa. Foi apenas uma fronteira que eu deveria ter traçado no primeiro dia e nunca tinha traçado.

O hábito por baixo: fonte versus derivado

A lição duradoura não é realmente sobre git. É uma pergunta que agora faço sobre cada arquivo que um projeto produz: eu autorei isto, ou meu código gerou isto? É fonte — a coisa insubstituível que um humano escreveu — ou é derivado, uma saída que pode ser recriada rodando a fonte de novo?

A fonte geralmente pertence ao controle de versão. Arquivos derivados grandes ou volumosos normalmente não — eles deveriam ser commitados apenas quando a reprodutibilidade ou o fluxo de trabalho genuinamente exigem, e idealmente permanecer reproduzíveis a partir da fonte a qualquer momento. Muitos dos repositórios que apodrecem virando uma bagunça incontrolável o fazem por perder de vista essa distinção — por tratar algo derivado como se fosse precioso, e deixar as coisas que sempre podem ser regeneradas se empilharem ao lado das coisas que nunca podem ser substituídas.

O git não quebrou naquele dia. Ele apenas finalmente me fez perceber que eu tinha parado de manter fonte e saída em seus lugares separados, e vinha pedindo a uma ferramenta que fizesse um trabalho para o qual ela nunca foi construída.

— Sem sinais, sem retornos, isto não é consultoria de investimento.