Explicação do fluxo aprimorado do Git

Publicados: 2022-03-11

Causar danos inadvertidamente com o Git pode ser muito fácil. No entanto, a melhor maneira de usar o Git sempre será controversa.

Isso porque o próprio Git detalha apenas operações básicas de ramificação, o que deixa seus padrões de uso – ou seja, modelos de ramificação – uma questão de opinião do usuário. Os modelos de ramificação do Git prometem aliviar a dor ao organizar o caos que inevitavelmente surge à medida que os desenvolvedores de software fazem alterações em suas bases de código.

Como muitos desenvolvedores, você queria algo que “simplesmente funcionasse” para que você pudesse continuar com o desenvolvimento de software real. Então você pegou o Git flow , um modelo de ramificação frequentemente recomendado para usuários do Git. Talvez você estivesse de acordo com a lógica do Git flow no início, até encontrar alguns problemas com ela na prática. Ou talvez o fluxo do Git não pareça adequado o suficiente para você adotá-lo. Afinal, existem inúmeras variáveis ​​em jogo, e nenhum modelo de ramificação funcionará bem em todas as situações.

Boas notícias! Uma variação do modelo de fluxo Git clássico, o fluxo Git aprimorado simplifica as manobras mais comuns do fluxo de trabalho de fluxo Git, mantendo as principais vantagens.

O esplendor e a miséria do modelo clássico de fluxo do Git

Sou um forte defensor do Git flow desde que descobri como ele se destaca no desenvolvimento de um produto que evolui em incrementos de valor significativos (em outras palavras, releases ).

Um incremento de valor significativo requer uma quantidade significativa de tempo para ser concluído, como os sprints de mais de duas semanas normalmente usados ​​no desenvolvimento baseado em Scrum. Se uma equipe de desenvolvimento já implantou na produção, pode haver problemas se o escopo do próximo lançamento se acumular no mesmo local onde o código de produção está – por exemplo, na ramificação principal do repositório Git que eles usam.

Enquanto o produto ainda está na fase de desenvolvimento inicial – ou seja, não há produção e não há usuários reais do produto – não há problema para a equipe simplesmente manter tudo dentro da ramificação principal. Na verdade, está tudo bem: essa estratégia permite o ritmo mais rápido de desenvolvimento sem muita cerimônia. Mas as coisas mudam em um ambiente de produção; então, pessoas reais começam a confiar no produto para ser estável.

Por exemplo, se houver um bug crítico na produção que precise ser corrigido imediatamente, seria um grande desastre para a equipe de desenvolvimento ter que reverter todo o trabalho acumulado na ramificação principal até agora apenas para implantar a correção. E implantar código sem testes adequados – seja o código considerado incompleto ou bem desenvolvido – claramente não é uma opção.

É aí que os modelos de ramificação brilham, incluindo o fluxo do Git. Qualquer modelo de ramificação sofisticado deve responder a perguntas sobre como isolar a próxima versão da versão do sistema atualmente usada pelas pessoas, como atualizar essa versão com a próxima versão e como introduzir correções de quaisquer bugs críticos na versão atual.

O processo de fluxo do Git aborda esses cenários fundamentais separando “main” (o branch de produção ou “versão atual”) e “develop” (o branch de desenvolvimento ou “próximo lançamento”) e fornecendo todas as regras sobre o uso de branches de recurso/lançamento/hotfix . Ele efetivamente resolve muitas dores de cabeça dos fluxos de trabalho de desenvolvimento de produtos baseados em lançamento.

Mas mesmo com projetos adequados ao modelo clássico de fluxo do Git, sofri os problemas típicos que ele pode trazer:

  • O fluxo do Git é complexo, com duas ramificações de longa duração, três tipos de ramificações temporárias e regras rígidas sobre como as ramificações lidam umas com as outras. Essa complexidade torna os erros mais prováveis ​​e aumenta o esforço necessário para corrigi-los.
  • As ramificações de lançamento e hotfix exigem “fusão dupla” – uma vez em main, depois em develop. Às vezes você pode esquecer de fazer as duas coisas. Você pode facilitar a ramificação do fluxo do Git com scripts ou plugins de cliente VCS GUI, mas você precisa configurá-los primeiro para cada máquina de cada desenvolvedor envolvido em um determinado projeto.
  • Em fluxos de trabalho de CI/CD, você geralmente acaba com duas compilações finais para uma versão - uma do último commit da própria ramificação de lançamento e outra do merge commit para main. Estritamente falando, você deve usar o do principal, mas os dois geralmente são idênticos, criando o potencial de confusão.

