Quem foi? Encontrando o culpado com git blame
git blame explicado: leia a saída, limite linhas com -L, -w, -M, -C, ignore commits em massa e trace a mudança real por trás de uma linha.
O git blame anota cada linha de um arquivo com o commit que a alterou mais recentemente, junto com o autor e a data desse commit.
O commit que ele indica costuma ser o errado. Você investiga uma linha estranha em um código que não escreveu, e o blame te entrega um commit de “apply prettier” com 4.000 arquivos, feito dezoito meses atrás.
Esse beco sem saída é o resultado normal de um git blame puro, e superá-lo é a verdadeira habilidade. Este artigo aborda como ler a saída padrão, como restringir e reduzir o ruído com -L, -w, -M e -C, como caminhar para trás pelos commits pais até chegar à alteração que realmente importa, e duas adições recentes: --diff-algorithm (Git 2.53 ou posterior) e git last-modified (Git 2.52 ou posterior).
Principais Conclusões
- O commit que o git blame mostra para uma linha é o último que a tocou, o que frequentemente é uma reformatação, uma renomeação ou uma movimentação, e não a alteração que deu significado à linha.
- Reexecutar o blame no pai do commit reportado (
git blame <hash>^ -- file) e repetir é a forma confiável de chegar à alteração original;--ignore-reve--ignore-revs-filepulam commits ruidosos conhecidos automaticamente. -wignora espaços em branco,-Macompanha linhas movidas dentro de um arquivo (limite padrão de 20 caracteres alfanuméricos) e-Cacompanha linhas copiadas de outros arquivos (padrão 40), com até três flags-Campliando a busca.- O Git 2.53 adicionou
--diff-algorithmao git blame, aceitandopatience,minimal,histogramoumyers, sendomyerso padrão. - O Git 2.52 adicionou o experimental
git last-modified, que reporta o último commit a tocar cada caminho em um diretório em uma única travessia.
Como Ler a Saída Padrão do git blame?
Cada linha da saída padrão do git blame carrega quatro campos, nesta ordem: o hash abreviado do commit, o nome do autor, a data do autor e o número da linha, seguidos pelo conteúdo da linha. A seção sobre o formato padrão do manual lista esses campos; o Git encurta o hash para sete dígitos hexadecimais por padrão e deixa mais uma coluna livre para o acento circunflexo que sinaliza um commit de fronteira (os commits mais antigos que o blame conseguiu alcançar). As datas são impressas no formato ISO, a menos que --date ou blame.date determinem o contrário.
git blame src/router.js
a1b2c3d4 (Jane Doe 2024-03-08 14:22:31 +0100 42) return cache.get(key) ?? fetchRoute(key);
Leia da esquerda para a direita: a1b2c3d4 é o commit, Jane Doe e o timestamp são a identidade do autor desse commit, 42 é o número da linha no arquivo atual, e tudo após o parêntese de fechamento é a própria linha.
O importante a entender sobre esse hash é o que ele não é. Ele não é o commit que introduziu a lógica. É o commit mais recente cujo diff tocou a linha e, em uma base de código com formatadores, linters e refatorações, isso é frequentemente uma alteração mecânica. Trate o primeiro resultado do blame como uma pista, não como um veredito.
Como Limitar o git blame a um Intervalo de Linhas Com -L?
git blame -L 40,60 -- src/router.js restringe a anotação às linhas 40 a 60, e git blame -L :handleRoute -- src/router.js a restringe ao corpo da função cujo nome corresponde a essa expressão regular. Ambas as formas estão documentadas na opção -L, que pode ser informada mais de uma vez.
git blame -L 40,60 -- src/router.js
git blame -L :handleRoute -- src/router.js
A forma :funcname não faz parsing da sua linguagem. Ela identifica nomes de funções da mesma maneira que o git diff descobre o que imprimir em um cabeçalho de hunk, e você pode ajustar isso por tipo de arquivo através do atributo diff no gitattributes. Ambos os extremos do intervalo também aceitam padrões /regex/, e o ponto final aceita deslocamentos +N, então -L '/^function handleRoute/,+15' também é válido.
Como Ignorar Espaços em Branco e Código Movido no git blame?
Passar -w faz o git blame ignorar espaços em branco ao comparar versões, de modo que uma reformatação apenas de indentação não reivindique mais as linhas que tocou. -M identifica linhas que se deslocaram dentro de um mesmo arquivo, e -C amplia a busca para linhas que vieram de outros arquivos alterados pelo mesmo commit; o manual informa seus limites padrão de correspondência como 20 e 40 caracteres alfanuméricos, respectivamente.
| Sintoma | Flag |
|---|---|
| Linha atribuída a uma reindentação ou limpeza de espaços finais | -w |
| Linha atribuída ao commit que reordenou o código dentro do arquivo | -M |
| Linha que chegou por cópia ou movimentação de outro arquivo | -C (empilhável) |
| Linha atribuída a um commit em massa conhecido | --ignore-rev <hash> |
git blame -w -- src/router.js
git blame -M -- src/router.js
git blame -C -C -C -- src/router.js
Cada -C adicional amplia os arquivos nos quais o git blame procura as linhas copiadas:
-Cpesquisa os outros arquivos alterados por esse mesmo commit.-C -Ctambém pesquisa os arquivos tocados pelo commit que adicionou este arquivo pela primeira vez.-C -C -Camplia mais uma vez, para arquivos em qualquer commit.
Se várias flags -C trouxerem um limite numérico, a última prevalece. Uma renomeação de arquivo inteiro não precisa de flag alguma: o blame continua rastreando as linhas através dela por conta própria, e o Git atualmente não oferece nenhuma maneira de desativar esse comportamento.
Como Encontrar o Commit Anterior a uma Reformatação em Massa?
Para ultrapassar um commit mecânico, reexecute o blame no pai desse commit, git blame <hash>^ -- src/router.js, e repita até que o commit exibido seja um que realmente alterou o comportamento da linha. O sufixo ^ é a sintaxe padrão de gitrevisions para o primeiro pai, então o blame começa a partir do estado do arquivo imediatamente antes do commit ruidoso entrar.
- Execute
git blame -L 40,60 -- src/router.jse anote o hash na linha de interesse. - Verifique o commit com
git show --stat <hash>. Se for uma reformatação, renomeação ou movimentação, continue. - Execute
git blame -n <hash>^ -L 40,60 -- src/router.js. A flag-nimprime o número de cada linha no commit original, o que importa porque os números de linha se deslocam entre revisões e você pode precisar reajustar o-Lna próxima passagem. - Repita a partir do passo 2 até que o commit exibido altere o que a linha faz.
git blame -n a1b2c3d4^ -L 40,60 -- src/router.js
Quando um repositório tem commits ruidosos conhecidos, pule a caminhada manual. --ignore-rev <hash> diz ao git blame para atribuir as linhas além de um commit especificado, e --ignore-revs-file faz o mesmo para um arquivo inteiro de hashes, escritos por extenso, um por linha. Defina blame.markIgnoredLines para sinalizar linhas reatribuídas com ? e blame.markUnblamableLines para sinalizar linhas que não puderam ser reatribuídas com *.
git blame --ignore-rev a1b2c3d4 -- src/router.js
git blame --ignore-revs-file .git-blame-ignore-revs -- src/router.js
git config blame.markIgnoredLines true
Fazer commit dessa lista como .git-blame-ignore-revs e apontar blame.ignoreRevsFile para ela é abordado em 5 Git Dotfiles Every Developer Should Know.
Experimentando um Algoritmo de Diff Diferente (Git 2.53 ou Posterior)
O Git 2.53 adicionou --diff-algorithm ao git blame, aceitando patience, minimal, histogram ou myers (com default como alias para myers), sendo myers o padrão. A adição aparece nas notas de lançamento do Git 2.53, e os valores aceitos estão listados na opção —diff-algorithm do manual.
O blame decide quais linhas do pai correspondem a quais linhas do filho fazendo o diff das duas versões, e algoritmos diferentes pareiam linhas de forma diferente. Quando um commit intercala linhas alteradas e inalteradas, como reformatações costumam fazer, um algoritmo pode creditar uma linha à reformatação enquanto outro a credita ao commit que originalmente a escreveu.
git blame -L 40,60 -- src/router.js
git blame -L 40,60 --diff-algorithm=patience -- src/router.js
Nenhum algoritmo está documentado como mais correto que outro. Se a atribuição padrão parecer implausível, executar o mesmo comando com patience ou histogram custa uma invocação extra e te dá uma segunda opinião para comparar.
Consultando um Diretório Com git last-modified
O Git 2.52 adicionou o git last-modified, que reporta o commit que alterou pela última vez cada caminho em um diretório em uma única travessia do histórico, em vez de um git log -1 por arquivo; o comando é marcado como experimental e seu comportamento pode mudar. A página de manual do git-last-modified declara o status experimental em sua linha NAME e mostra o formato da saída como <oid> TAB <path>, uma linha por caminho, com um object ID completo e sem autor, data ou assunto.
git last-modified -r -- src/
Sem -r (ou um --max-depth diferente de zero), você obtém apenas as entradas que correspondem ao próprio pathspec, sem descer pelos subdiretórios abaixo delas. Renomeações e alterações de modo contam como modificações. O laço por arquivo que ele substitui percorre os mesmos commits novamente para cada arquivo; o last-modified os percorre uma única vez. Ele responde “o que mudou recentemente neste módulo”, uma pergunta diferente de “por que esta linha existe”, e vale a pena recorrer a ele antes de começar a fazer blame de arquivos individuais.
Blame É uma Pergunta, Não um Veredito
A saída do git blame nomeia a última pessoa a tocar em uma linha, e esse nome quase nunca é a resposta que você precisa. Execute o blame com -L para focar, -w e -M/-C para eliminar o ruído mecânico, e então retroceda pelos pais (ou mantenha um arquivo de ignore) até que o commit exibido traga uma mensagem que explique a linha. Uma vez que você tenha esse commit, git show <hash> te dá o diff e o raciocínio, que é o objetivo do exercício: entender por que o código está ali, para que você possa alterá-lo sem repetir o incidente que o colocou ali em primeiro lugar.
Perguntas Frequentes
Como descubro quem deletou uma linha, já que o git blame só mostra linhas que ainda existem?
O git blame não diz nada sobre linhas que foram removidas ou sobrescritas, como aponta seu manual. Use a picareta (pickaxe) em vez disso: git log -S'algum texto' -- src/router.js lista todos os commits que adicionaram ou removeram essa string, e adicionar -p mostra a própria remoção. Alternativamente, git blame --reverse a1b2c3d..HEAD -- src/router.js percorre o histórico para frente a partir daquele commit e nomeia a revisão mais recente na qual cada linha ainda existia.
Por que o git blame mostra 00000000 e 'Not Committed Yet' em algumas linhas?
Essas linhas contêm alterações não commitadas. Sem um argumento de revisão, o git blame anota a cópia do arquivo na árvore de trabalho, então qualquer linha que difira do HEAD recebe um hash todo zerado e 'Not Committed Yet' no lugar do nome do autor. Faça o commit ou o stash da alteração, ou execute git blame HEAD -- src/router.js para anotar a versão commitada e ignorar completamente as edições locais.
A visualização de blame do GitHub respeita um arquivo .git-blame-ignore-revs?
Sim. O GitHub aplica automaticamente um arquivo chamado .git-blame-ignore-revs na raiz do repositório à sua visualização de blame, usando o mesmo mecanismo --ignore-revs-file da linha de comando, e exibe um banner 'Ignoring revisions' quando o faz. Linhas que não podem ser reatribuídas a um commit anterior ainda mostram o commit ignorado. O arquivo não configura o git local; cada desenvolvedor ainda precisa executar git config blame.ignoreRevsFile .git-blame-ignore-revs.
Qual é a diferença entre git blame e git log -L?
O git blame reporta um commit por linha: o commit mais recente que a tocou em uma única versão do arquivo. Já git log -L 40,60:src/router.js rastreia as linhas 40 a 60 ao longo do histórico e imprime todos os commits que as alteraram, cada um com o diff daquele intervalo, do mais recente ao mais antigo. Use o blame para identificar um suspeito rapidamente e o log -L para acompanhar a evolução das linhas. Ambos aceitam a forma :funcname, como em git log -L :handleRoute:src/router.js.