Git Workflow

Introdução

O Git possibilita uma variedade de estratégias de uso, o que pode tornar o processo de desenvolvimento complicado. Visando trazer produtividade e simplificar o uso do Git, bem como viabilizar integração contínua via ferramentas de CI/CD, propõe-se neste capítulo um conjunto claramente definido de boas práticas e fluxos de trabalho, projetado para suportar uma grande quantidade de desenvolvedores atuando concorrentemente nos projetos.

Pré-requisitos

Para leitura deste capítulo, são pré-requisitos conhecimentos básicos de Git: o que são branches e as operações comuns commit, pull, merge e push.

Diretrizes e Convenções

  • Deverá existir apenas 1 branch de longa duração, o tronco, que contém o código estável, sempre apto para publicação em produção.

  • A implementação de mudança, seja evolutiva ou corretiva, emergencial ou eletiva, deverá ser realizada em branch temporário, criado a partir do tronco.

  • Uma vez que a mudança foi aprovada pelo responsável técnico e/ou negocial, seu branch deverá ser reintegrado no tronco e então removido.

  • O branch de mudança deverá ser atualizado no repositório local, para recebimento das novidades do código estável, conforme a política:

    • Frequentemente, com vistas a minimizar esforço de adaptações causadas por impacto de outras demandas (conflitos).

    • Tipicamente antes da disponibilização para teste ou homologação.

    • Necessariamente antes da reintegração no tronco.

  • A atualização local de branch de mudança deverá ocorrer por merge, podendo ser com fast forward.

  • A reintegração do branch de mudança no tronco deverá ocorrer por merge ou Merge Request, sem fast forward.

  • Não utilizar as operações rebase e squash.

  • Evitar alterar commit message, pois o Git refaz os commits subsequentes por rebase, podendo “danificar” o histórico.

  • Um branch temporário de nome release poderá ser criado caso o código-fonte estável do tronco requeira ajuste de configuração para fins de deployment. Esse release branch representa uma janela técnica de preparação para produção.

  • Enquanto existir o release branch, demandas de mudança não deverão ser reintegradas no tronco.

  • Tags para teste ou homologação de determinada mudança são criadas a partir do seu respectivo branch, enquanto tags de produção são criadas a partir do tronco ou do release branch, se aplicado.

  • Nome sugerido para o branch tronco: master (ou main ou stable).

  • Nome sugerido para branch de mudança: “dev” + <número da demanda>, ex: dev77.

    • Quando relativo a um grupo de demandas, o nome poderá ser composto dos números das demandas separados por hífen, ex: dev105-98-62.

    • Se relativo a uma demanda sem registro, sugere-se limitar-se aos caracteres a-z (minúsculos), números e hífen: negativacao-cartorio.

  • Nome sugerido para tag: <nome do branch> + “.” + <sequencial>, ex: dev77.1, dev77.2, master.1.

  • Os nomes dos pacotes de aplicação, como builds, imagens Docker, etc, terão os mesmos nomes das respectivas tags de origem. Ex: tag dev77.1 gera imagem Docker de nome dev77.1.

  • No caso de aplicações legadas cujo profile (ex: homologação ou produção) é definido durante o build, os nomes dos pacotes serão sufixados com o profile:

    • Tag dev77.1 gerar imagem Docker dev77.1.homologacao.

    • Tag master.1 gerar imagens Docker master.1.homologacao e master.1.producao.

    • O script de empacotamento, como pipeline CI/CD, deverá tratar cada cenário, gerando somente os profiles aplicáveis para o tipo de branch.

Diagrama

O diagrama abaixo exemplifica uma sequência temporal do histórico de um repositório:

Gitflow

  • Nesse exemplo, a demanda dev50 teve sua primeira tentativa de homologação dev50.1 reprovada. Após ajuste, a versão dev50.2 foi aprovada e então reintegrada no tronco.

  • A demanda dev77 foi aprovada conforme tag dev77.1 e sua reintegração ficou pendente durante um tempo (curto), enquanto ocorria uma janela de preparação para produção, tag master.5.

Próximos Passos

IDEs como IntelliJ e VSC oferecem opções gráficas para uso do Git.

Observação: O IntelliJ não tem suporte gráfico a tags (2024), sendo necessária a utilização de comandos via terminal somente neste caso. As tags podem ser criadas pela interface web do GitLab ou GitHub.