Digite "Fluxo Git Aprimorado"

A primeira vez que usei o fluxo aprimorado do Git foi em um projeto greenfield de código fechado. Eu estava trabalhando com outro desenvolvedor e estávamos trabalhando no projeto nos comprometendo diretamente com o branch principal.

Observação: até o primeiro lançamento público de um produto, faz todo o sentido confirmar todas as alterações diretamente no branch principal - mesmo se você for um defensor do fluxo Git - por causa da velocidade e simplicidade do fluxo de trabalho de desenvolvimento. Como ainda não há produção, não há possibilidade de um bug de produção que a equipe precise corrigir o mais rápido possível. Fazer toda a mágica de ramificação que o fluxo clássico do Git implica é, portanto, um exagero neste estágio.

Então chegamos perto do lançamento inicial e concordamos que, além desse ponto, não estaríamos mais confortáveis ​​em nos comprometer diretamente com o branch principal. Tínhamos mudado muito rapidamente, e as prioridades de negócios não deixavam muito espaço para estabelecer um processo de desenvolvimento sólido, ou seja, um com testes automatizados suficientes para nos dar a confiança de manter nossa filial principal em um estado pronto para lançamento.

Parecia ser um caso válido para o modelo clássico de fluxo do Git. Com ramificações principais e de desenvolvimento separadas e tempo suficiente entre incrementos de valor significativos, havia confiança de que a maioria do controle de qualidade manual produziria bons resultados. Quando defendi o fluxo do Git, meu colega sugeriu algo semelhante, mas com algumas diferenças importantes.

No começo, eu empurrei para trás. Pareceu-me que alguns dos “patches” propostos para o fluxo clássico do Git eram um pouco revolucionários demais. Eu pensei que eles poderiam quebrar a ideia principal, e toda a abordagem ficaria aquém. Mas pensando melhor, percebi que esses ajustes não interrompem o fluxo do Git. Enquanto isso, eles o tornam um modelo de ramificação Git melhor, resolvendo todos os pontos problemáticos mencionados acima.

Após o sucesso com a abordagem modificada nesse projeto, eu a usei em outro projeto de código fechado com uma pequena equipe por trás dele, onde eu era o proprietário permanente da base de código e um desenvolvedor terceirizado ou dois ajudavam de tempos em tempos. Neste projeto, entramos em produção em seis meses e, desde então, estamos usando testes de CI e E2E há mais de um ano, com lançamentos a cada mês.

Um gráfico típico de confirmação do Git ao usar o fluxo aprimorado do Git. O gráfico mostra alguns commits em develop e main, e antes do commit comum, várias tags baseadas em data.

Minha experiência geral com essa nova abordagem de ramificação foi tão positiva que quis compartilhá-la com meus colegas desenvolvedores para ajudá-los a contornar as desvantagens do fluxo Git clássico.

Semelhanças com o Git Flow clássico: isolamento de desenvolvimento

Para o isolamento de trabalho no fluxo aprimorado do Git, ainda existem duas ramificações de longa duração, main e develop. (Os usuários ainda têm recursos de hotfix e lançamento – com ênfase em “recursos”, já que não são mais ramificações. Entraremos em detalhes na seção de diferenças.)

Não há um esquema de nomenclatura oficial para ramificações clássicas de recursos de fluxo do Git. Você apenas se ramifica do desenvolvimento e mescla novamente para desenvolver quando o recurso estiver pronto. As equipes podem usar qualquer convenção de nomenclatura que desejarem ou simplesmente esperar que os desenvolvedores usem nomes mais descritivos do que "meu-ramo". O mesmo vale para o fluxo aprimorado do Git.

Todos os recursos acumulados no ramo de desenvolvimento até algum ponto de corte moldarão a nova versão.

Squash Merges

Eu recomendo fortemente o uso de squash merge para ramificações de recursos para manter o histórico agradável e linear na maioria das vezes. Sem ele, os gráficos de commit (de ferramentas GUI ou git log --graph ) começam a parecer desleixados quando uma equipe está manipulando até mesmo um punhado de ramificações de recursos:

Comparando o gráfico de confirmação resultante de uma estratégia de mesclagem squash com o resultante de uma estratégia de confirmação de mesclagem. O gráfico de commit inicial do Git tem seu branch primário rotulado como "develop", com um commit anterior com "feature-D" e "feature-C" ramificados dele, e um commit ainda anterior com "feature-B" e "feature-A " ramificando dele. O resultado do commit de mesclagem é semelhante, mas com cada branch de recurso causando um novo commit no desenvolvimento no ponto em que ele se vincula a ele. O resultado do squash merge se desenvolveu simplesmente como uma linha reta de commits, sem outras ramificações.

