O que o guarda-roupa de Barack Obama tem em comum com uma boa história do Git

Em um dia de verão no final de agosto de 2014, o então presidente Barack Obama tomou uma decisão que chocaria a nação: ele usava um terno diferente. A “ controvérsia do terno tan ” resultante dominou um ciclo de notícias e se espalhou por uma variedade de razões, mas no final das contas foi desencadeada pela novidade do próprio terno. Assim como as camisas pretas de gola alta de Steve Jobs e as camisas cinzas de Mark Zuckerberg, Obama normalmente usava os mesmos ternos azuis ou cinzas todos os dias.
O denominador comum por trás desse comportamento compartilhado é o conceito psicológico de fadiga de decisão : mesmo as menores decisões que tomamos todos os dias podem drenar a quantidade finita de inteligência que temos para tomar decisões e fazê-las bem. Uma estratégia adotada por esses indivíduos para preservar esse precioso recurso é eliminar o máximo possível de decisões menores: vestir as mesmas coisas, comer as mesmas coisas, seguir os mesmos horários e assim por diante. Isso permite que você concentre sua energia mental nas decisões que realmente importam.
Então, o que tudo isso tem a ver com o sistema de controle de versão favorito de todos, o git? Assim como acontece com tanta coisa na programação, não existe uma maneira “certa” de estruturar um git commit ou gerenciar o histórico do git de um projeto; você deve simplesmente escolher um princípio orientador e organizar seus padrões em torno dele. Pessoalmente, acredito na escolha de uma estratégia que reduza a fadiga da decisão (e a “fadiga mental” de forma mais ampla) para todos os vários “consumidores” de um commit. Entrarei em mais detalhes sobre como fazer isso abaixo (e sinta-se à vontade para pular para a lista numerada, se desejar), mas acho muito importante primeiro abordar por que acredito nisso. E precisamos começar com para quem é realmente um commit.
De quem é a história?
Deve-se enfatizar que ao longo de todo o ciclo de vida de um projeto médio, o número de pessoas que visualizam um commit que não o escreveu ultrapassará em muito o número daqueles que o fizeram. Essas outras pessoas também terão o conhecimento menos íntimo do que realmente é um commit e como ele deve funcionar. Como uma questão prática, então, construir um bom histórico git deve ser realmente para eles . E, com tempo suficiente, até mesmo o código que você mesmo escreveu pode um dia parecer estranho para você. Portanto, manter uma boa história também pode ajudar o seu eu futuro.
Tendo isso em mente, vale a pena notar que existem duas grandes categorias de pessoas que irão, em algum momento, visualizar um determinado commit:
- Revisores de código
Aqueles que visualizam o commit antes de serem mesclados no histórico por meio do processo de revisão de código. Essas pessoas são geralmente conhecidas como “revisores de código”. Em uma boa equipe, todos serão, em algum momento, revisores de código e pode haver vários para cada conjunto de novas alterações de código. - Detetives de código
Aqueles que visualizam o commit após ele ter sido mesclado. Normalmente, são indivíduos que estão voltando na história para tentar entender por que algo foi adicionado ou quando um bug foi introduzido. Por falta de um nome melhor, chamarei essas pessoas de “detetives de código” para distingui-las das pessoas acima.
Os detetives de código têm todos esses mesmos desafios, além de outros: eles nem sempre sabem o que estão procurando e, mesmo quando o encontram, podem não ter um contexto vital para entendê-lo. Muitas vezes eles nem têm o benefício de poder falar com o autor original do código. Por esse motivo, grande parte do que um detetive de código faz é tentar inferir a intenção do código existente sem realmente poder perguntar sobre ele ou acompanhar suas suspeitas.
Os trabalhos desses dois grupos são ligeiramente diferentes, mas, em sua essência, ambos envolvem uma série de decisões que devem ser tomadas, linha por linha, para responder à pergunta final: o que esse código faz? Dependendo de como os commits são estruturados pelo autor, isso pode ser relativamente direto ou um trabalho árduo com obstáculos desnecessários e pistas falsas.
Decisões decisões
Vamos agora considerar que tipos de decisões são necessárias para entender um commit. Primeiro, precisamos observar que cada linha de um commit pode ser categorizada como uma de duas coisas: uma linha de código “adicionada” ou uma linha “removida”.
Sem qualquer contexto adicional, as seguintes decisões devem ser tomadas ao visualizar uma única linha de código “adicionado”:
- Esta é uma linha de código inteiramente nova?
- Se não for uma nova linha de código, é uma linha de código existente que simplesmente foi movida de outro lugar?
- Se não é uma nova linha de código e não foi movida, é uma modificação trivial de uma linha existente (como uma alteração de formatação) ou é uma alteração lógica legítima?
- Se é uma linha de código totalmente nova ou uma modificação que leva a uma alteração lógica, por que isso está sendo feito? É feito corretamente? Pode ser simplificado ou melhorado?
Podemos ver um processo semelhante para cada linha de código “removida”:
- Esta linha está sendo totalmente removida?
- Se não está sendo removido totalmente, está sendo movido ou modificado?
- Se não está sendo removido totalmente e não está apenas sendo movido, é o resultado de uma modificação trivial (ex: formatação) ou o resultado de uma alteração lógica?
- Se é de fato uma modificação lógica, por que está sendo modificada? Está sendo feito corretamente?
- Se a linha está sendo totalmente removida, por que não é mais necessária?
Então, isso finalmente nos traz de volta à fadiga da decisão:
Como podemos organizar os commits para eliminar essas primeiras escolhas triviais e permitir que os visualizadores se concentrem nas mais importantes?
Você não quer que sua equipe gaste seu poder cerebral limitado e tempo decidindo, por exemplo, que algum pedaço de código acabou de ser movido de um módulo para outro sem modificação e então perderá os erros de codificação presentes no novo código real. Multiplique isso pelas equipes de uma grande organização e isso pode resultar em uma perda mensurável de produtividade.
Então, agora que discutimos por que acredito que devemos seguir essa estratégia, vamos finalmente discutir como defendo colocá-la em prática.
1. Coloque modificações triviais em seus próprios commits
A coisa mais simples e importante a fazer é separar as modificações triviais em seus próprios commits. Alguns exemplos disso incluem:
- Mudanças na formatação do código
- Funções / variáveis / renomeações de classe
- Reordenação de funções/variáveis/importações dentro de uma classe
- Removendo código não utilizado
- Mover localizações de arquivos
Considere o seguinte commit misturando alterações triviais com não triviais:
Mensagem de commit: “Atualizar lista de frutas válidas”