O anexo a seguir traz exemplos de como realizar os fluxos de trabalho por linha de comando, em conformidade com as diretrizes e convenções acima dispostas. Entretanto, dificilmente será necessário utilizar comandos, salvo se for sua preferência.

Importante considerar as dicas constantes em “Configurando o repositório” do anexo “Fluxos de Trabalho”.

Anexos

I - Fluxos de Trabalho

A seguir são exemplificados fluxos do processo de trabalho por linha de comando. Verifique o anexo “Cartilha de Comandos” ou a documentação oficial do Git para mais detalhes sobre cada comando.

Obtendo o repositório:

git clone <url>

Configurando o repositório:

git config push.followTags true # Faz o Git enviar as tags junto com o push.
git config pull.rebase false    # Desativa o rebase durante o pull. 
git config core.autocrlf false  # Evita que scripts .sh sejam alterados de LF para CRLF (impede execução no Mac)

Se na raiz do repositório existir o arquivo config.sh (Linux/Mac) ou config.bat (Windows), recomenda-se execulá-lo para ativar extensões do Git (vide anexo “Git Config”).

É possível também, opcionalmente, adicionar um commit hook que complementa automaticamente o comentário de commits adicionando o nome da demanda como prefixo da mensagem (vide anexo “Git Hooks”).

Começando nova demanda:

git checkout master
git pull
git checkout -b dev50
git push -u origin HEAD

Participando de uma demanda existente:

git fetch
git checkout dev70

Recebendo novidades do repositório remoto relativas à demanda:

git pull

Recebendo novidades do tronco (código estável):

git fetch
git merge master

Salvando as alterações locais:

git add -A
git commit -m "Implementação de ..."

Enviando a demanda para o repositório remoto:

git pull # Recebe novidades relativas à demanda
git push # Envia

Enviando a demanda para teste ou homologação:

git pull                               # Recebe novidades relativas à demanda
git merge master                       # Recebe novidades do tronco
git tag -l "dev50.*"                   # Lista as tags existentes
git tag -a dev50.1 -m "Versão dev50.1" # Cria a próxima tag (o sequencial inicia em 1)
git push                               # Envia

Finalizando uma demanda aprovada (via Merge Request):

Acesse a interface web do Git e crie um Merge Request do branch da demanda. O branch local poderá ser removido quando o Merge Request for aplicado. Certique-se de que a tag do branch foi aprovada pelo responsável técnico e/ou negocial e que não houve novos commits no branch após a aprovação.

Finalizando uma demanda aprovada (manual):

Certique-se de que a tag do branch foi aprovada pelo responsável técnico e/ou negocial e que não houve novos commits no branch após a aprovação.

git checkout master                                # Acessa o tronco local
git pull                                           # Atualiza o tronco
git merge --no-ff -m "Merge de dev50" origin/dev50 # Faz o merge da demanda
git tag -l "master.*"                              # Lista as tags existentes
git tag -a master.1 -m "Versão master.1"           # Cria a próxima tag (o sequencial inicia em 1)
git push                                           # Envia
git branch -d dev50                                # Remove o branch local
git push origin :dev50                             # Remove o branch remoto

Finalizando uma versão de produção:

git checkout master                                # Acessa o tronco local
git pull                                           # Atualiza o tronco
git checkout -b release                            # Cria o branch de release
git push -u origin HEAD                            # Envia branch de release para sinalizar a janela de mudança

# Agora ajuste os arquivos de configuração necessários

git add -A                                     # Prepara os arquivos de configuração para commit
git commit -m "Preparação para produção"       # Salva as mudanças
git tag -l "master.*"                          # Lista as tags existentes
git tag -a master.1 -m "Versão master.1"       # Cria a próxima tag (o sequencial inicia em 1)
git checkout master                            # Retorna para o tronco
git merge --no-ff -m "Versão master.1" release # Faz o merge do release
git push                                       # Envia
git branch -d release                          # Remove o branch local
git push origin :release                       # Remove o branch remoto

Nota: Caso o projeto não requeira ajustes de configurações, apenas crie a tag de produção diretamente a partir de tronco:

git checkout master                      # Acessa o tronco local
git pull                                 # Atualiza o tronco
git tag -l "master.*"                    # Lista as tags existentes
git tag -a master.1 -m "Versão master.1" # Cria a próxima tag (o sequencial inicia em 1)
git push                                 # Envia

II - Tarefas de Apoio

Este anexo contém tarefas úteis para manutenção do repositório local. IDEs oferecem opções gráficas para a maioria dessas tarefas.