Mas mesmo que você esteja bem com o visual neste cenário, há outro motivo para esmagar. Sem squashing, as visualizações do histórico de commits - entre elas o git log simples (sem --graph ) e também o GitHub - contam histórias bastante incoerentes, mesmo com os cenários de mesclagem mais simples:

Comparando a visualização do histórico de commits resultante de uma tática de merge de squash com aquela resultante de uma tática de commit de merge. O estado do repositório original é dado em forma de linha do tempo, mostrando a cronologia de commits para duas ramificações de recursos provenientes de um commit comum "x" no desenvolvimento, alternando as confirmações entre as ramificações, ou seja, na ordem 1a, 2a, 1b e 2b. Os resultados do commit de mesclagem são mostrados em duas variações. No histórico de confirmação resultante da fusão da segunda ramificação, da fusão da primeira ramificação e, em seguida, da exclusão de ambas as ramificações, a história é cronológica, mas não coesa, e inclui uma confirmação de mesclagem extra para a primeira ramificação. No histórico resultante da mesclagem das ramificações em ordem antes de excluí-las, os commits são ordenados "x, 2a, 1a, 2b, 1b", seguidos pelo commit de mesclagem para a segunda ramificação, que nem é cronológica. O histórico de commits do squash merging simplesmente tem um único commit para cada feature branch, com a história de cada branch contada pelo committer.

A ressalva ao usar a mesclagem de squash é que o histórico de ramificação de recurso original se perde. Mas essa ressalva nem se aplica se você estiver usando o GitHub, por exemplo, que expõe todo o histórico original de uma ramificação de recurso por meio da solicitação de pull que foi mesclada por squash, mesmo depois que a própria ramificação de recurso é excluída.

Diferenças do Classic Git Flow: versões e hotfixes

Vamos passar pelo ciclo de lançamento, pois (espero) é a principal coisa que você fará. Quando chegamos ao ponto em que gostaríamos de liberar o que está acumulado no desenvolvimento, é estritamente um superconjunto de main. Depois disso, é onde começam as maiores diferenças entre o fluxo Git clássico e aprimorado.

Gráficos de confirmação do Git conforme eles mudam ao executar uma versão normal no fluxo Git aprimorado. O gráfico inicial tem divergências principais de desenvolver vários commits por trás da dica e por um commit. Após a marcação, main e vYYYY-MM-DD são iguais entre si. Depois de deletar o local principal, criá-lo na ponta do desenvolvimento, forçar o push, implantar, testar, etc., o principal é mostrado mesmo com o desenvolvimento, deixando vYYYY-MM-DD onde o principal estava originalmente. Após o ciclo de deploy/teste, correções de staging no main (eventualmente o squash merge no develop) e, enquanto isso, mudanças não relacionadas no develop, o gráfico final tem develop e main diverging, cada um com vários commits, de onde eles estavam mesmo uns com os outros em o gráfico anterior.

Versões no fluxo Git aprimorado