Quanto tempo você levou para identificar as mudanças não triviais ? Agora veja o que acontece quando essas duas alterações são divididas em dois commits separados:
Mensagem de confirmação: “Atualizar formatação válida da lista de frutas”

Mensagem de confirmação: “Adicionar datas à lista de frutas válidas”

O commit “somente formatação” pode ser essencialmente ignorado e as adições de código podem ser descobertas imediatamente.
2. Coloque os refatoradores de código em seus próprios commits
Refatorações de código envolvem mudanças na estrutura de algum código, mas não em sua função. Às vezes, isso é feito por si só, mas geralmente é feito por necessidade: para construir sobre o código existente, às vezes é necessário primeiro refatorá-lo e pode ser tentador fazer as duas coisas ao mesmo tempo. No entanto, erros podem ser cometidos durante uma refatoração e é necessário um cuidado especial ao revisá-los. Ao colocar esse código em seu próprio commit claramente indicado como refatoração, o revisor sabe sinalizar qualquer desvio do comportamento lógico existente como um possível erro.
Por exemplo, com que rapidez você consegue identificar o erro aqui?
Mensagem de confirmação: “Atualizar lógica da dica”

Que tal agora com o refactor dividido?
Mensagem de confirmação: “Extrair taxa de gorjeta padrão”

Mensagem de confirmação: “Permitir taxas de gorjeta personalizadas”