Limpando branches locais que foram removidos do remoto:

git checkout master
git fetch origin --prune
git branch -a

Limpando um branch local que não tem referência remota:

git branch -d dev50

Salvando todas as mudanças:

git add -A
git commit -m "<Mensagem aqui>"

Analisando quais mudanças serão inclusas no commit:

O próximo commit incluirá as mudanças marcadas como staged. O comando abaixo mostra o conteúdo da stage area:

git status # Mostra o conteúdo da stage area
git reset  # Limpa a stage area

Revertendo as mudanças:

Esse comando volta o projeto para o estado do último commit. Essa operação não pode ser desfeita. Semelhante ao revert do SVN. Tecnicamente é feita limpeza da stage area e do workdir.

git reset --hard

Revertendo o último commit:

Esse comando desfaz o último commit completamente. Essa operação não pode ser desfeita. Tecnicamente o ponteiro do branch volta para o commit anterior ao commit atual, sendo feita limpeza da stage area e do workdir.

git reset --hard HEAD~1

Cancelando o último commit:

Esse comando desfaz o último commit, porém mantém os arquivos em stage e mantém no workdir. Se executar git commit retornamos exatamente ao estado imediatamente anterior ao reset. Útil caso queira ajustar o último commit.

git reset --soft HEAD~1

Opcionalmente pode-se fazer conforme abaixo. Neste caso, além do retorno do ponteiro, as mudanças ficarão unstaged. O workdir é mantido.

git reset HEAD~1

Revertendo para a última versão submetida:

Esse comando volta o branch para o último commit que teve push. Tudo será desfeito localmente. Essa operação não pode ser desfeita.

git fetch
git reset --hard origin/dev50

Revertendo um arquivo específico:

git checkout -- <arquivo>

III - Cartilha de Comandos

A seguir apresenta-se uma cartilha dos comandos Git mais importantes e seus principais parâmetros.

Clone:

git clone <url>

Faz o download do repositório remoto para o diretório local.

  • url endereço HTTP do repositório remoto

Config:

git config [--global] <name> <value>

Configura o comportamento do Git.

  • --global aplica a configuração no profile global ~/.gitconfig em vez de aplicar no próprio repositório .git/config
  • name nome da configuração, vide documentação oficial
  • value valor da configuração, vide documentação oficial

Branch:

git branch [-a]
git branch -d <name>

Lista os branches do repositório local ou remove um branch local.

  • -a ao listar os branches, inclui os remotos
  • name nome do branch local a ser removido

Add:

git add -A

Adiciona todos os arquivos novos, alterados e removidos para a lista de itens que são inclusos no próximo commit. Essa lista é chamada de stage area.

Commit:

git commit -m "<comment>"

Salva as alterações feitas no repositório local. O commit cria um snapshot (fotografia ou versão) dos arquivos, identificado por um hash único que o torna rastreável e referenciável.

  • comment comentário

Fetch:

git fetch [remote] [--prune] [--prune-tags]

Baixa os novos commits, branches e tags do repositório remoto para o local.

  • remote identificação do repositório remoto, por padrão origin
  • --prune remove referências locais de branches remotos que não mais existem no repositório remoto
  • --prune-tags remove tags locais que inexistem no remoto

Checkout:

git checkout <name>
git checkout -b <new> [src]

Alterna para determinado branch ou cria um novo branch local.

  • name branch para o qual ocorrerá o switch
  • -b cria um novo branch
  • new nome do novo branch a ser criado a partir de src
  • src por padrão o branch atual

Se existir branch remoto com mesmo nome new, o branch local terá tracking para ele. Caso não existir, o tracking poderá ser definido no momento do push. Não use src referenciando um remoto (ex: origin/master).

Merge:

git merge [--no-ff] -m "<comment>" <src>

Faz o merge de determinado branch sobre o branch atual.

  • --no-ff forção geração de novo commit mesmo quando fast forward for possível
  • comment informe no padrão “Merge de <src>”
  • src nome do branch que será aplicado sobre o branch atual

Esse comando considera a versão do branch que existe no repositório local. Normalmente é conveniente executar o fetch antes.

Pull:

git pull [remote]

Faz fetch seguido de merge: primeiro atualiza o repositório local e em seguida faz o merge do branch remoto sobre o branch local.

  • remote identificação do repositório remoto, por padrão origin

Tag:

git tag [-l "<exp>"]
git tag -a -m "<comment>" <name> [src]

