Git Flow na vida real de modo prático!
Ah, me lembro como era fazer o versionamento dos meu projetos em pastinhas. Era aquela coisa bonita. Uma pasta principal, que quando ficava pronta a gente subia pra produção e para não perder uma versão, criavamos uma cópia da pasta para trabalhar. A anterior ganhava o sufixo “_old”. Quando novamente uma versão era lançada, uma nova cópia era feita e as pastas anteriores eram renomeadas para “_old” e “_old_old”, ou, “_old2”. Mas quando precisamos de fato voltar para uma versão de acordo com as mudanças, era uma bagunça só!
As coisas evoluiram quando comecei a por o nome das pastas com datas. então virava algo como 20200701. A cópia dela, era renomeada para a data que foi gerada. Isso já facilitou pelo menos localizar a ordem das versões.
Não muito tarde, comecei a usar SVN para fazer o versionamento o que parecia verdadeiramente uma mágica! Mas, em pouco tempo, o ainda pouco utilizado GIT entrou em minha vida e desde então não saiu mais. Até hoje é com ele que faço o versionamento dos meus projetos.
No início, todos do time com pouco conhecimento ainda, o trabalho era feito praticamente na master. Gerenciar branches não era algo comum. Aos poucos começamos a usar a develop e o processo de pull request para jogar o código estável para a master. Mais adiante, percebemos que também não era o suficiente e aprendemos a usar branches auxiliares para o desenvolvimento, cada um no seu quadrado. Usavamos a develop como uma branch semi-estável, onde o projeto e o código ali contido, passava pelo processo de homologação.
Esse formato perdurou por muito tempo (muito mesmo) no meu dia a dia e funcionava muito bem. Até mesmo quando conheci a tal de integração contínua e os principios do DevOps. Desenvolvia em branches auxiliares, a develop era a trigger para, além do CI, ativar o processo de entrega contínua (CD) e fazer o deploy automatizado no ambiente de homologação. Por fim, quando um código era “mergeado” para master, tudo ficava no ponto, por apenas um clique, do deploy no ambiente produtivo. Linda história, né?
Mas, com toda a evolução nos processos automatizados de distribuição de software, cada vez entregavamos mais versões e em tempo recorde. Diversos deploys na mesma semana, até mesmo no mesmo dia. Além disso, tinhamos times multidisciplinares e usavamos metodologia ágil, onde as tarefas conseguiam ser bem pequenas e conseguiamos paralelizar bastante as atividades. Com muitas versões lançadas, muitas pessoas trabalhando de forma rápida no mesmo código, tinha tudo pra dar errado. E deu!
Era um show de horrores. Conflito pra todos os lados, bugs que subiam despercebidos, não por um erro de desenvolvimento, mas por falha na resolução de merges e conflitos. Era uma festa onde o revert e o reset faziam par e dançavam de uma forma que ninguém queria assistir, muito menos participar.
Foi nesse momento que decidimos que estava na hora de evoluir como faziamos a gestão das branches e das versões dos softwares que desenvolviamos. Em nossos estudos por tais fluxos, optamos pela adoção do Git Flow. Não é uma bala de prata, tem seus pontos falhos, mas ainda assim é simples de utilizar, resolveu nossos problemas, garantiu a organização das nossas branches, código, atividades em paralelo e ainda nos ajudou na melhor gestão das versões.
Git Flow
Quando estamos trabalhando sozinho ou em times pequenos e/ou com pouca alteração no mesmo repositório, é comum adotarmos pouco controle (às vezes até nenhum) sobre o gerenciamento das branches dos nossos projetos. Porém conforme a complexidade do projeto e equipe aumentam, o que era simples, como um pequeno ajuste, pode dar bastante dor de cabeça e por isso criar regras para ter uma boa organização e gestão das modificações é importante.
Resumidamente, o Git Flow é um conjunto de diretrizes que times de desenvolvimento podem seguir para organizar as branches e as suas versões.
O Git Flow não é rígido na sua essência, logo, funciona como um framework, onde você utiliza o que é interessante pro seu dia-a-dia e tem a liberdade de customizar o que quiser. Para ser mais fácil ter referências e também para quando um novo membro chegar no time (que já conheça o modelo) se adequar rapidamente ao processo, evite customizar “demais”.
Existem outros modelos além do Git Flow para fazer gestão das branches, como por exemplo o GitHub Flow e o GitLab Flow. Muitas pessoas acham o Git Flow “complexo” e criticam o seu funcionamento, também pelo fato de ser baseado em “merge” e não deixar o histórico taaão clean. Mas a gestão funciona muito bem e no fundo é bem fácil. Na minha experiência até hoje, ele foi o mais eficiênte, principalmente quando tenho várias pessoas atuando no mesmo projeto, ao invés do velho lobo solitário.
Junto com meu lindo fluxo de CI/CD, se tornaram os melhores amigos, facilitando o uso do Semantic Versioning e liberação de versões alpha, beta, release candidate etc.
Como funciona?
Vou fazer um explicação baseada no seu funcionamento original e de acordo minhas poucas customizações e adequações. Para começar vamos analisar essa imagem:
Branches fixas e principais
As branches master e develop são as únicas que sempre existirão. O big bang de um projeto (o seu primeiro commit) é o único feito diretamente na master. Junto com ele adicionamos a primeira tag, algo como 0.1.0. Logo em seguida criamos a develop a partir da master. A partir daí, não podemos fazer mais commits diretamente nessas duas branches.
- master: É a branch estável com o código que reflete exatamente a última versão distribuida. Essa branch não recebe commits diretos. Em meu fluxo de CI, o deploy em produção é disponível apenas quando algo novo “chega” na master com uma tag (para quem não sabe, tag seria apenas um marcador na linha do tempo, como um número de versão “1.5.3”).
- develop: Essa branch contém o código da nossa próxima versão, ou seja, conforme as features e pequenos ajustes vão sendo finalizados, eles são juntados na develop para posteriormente passarem por mais uma etapa (criação da release) antes de ser juntada com a master.
Branches de lançamento e correções — Ou, “preparo de versão”
O nome “preparo de versão” é um apelido carinho que algumas pessoas atribuem para essas branches. O fato é que essas branches “precedem” a geração do merge com a master, e, logo após isso, a geração da tag na master na posição do commit de merge. O número da versão (tag) é justamente o sufixo dessas branches. Após a criação da tag, o código também é levado para a develop para garantir que ambas sempre estão atualizadas.
- release/*: São usadas na preparação do lançamento da próxima versão. Uma branch de release sempre é criada a partir de develop. Na criação da branch de release é decidido qual o número da próxima versão. No meu caso, digamos que o projeto está na versão 1.1.4 e será criada uma nova branch de release. O número dessa release será 1.2.0 (incrementamos o {minor} e resetamos o {patch} para zero de acordo com o Semantic Versioning). Nesse exemplo, seria criada a branch release/1.2.0. Em casos que a develop receba alguma nova feature que necessite ser incluida nessa release em aberto, apenas fazemos um merge da develop para a branch de release. Esta branch deve existir até que esteja pronta para ser liberada para produção. Pequenas correções são permitidas (commits direto na branch de release) caso necessário. Mas somente pequenos ajustes.
- hotfix/*: Branches de hotfix (correções emergenciais) são muito parecidas com branches de lançamentos em sua concepção, tem o mesmo objetivo de preparar uma versão para produção, embora não planejada. Elas surgem da necessidade de agir imediatamente em uma versão de produção já implantada. Quando um bug crítico ocorre em produção, uma branch de hotfix precisa ser criada a partir da master (para não levar outras coisas que estejam em andamento). A ideia é que o time que está trabalhando na próxima versão na branch develop possa continuar enquanto alguém prepara uma correção. No mesmo exemplo que fiz anteriormente, digamos que o projeto está na versão 1.1.4 e será criada uma nova branch de hotfix. O número dessa release será 1.1.5 (incrementamos o {patch}). Nesse exemplo, seria criada a branch hotfix/1.1.5. Essa branch recebe as correções diretamente nela e também só deve existir até que esteja pronta para ser liberada para produção. Assim como a branch de release, a finalização dessa branch faz o merge para master, cria a tag e também faz o merge para a develop.
Branches de melhoria ou desenvolvimento
Essas branches em grande resumo, são todas as outras branches diferentes das citadas até agora (master, develop, release/*, hotfix/*). Elas são sempre criadas a partir da develop e devem retornar para a develop somente quando parte da próxima release, caso contrário a branch permanece aberta. Os padrões do Git Flow usam os prefixos feature/* (implementação de uma nova funcionalidade ou melhoria técnica) e bugfix/* (correções planejadas, não emergenciais). O complemento do nome da branch deve ser, de forma sucinta, o que ela se propõe em fazer, por exemplo: feature/accept-credit-card. No meu caso, permitimos uso de outros prefixos, como descrito abaixo:
- feature/*: Implementa uma nova funcionalidade no sistema (requisito funcional). Seu resultado deveria prover ao cliente final um novo recurso.
- bugfix/*: Qualquer correção planejada de algo que não está acontecendo como deveria, fora da regra de negócio, sem a implementação de novas funcionalidades, apenas ajustando algo já existente.
- requirement/*: Efetua melhorias e implementações puramente técnicas (requisitos não-funcionais). Seja uma refatoração, inclusão de logs, otimização de performance, documentação etc.
Branches de suporte
Seriam as branches com o nome support/*, mas de forma geral, não use, finja que não existe, ok? Elas servem pra criar literalmente um “suporte” para versões “antigas e sem compatibilidade” (normalmente mudanças no major da versão). Nesse caso, essa branch tem vida própria e os commits, tags, acionamentos de CI/CD acontecem no seu escopo e as alterações dela não refletirão no restante das branches. Podemos imaginar como um “fork” dentro do próprio projeto, feito em branches que nunca mais irão se juntar. No lugar disso, crie outro repositório (pode ser um fork de verdade hehe) para manter a versão anterior.
Como fazer na linha de comando?
O Git Flow não é uma ferramenta padrão do Git, mas as suas versões mais atuais já vem com o plugin instalado. Faça um teste no seu terminal git flow version para verificar se você já tem o plugin. Caso não tenha, atualize o seu Git ou siga as instruções no repositório oficial do Git Flow. Importante lembrar que tudo que o plugin faz, é possível fazer “na mão”, apenas com comandos nativos do Git, sendo o plugin apenas um “facilitador”. No geral, acredito que ele realmente facilite a criação e a finalização das branches de release e hotfix, dado algumas validações nas branches, merge final para master e develop e a criação da tag.
Veja abaixo um exemplo com comandos “puramente” Git e com uso do plugin do Git Flow.
Pull Request
Podemos trabalhar com Git Flow e Pull Request. Mas, isso não é gerenciado pelo plugin, quando o utilizamos. Tudo que vem das branches de melhoria (feature, bugfix etc) pode ser enviado para a develop via Pull Request, ao invés de um merge manual, até ai bem tranquilo. Assim conseguimos garantir que o time está revisando tudo que chega na branch develop.
Para a branch master, as coisas mudam um pouco. As branches que originam merges para a master (branches de “preparo de versão” — release e hotfix) também precisam fazer o merge para a develop. O processo para que PRs para a master funcione, basicamente consiste em, após o PR criado, aprovado e “mergeado” da release/hotfix para master, fazemos a finalização da release/hotfix normalmente pela linha de comando. Usando comandos do Git Flow, tudo funciona normal, e com comandos do Git podemos “pular” a etapa de merge com a master.
Em alguns times que utilizam processos semelhantes é possível observar a adoção apenas de pull request a partir de hotfix dado que todo código feito nessa branch não é validada e revisada em outra etapa. Branches de release são iniciadas a partir da develop, branch previamente validada,
Fluxo de CI/CD
Para o acionamento do fluxo de CI/CD e a distribuição de versões, a seguinte configuração pode ser adotada para quem usa ambientes de desenvolvimento compartilhado, homologação e produçao:
- develop: Libera a versão no ambiente compartilhado de desenvolvimento, ambiente normalmente interno do time de desenvolvimento para testes em conjunto com outros serviços. Costuma ser um ambiente mais caótico e instável. Esse ambiente ainda desconhece o próximo número de versão, então você pode utilizar um número incremental de build apenas para saber a ordem das coisas. Um pouco melhor do que isso, você pode deduzir que o código da develop, obviamente, não é um hotfix e identificar automaticamente a versão que ele entrará (próxima alteração do {minor} da última versão existente). Para essas versões, pode-se usar o sufixo alpha. Por exemplo, minha última tag na master foi 1.3.6, faço a alteração do {minor} e crio a versão 1.4.0-alpha.1. Num próximo merge na develop, de outra feature, caso a última tag seja a mesma, será gerado a versão 1.4.0-alpha.2. Se você tiver uma branch de release em aberto, você precisa tomar uma decisão: Por padrão, meus commits em develop serão associados a essa versão em aberto (o mesmo número do exemplo) ou serão associados ao número posterior da versão que está em aberto? No geral, faz sentido ser para uma versão futura à versão que está aberta. Não é muito comum ficarmos adicionando features e mantendo uma release em aberto.
- release/* ou hotfix/*: Libera a versão no ambiente de homologação para a execução de testes e validações automatizadas, pelo time, stakeholders, etc. Nesse momento já temos o número da versão como sufixo da branch e podemos utiliza-lo para gerar versões beta ou preview. Por exemplo, 1.1.5-preview.1 sendo o último o número de “build”. Lembra que podemos ter pequenos ajustes direto na branch de release e hotfix recebe todos os commits diretos? Então, esses commits apenas mudariam o número do build. Um commit com um ajuste nessa mesma branch geraria agora a versão 1.1.5-preview.2.
- tag (master): Sempre que uma nova tag for criada na master preparamos e distribuimos a versão, finalmente, em produção. A tag, para o exemplo anterior, seria 1.1.5.
Para times que não utilizam ambiente compartilhado de desenvolvimento, para não precisar ficar abrir releases para que o código entre no único ambiente integrado de teste / homologação disponível, pode-se adotar o envio de uma flag na mensagem de commit (por exemplo [staging 1.2.0]) que faça o build da versão alpha e o seu deploy no ambiente de homologação, para fins de testes integrados.
Importante que esses fluxos sejam alinhados com o time, visando todos terem o conhecimento de qual versão está rodando naquele ambiente e evitando conflito de interesses (quando mais de uma pessoa quer usar o ambiente para subir uma versão alpha hehe).
Release Candidates
Para alguns tipos de aplicação podemos considerar o tipo preview apresentado anteriormente como uma release candidate, porém, são versões incrementais e não concorrentes.
O problema ocorre em versões concorrentes porque o Git Flow não permite mais de uma release simultânea. Para criar release candidates, nesses casos, vamos imaginar uma aplicação na versão 1.5.0. Ela está com duas “versões concorrentes” em análise para saber qual vai subir, pode ser somente uma mudança na implementação técnica, mas no fim, oferecem o mesmo recurso e precisamos ter as duas disponíveis. Posso querer ter a versão 1.5.0-rc.1 e a versão 1.5.0-rc.2 (respectivamente release/1.5.0-rc.1 e release/1.5.0-rc.2). São duas versões distintas, que, quando uma é escolhida, você cria a release/1.5.0 a partir dela, deleta as anteriores e segue para o fluxo de liberação final.
Porém, esse fluxo só pode existir se criarmos duas releases simultâneas. E como fazemos? Simples, fazemos com comandos nativos do Git ou customizamos aliases e functions e compartilhamos com o time para ajustarem seus terminais para o processo que desejamos. 💁♂️
Posso finalizar este (loooooongo) artigo, falando que, no meu cenário, dado a minha experiência e a experiência do meu time com todos os processos ao redor do “desenvolvimento de software”, do planejamento à distribuição, o Git Flow tem ajudado muito na organização de branches e gestão de versões. O principal problema que mais incomoda as pessoas em relação ao Git Flow é a “timeline” de commits suja por conta dos diversos “merges” ao longo do processo. Pra nós, não é um grande problema, visto que em processos de blame, revert e afins não percebemos dificuldades adicionais. Contudo, essa é uma bela e grande discussão, sobre os comandos merge (fast forward e no ff) e rebase, podendo se extender para o squash e cherry pick, que prometo abordar em um próximo artigo.
Se você chegou até aqui, parabéns! 👏