12k
All articles

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.

OpenReplay Team
OpenReplay Team
Quem foi? Encontrando o culpado com git blame

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-rev e --ignore-revs-file pulam commits ruidosos conhecidos automaticamente.
  • -w ignora espaços em branco, -M acompanha linhas movidas dentro de um arquivo (limite padrão de 20 caracteres alfanuméricos) e -C acompanha linhas copiadas de outros arquivos (padrão 40), com até três flags -C ampliando a busca.
  • O Git 2.53 adicionou --diff-algorithm ao git blame, aceitando patience, minimal, histogram ou myers, sendo myers o 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.

SintomaFlag
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:

  • -C pesquisa os outros arquivos alterados por esse mesmo commit.
  • -C -C também pesquisa os arquivos tocados pelo commit que adicionou este arquivo pela primeira vez.
  • -C -C -C amplia 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.

  1. Execute git blame -L 40,60 -- src/router.js e anote o hash na linha de interesse.
  2. Verifique o commit com git show --stat <hash>. Se for uma reformatação, renomeação ou movimentação, continue.
  3. Execute git blame -n <hash>^ -L 40,60 -- src/router.js. A flag -n imprime 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 -L na próxima passagem.
  4. 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.

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — self-hosted, with full data ownership.

Star on GitHub

We use cookies to improve your experience. By using our site, you accept cookies.