Lista ou cria tags.

  • -l lista as tags conforme expressão exp (wilcards aceitos: *, ?, [abc], [!abc] outros conforme fnmatch)
  • -a cria uma tag do tipo Anotada
  • comment comentário obrigatório
  • name nome da tag a ser criada
  • src commit de origem para o qual a tag será referenciada (pode ser nome de um branch ou hash de um commit)

Push:

git push [--set-upstream origin <name>] [--follow-tags]
git push origin :<remove>

Envia o branch atual do repositório local para o remoto. O segundo comando remove um branch do repositório remoto.

  • --set-upstream configura o tracking como sendo o branch remoto de nome name
  • --follow-tags inclui as tags no push
  • remove nome do branch a ser removido do repositório remoto

IV - Git Config

Este anexo contém scripts que estendem os comandos do Git.

Os seguintes comandos são adicionados:

  • git revert: Volta o código-fonte para o commit anterior.
  • git uncommit: Cancela o último commit, porém mantém os arquivos alterados e a stage area. Útil quando se deseja fazer novas alterações no código-fonte e regerar o commit.
  • git unstage: Cancela o último commit e limpa a stage area, mantendo os arquivos alterados. Útil quando se deseja especificar quais alterações entrarão no commit.
  • git rlog: Imprime o log seguindo o percurso raiz dos merges. Ou seja, quando existe um merge, o rlog exibe apenas o commit aplicado no branch, sem exibir o histórico de commits do branch que foi reintegrado. Útil para visualizar as alterações no branch principal (master).
  • git prunetags: Remove as tags locais que não existem no repositório remoto.

Arquivo /.gitconfig:

[alias]
    unstage = reset HEAD
    revert = reset --hard HEAD
    uncommit = reset --soft HEAD~1
    rlog = log --oneline --decorate --graph --first-parent --author-date-order --pretty=format:'%C(yellow)%h|%Cred%ad|%Cblue%<(15,trunc)%an|%Cgreen%d %Creset%s' --date=short
    tagref = rev-list -n 1

Arquivo /config.sh:

#!/bin/sh
git config include.path ../.gitconfig

Arquivo /config.bat:

git config include.path ../.gitconfig

V - Git Hooks

Este anexo contém script que adiciona um commit hook responsável por incluir automaticamente o nome do branch nos comentários de commit. Requer Git versão 2.9+.

Arquivo /.gitconfig:

[core]
    hooksPath = .githooks

Arquivo /.githooks/prepare-commit-msg:

#!/bin/sh
  
COMMIT_MSG_FILE=$1
COMMIT_SOURCE=$2
SHA1=$3

# Gets branch name
BRANCH_NAME=$(git branch | grep '*' | sed 's/* //')
PREFIX="[${BRANCH_NAME}]"

# Gets merged branch info
#if [ "$COMMIT_SOURCE" = "merge" ]
#then
#        # Retrieve merged branch name from an env var GITHEAD
#        # We cannot use a sym ref of MERGE_HEAD, as it doesn't yet exist
#        HEAD=$(env | grep GITHEAD) # e.g. GITHEAD_<sha>=dev77
#
#        # Cut out everything up to the last "=" sign
#        SOURCE="${HEAD##*=}"
#
#        MSG="${MSG} Merge from '${SOURCE}'"
#fi

# Removes lines starting with #
# printf %s "$(grep -s -v '^ *#' "$COMMIT_MSG_FILE")" > "$COMMIT_MSG_FILE"

# Get first character of the message
FIRST=$(head -c 1 "$COMMIT_MSG_FILE")

# Adds a separator between prefix and the message
if [ "$FIRST" = "#" ] || [ "$FIRST" = "\n" ]
then
        PREFIX="${PREFIX}\n"
else
        # If other chars (not empty)
        if [ ${#FIRST} -ge 1 ]
        then
                PREFIX="${PREFIX} "
        fi
fi

# Writes the prefix into the message file
printf %s "$PREFIX$(cat "$COMMIT_MSG_FILE")" > "$COMMIT_MSG_FILE"

Arquivo /config.sh:

#!/bin/sh
git config include.path ../.gitconfig

Para testar o hook, simule uma demanda e faça um commit:

git checkout -b dev01
touch arquivo
git add arquivo
git commit -m "Primeiro arquivo"
git log -1 --pretty=%B

Deverá aparecer “[dev01] Primeiro arquivo”.

Agora o teste:

git checkout master
git branch -d dev01

Referências