Cada etapa de fazer um lançamento com fluxo Git aprimorado difere do processo de fluxo Git clássico:

  1. As versões são baseadas em main, em vez de desenvolver. Marque a ponta atual da ramificação principal com algo significativo. Adotei tags com base na data atual no formato ISO 8601 prefixado com um “v”—por exemplo, v2020-09-09 .
    • Se houver vários lançamentos em um dia - por exemplo, hotfixes - o formato pode ter um número sequencial ou uma letra anexada conforme necessário.
    • Esteja ciente de que as tags, em geral, não correspondem às datas de lançamento. Eles são apenas para forçar o Git a manter uma referência de como o branch principal era no momento em que o próximo processo de lançamento começou.
  2. Envie a tag usando git push origin <the new tag name> .
  3. Depois disso, uma pequena surpresa: exclua seu branch principal local . Não se preocupe, porque vamos restaurá-lo em breve.
    • Todos os commits para main ainda são seguros - nós os protegemos da coleta de lixo marcando main na etapa anterior. Cada um desses commits – mesmo hotfixes, como abordaremos em breve – também faz parte do desenvolvimento.
    • Apenas certifique-se de que apenas uma pessoa em uma equipe esteja fazendo isso para qualquer versão; esse é o chamado papel de “gerente de lançamento”. Um gerente de liberação geralmente é o membro mais experiente e/ou mais sênior da equipe, mas uma equipe seria sensata para evitar que qualquer membro específico da equipe assumisse essa função permanentemente. Faz mais sentido espalhar conhecimento entre a equipe para aumentar o famigerado fator ônibus.
  4. Crie um novo branch principal local no commit de ponta do seu branch de desenvolvimento .
  5. Envie essa nova estrutura usando git push --force , já que o repositório remoto não aceitará uma “mudança drástica” tão facilmente. Novamente, isso não é tão inseguro quanto pode parecer neste contexto porque:
    • Estamos apenas movendo o ponteiro do branch principal de um commit para outro.
    • Apenas um membro específico da equipe está fazendo essa alteração por vez.
    • O trabalho de desenvolvimento diário acontece na ramificação de desenvolvimento, portanto, você não interromperá o trabalho de ninguém movendo o main dessa maneira.
  6. Você tem seu novo lançamento! Implante-o no ambiente de preparo e teste-o. (Discutiremos padrões de CI/CD convenientes abaixo.) Quaisquer correções vão diretamente para a ramificação principal, e ela começará a divergir da ramificação de desenvolvimento por causa disso.
    • Ao mesmo tempo, você pode começar a trabalhar em uma nova versão no branch de desenvolvimento, a mesma vantagem vista no fluxo Git clássico.
    • No infeliz caso de um hotfix ser necessário para o que está atualmente em produção (não para a próxima versão em preparação) neste momento, há mais detalhes sobre esse cenário em “Lidando com hotfixes durante uma versão ativa…” abaixo.
  7. Quando sua nova versão for considerada estável o suficiente, implante a versão final no ambiente de produção e faça uma única mesclagem squash de main para desenvolver para obter todas as correções.

Correções no fluxo Git aprimorado

Os casos de hotfix são duplos. Se você está fazendo um hotfix quando não há uma versão ativa — ou seja, a equipe está preparando uma nova versão na ramificação de desenvolvimento — é muito fácil: Comprometa-se com o main, faça suas alterações serem implantadas e testadas no staging até que estejam prontas, então implantar em produção.

Como último passo, escolha a dedo seu commit do principal ao desenvolvimento para garantir que a próxima versão contenha todas as correções. Caso você acabe com vários hotfix commits, você economiza esforço - especialmente se seu IDE ou outra ferramenta Git puder facilitar - criando e aplicando um patch em vez de escolher várias vezes. Tentar squash merge main para desenvolver após o lançamento inicial provavelmente resultará em conflitos com o progresso independente feito na ramificação de desenvolvimento, então eu não recomendo isso.

Lidar com hotfixes durante uma versão ativa — ou seja, quando você apenas força o push principal e ainda está preparando a nova versão — é a parte mais fraca do fluxo aprimorado do Git. Dependendo da duração do seu ciclo de lançamento e da gravidade do problema que você precisa resolver, sempre procure incluir correções na nova versão - essa é a maneira mais fácil de seguir e não interromperá o fluxo de trabalho geral.

Caso isso seja impossível - você precisa introduzir uma correção rapidamente e não pode esperar que a nova versão fique pronta - então prepare-se para um procedimento Git um tanto intrincado:

  1. Crie um branch—nós o chamaremos de “new-release”, mas sua equipe pode adotar qualquer convenção de nomenclatura aqui—no mesmo commit que a dica atual de main. Empurre nova versão.
  2. Exclua e recrie a ramificação principal local no commit da tag que você criou anteriormente para a versão ativa atual. Força empurrão principal.
  3. Introduza as correções necessárias para main, implante no ambiente de preparo e teste. Quando estiver pronto, implante para produção.
  4. Propagar alterações do principal atual para o novo lançamento por meio de seleção de cerejas ou de um patch.
  5. Depois disso, refaça o procedimento de liberação: marque a ponta do main atual e empurre a tag, exclua e recrie o main local na ponta do branch new-release e force o push do main.
    1. Você provavelmente não precisará da tag anterior, então você pode removê-la.
    2. A ramificação de nova versão agora é redundante, então você também pode removê-la.
  6. Agora você deve estar pronto para ir como de costume com uma nova versão. Termine propagando os hotfixes de emergência do principal para o desenvolvimento via seleção de cereja ou um patch.

Gráficos de confirmação do Git conforme eles mudam ao executar um hotfix durante uma versão ativa no fluxo Git aprimorado. O gráfico inicial se desenvolveu como a linha mais longa de commits, com main divergindo dois commits anteriores por um commit, e três commits antes disso, a ramificação a diverge por um commit, marcado v2020-09-18. Após as duas primeiras etapas acima, o gráfico tem nova versão onde o main costumava estar, e o main não está nem com a v2020-09-18. O restante das etapas é executado, resultando no gráfico final, em que main é um commit antes de onde a nova versão estava e v2020-09-20 é um commit antes de onde v2020-09-18 estava.