3. Colocar correções de bugs em seus próprios commits
Às vezes, ao fazer alterações no código, você percebe um bug no código existente que está tentando modificar ou desenvolver. No interesse de seguir em frente, você pode apenas corrigir esse bug e incluí-lo em suas alterações não relacionadas no mesmo commit. Quando misturado desta forma, existem várias complicações:
- Outros visualizando este código podem não saber que há um bug sendo corrigido.
- Mesmo quando se sabe que há uma correção de bug incluída, pode ser difícil saber qual código fez parte da correção do bug e qual fez parte das outras alterações lógicas.
4. Coloque alterações lógicas separadas em seus próprios commits
Depois de dividir os tipos de alterações acima, você deve ficar com um único commit com alterações lógicas e legítimas para adicionar/atualizar/remover funcionalidade. Para uma mudança pequena e concisa, isso geralmente é suficiente. No entanto, às vezes, esse commit adiciona um recurso totalmente novo (com testes) e atinge mais de 1000 linhas (ou mais). O Git não apresentará essas alterações de maneira coerente e entender com sucesso esse código exigiria que o revisor pulasse e mantivesse uma grande fração dessas linhas na memória de uma só vez para acompanhar. Juntamente com a fadiga de decisão envolvida no processamento de cada linha, estender sua memória de trabalho dessa maneira é mentalmente desgastante e, em última análise, ineficiente.
Sempre que possível, divida os commits com base em domínios de forma que cada commit seja compilado independentemente. Isso significa que o código mais independente pode ser adicionado primeiro, seguido pelo código que depende dele e assim por diante. Um código bem estruturado deve se dividir dessa maneira de maneira bastante natural, enquanto as dificuldades encontradas nesse estágio podem sugerir problemas estruturais maiores, como dependências circulares. Este exercício pode até levar a melhorias no próprio código.
5. Mesclar quaisquer alterações de revisão nos commits aos quais pertencem
Depois de dividir seu trabalho em vários commits limpos, você pode obter feedback de revisão que exige que você faça alterações no código que aparece em um ou mais deles. Alguns desenvolvedores reagirão a esse feedback adicionando novos commits que abordam essas preocupações. A lista de confirmação em um determinado PR pode começar a se parecer com o seguinte:
- <Initial commits>
- Respond to review feedback
- Work
- More work
- Addressing more review feedback
O mesmo vale para quando um pull request é aberto pela primeira vez: cada commit deve ter um propósito e não deve ser negado por alterações em commits posteriores pelos mesmos motivos mencionados acima.
Considere esta mudança inicial seguida por vários commits de “trabalho”:




Agora imagine que você está vendo essas mudanças bem no processo (ou mesmo anos depois). Você não gostaria de ver apenas o seguinte?

6. Rebase, rebase, rebase!
Se um branch de recurso já existe há tempo suficiente — seja por causa do tempo que leva para adicionar o código inicial ou por causa de um longo processo de revisão de código — ele pode começar a entrar em conflito com as alterações feitas no branch principal no qual foi originalmente baseado. Agora existem duas maneiras de tornar a ramificação do recurso atual:
- Mescle a ramificação principal na ramificação de recursos. Isso gerará um “merge commit” no qual todas as alterações de código necessárias para resolver os conflitos serão incluídas. Se a ramificação do recurso for particularmente antiga, esses tipos de confirmação podem ser substanciais.
- Realize o rebase da ramificação do recurso em relação à ramificação principal. O produto final aqui é um novo conjunto de commits que agem como se tivessem acabado de ser criados com base no branch principal atualizado. Quaisquer conflitos precisarão ser tratados como parte do processo de rebase, mas todas as evidências da versão original do código desaparecerão.
Se você se preocupa em produzir um histórico limpo (e deveria!), o rebase é a melhor opção aqui: todas as alterações se acumulam de maneira ordenada e linear. Você não precisa de ferramentas sofisticadas de visualização de histórico para entender o relacionamento entre as ramificações.
Considere o seguinte histórico de projeto que emprega mesclagem entre ramificações:

Compare isso com um projeto que faz o rebase de todas as alterações e proíbe as confirmações de mesclagem, mesmo ao mesclar recursos na ramificação principal :