Com planejamento adequado, qualidade de código alta o suficiente e uma cultura saudável de desenvolvimento e controle de qualidade, é improvável que sua equipe tenha que usar esse método. Era prudente desenvolver e testar esse plano de desastre para fluxo aprimorado do Git, apenas por precaução, mas nunca precisei usá-lo na prática.

Configuração de CI/CD em cima do fluxo Git aprimorado

Nem todo projeto requer um ambiente de desenvolvimento dedicado. Pode ser bastante fácil configurar um ambiente de desenvolvimento local sofisticado em cada máquina do desenvolvedor.

Mas um ambiente de desenvolvimento dedicado pode contribuir para uma cultura de desenvolvimento mais saudável. Executar testes, medir a cobertura de testes e calcular métricas de complexidade na ramificação de desenvolvimento geralmente diminui o custo dos erros, detectando-os bem antes que eles acabem no teste.

Achei alguns padrões de CI/CD especialmente úteis quando combinados com o fluxo aprimorado do Git:

  • Se você precisar de um ambiente de desenvolvimento, configure o CI para compilar, testar e implantar nele em cada confirmação para a ramificação de desenvolvimento. Encaixe nos testes E2E aqui também, se você tem e se faz sentido no seu caso.
  • Configure o CI para compilar, testar e implantar no ambiente de preparo em cada confirmação para a ramificação principal. O teste E2E também é bastante benéfico neste momento.
    • Pode parecer redundante usar o teste E2E em ambos os lugares, mas lembre-se de que os hotfixes não acontecerão no desenvolvimento. Acionar o E2E em commits para main testará os hotfixes e as mudanças diárias antes que eles saiam, mas também acionar em commits para desenvolvimento detectará bugs mais cedo.
  • Configure o CI de uma maneira que permita que sua equipe implante compilações do ambiente principal para o ambiente de produção mediante solicitação manual.

A configuração recomendada de CI/CD com fluxo Git aprimorado quando há um ambiente de desenvolvimento ("dev") além de preparação ("stage") e produção ("prod"). Todos os commits na ramificação de desenvolvimento resultam em uma implantação de compilação para dev. Da mesma forma, todos os commits para main resultem em uma build implantada no stage. Uma solicitação manual de uma confirmação específica de main resulta em uma implantação de compilação para produção.

Esses padrões são relativamente simples, mas fornecem um maquinário poderoso para dar suporte às operações de desenvolvimento do dia-a-dia.

O modelo de fluxo Git aprimorado: melhorias e possíveis limitações

O fluxo aprimorado do Git não é para todos. Ele alavanca a tática controversa de forçar o ramo principal, então os puristas podem se ressentir. Do ponto de vista prático, não há nada de errado com isso, no entanto.

Como mencionado, os hotfixes são mais desafiadores durante um lançamento, mas ainda são possíveis. Com a devida atenção ao controle de qualidade, cobertura de teste, etc., isso não deve acontecer com muita frequência, então, da minha perspectiva, é uma compensação válida para os benefícios gerais do fluxo Git aprimorado em comparação com o fluxo Git clássico. Eu estaria muito interessado em saber como o fluxo aprimorado do Git funciona em equipes maiores e com projetos mais complexos, onde os hotfixes podem ser uma ocorrência mais frequente.

Minha experiência positiva com o modelo de fluxo Git aprimorado também gira principalmente em torno de projetos comerciais de código fechado. Pode ser problemático para um projeto de código aberto em que as solicitações pull geralmente são baseadas em uma derivação de versão antiga da árvore de origem. Não há obstáculos técnicos para resolver isso - pode exigir mais esforço do que o esperado. Congratulo-me com o feedback de leitores com muita experiência no espaço de código aberto sobre a aplicabilidade do fluxo Git aprimorado nesses casos.

Agradecimentos especiais ao colega da Toptal, Antoine Pham, por seu papel fundamental no desenvolvimento da ideia por trás do fluxo aprimorado do Git.


Leitura adicional no Blog da Toptal Engineering:

  • Desenvolvimento baseado em tronco vs. Git Flow
  • Fluxos de trabalho do Git para profissionais: um bom guia do Git

Selo Microsoft Gold Partner.

Como Microsoft Gold Partner, a Toptal é sua rede de elite de especialistas da Microsoft. Construa equipes de alto desempenho com os especialistas que você precisa - em qualquer lugar e exatamente quando você precisar deles!