No primeiro, as relações entre as mudanças devem ser traçadas, ponderadas e decifradas; no último, você simplesmente flui para frente e para trás no tempo.
Alguns podem argumentar que, na verdade, é o rebase que destrói a história; que você perca o histórico de alterações feitas para obter algum código em sua forma final antes de ser mesclado. Mas esse tipo de histórico raramente é útil e depende muito do desenvolvedor: a jornada de uma pessoa pode diferir da seguinte, mas o que importa é ver uma série de commits no histórico que refletem as alterações finais que representam... lá. Sim, há casos especiais aqui em que merge commits são inevitáveis, mas devem ser a exceção. E muitas vezes os cenários que causam isso (como ramificações de recursos de longa duração compartilhadas por vários membros da equipe) podem ser evitados usando fluxos de trabalho melhores (como usar sinalizadores de recursos em vez de ramificações de recursos compartilhados ).
Contra-argumentos
Certamente existem argumentos que podem ser feitos contra essa abordagem e já tive muitas discussões com pessoas que discordam dela. Esses pontos têm mérito e, como mencionei no início do artigo, não existe uma maneira “certa” de estruturar os commits. Quero destacar rapidamente alguns pontos que ouvi e dar minha opinião sobre cada um.
“Preocupar-se tanto com a estrutura do commit atrasa o desenvolvimento.”
Este é um dos pontos mais comuns que ouvi contra esta abordagem. Certamente é verdade que levará algum tempo extra para que o desenvolvedor que escreve o código considere cuidadosamente e divida suas alterações. No entanto, isso também é verdade para qualquer outro tipo de processo adicional destinado a proteger contra as fraquezas inerentes de priorizar a velocidade e, a longo prazo, pode não salvar a equipe como um todo em nenhum momento. Por exemplo, o argumento de que o desenvolvimento será lento é usado por equipes que não escrevem testes de unidade, mas essas mesmas equipes precisam gastar mais tempo corrigindo códigos quebrados e testando refatorações manualmente. E, uma vez que uma equipe tem o hábito de dividir suas alterações dessa forma, o tempo extra adicionado é bastante reduzido, pois passa a fazer parte do processo normal de desenvolvimento.
“Meu projeto usa ferramentas que nem permitem mudanças triviais de formatação.”
Concordo que esta é uma ótima maneira de minimizar os danos causados pela rotatividade de código relacionada à formatação. Como desenvolvedor Android, acredito firmemente no uso de auto-formatadores em toda a equipe e juro por ferramentas como ktlint . No entanto, também sei em primeira mão, ao configurar todas essas ferramentas, que elas não são perfeitas e que há muitas alterações de formatação possíveis sobre as quais eles são totalmente agnósticos. E, como discutido acima, algumas mudanças triviais não são simplesmente mudanças de formatação, como reordenar o código. Sempre haverá mudanças de código triviais que podem ser feitas e, portanto, deve haver um plano para a melhor forma de lidar com elas.
“Nem todos os sites de hospedagem git permitem solicitações pull com vários commits.”
Isso é verdade! Minhas recomendações são baseadas principalmente no uso de ferramentas como GitHub e GitLab que permitem que um PR tenha quantos commits você quiser, mas existem ferramentas como Gerrit que não. Neste caso, apenas considere cada commit como sendo seu próprio PR. Isso introduz ainda mais sobrecarga para o autor (e às vezes para os revisores), mas acredito que a longo prazo vale a pena o esforço. Pode até haver maneiras de simplificar esse processo e relacionar esses PRs separados entre si, como usar “mudanças dependentes” no Gerrit.
“Um único commit garante que todas as alterações sejam compiladas e aprovadas nos testes.”
Este também é um ponto muito bom. As verificações automatizadas executadas em sites de hospedagem git geralmente são executadas apenas em todo o conjunto de alterações, não para cada confirmação individual. Se houver um commit quebrado ao longo do caminho que é corrigido por alterações posteriores, não há como detectar isso automaticamente. Você quer que cada commit seja capaz de funcionar sozinho caso algum dia precise voltar e testar o estado do código naquele ponto para rastrear bugs, etc. PR de confirmação múltipla para compilar e passar em quaisquer testes relevantes, mas não há como impor isso estritamente (além de fazer com que cada commit seja seu próprio PR). Isso requer vigilância, mas é apenas algo que precisa ser pesado contra os benefícios que vêm com a divisão do código.
“Um único commit fornece mais contexto para todas as mudanças.”
Este é um ponto interessante. Enquanto sites de hospedagem git como o GitHub permitem que comentários sejam adicionados em massa a um grupo de commits como parte de uma descrição de PR, tal coisa não existe no próprio git. Isso significa que o link entre os commits adicionados no mesmo PR não faz parte estritamente do histórico. Felizmente, sites como o GitHub possuem recursos que adicionam um link para o PR que produziu um commit ao visualizá-lo isoladamente:

Embora isso não seja tão útil quanto ter esse link no próprio histórico do git, para muitos projetos essa é uma maneira adequada de acompanhar o relacionamento entre os commits.
Pensamentos finais
Espero ter convencido você de que dividir as alterações de código em commits distintos de vários tipos traz benefícios para todos no processo de desenvolvimento:
- Pode ajudar o escritor a melhorar a estrutura do código e transmitir melhor o conteúdo das alterações.
- Ele pode ajudar os revisores de código a revisar o código mais rapidamente e reduzir a fadiga mental, permitindo que eles concentrem sua atenção em mudanças separadas e significativas.
- Ele pode ajudar qualquer pessoa que revise o histórico do código a encontrar alterações lógicas e bugs mais rapidamente e, da mesma forma, reduzir a carga mental que acompanha a leitura rápida de grandes quantidades de histórico.
Brian trabalha na Livefront , onde está sempre tentando fazer um pouco mais de (git) história.