O que Quem Mudou faz?
Responda "quem construiu isso, sob qual ticket e quando" sem sair do terminal. Tudo o que é necessário já está no histórico git local. A única coisa que o host de código adiciona é a narrativa do PR, e a saída fornece um link para isso.
Ele substitui um loop manual: abrir o arquivo, encontrar a funcionalidade, abrir a visualização de blame, copiar um hash, colá-lo no host de código, navegar até o PR, ler o ticket e o autor.
O que o usuário recebe
Nome do autor, hash do commit, número do ticket, nome da branch, data, número do PR e links clicáveis para o commit e PR. Um veredito principal para o intervalo e, em seguida, uma tabela de cada commit que tocou no código.
Executar o script
bash ~/.claude/skills/who-changed/scripts/who-changed.sh <arquivo> [início fim] [raw]
Se o skill estiver instalado por projeto, o caminho é
.claude/skills/who-changed/scripts/who-changed.sh.
<arquivo>sozinho: modo arquivo completo. Principais contribuidores mais os últimos 12 commits.<arquivo> <início> <fim>(ou um número de linha): modo intervalo de linhas. Propriedade de linhas por commit.- Anexar
raw: blame sem-w -C -M, para quando o usuário quiser a verdade byte a byte.
Sobrescritas de ambiente: WHO_CHANGED_BASE define a referência para a qual a busca de PR caminha
(padrão: origin/HEAD, depois origin/main, origin/master, main, master);
WHO_CHANGED_TICKET_REGEX altera o padrão do ticket (padrão [A-Z][A-Z0-9]+-[0-9]+).
O script imprime um relatório separado por tabulações e nunca escreve no repositório ou na rede. Reformate-o para o usuário — não cole o TSV bruto.
Etapa 1: resolver o alvo para um intervalo de linhas
O script aceita números, então um nome de função precisa ser transformado em um intervalo primeiro. O
atalho -L :nomeDaFunção: do Git depende de um padrão de função específico da linguagem e,
para muitas linguagens (membros de classe TypeScript em particular), falha com no match.
Resolva o intervalo você mesmo:
- Pesquise (grep) pela declaração, por exemplo
^\s*(public|private|protected)?\s*(async\s+)?<nome>\s*[(<], e<nome>\s*[:=]\s*(function|\()para propriedades de arrow function. Para HTML ou CSS, pesquise pelo seletor, chave ou bloco literal. - Leia a partir dessa linha e faça a correspondência de chaves a partir do primeiro
{para encontrar a linha de fechamento. - Se o nome corresponder em mais de um lugar, liste as correspondências e pergunte qual delas. Nunca escolha silenciosamente a primeira — nomes comuns frequentemente existem em vários arquivos.
Os números de linha vêm da árvore de trabalho, então eles se alinham com o que o usuário vê em seu editor.
Dado um intervalo explícito (285-300, 285, L285-L300), use-o como está. Dado apenas um caminho de
arquivo, use o modo arquivo completo.
Etapa 2: ler a saída
O bloco ## meta carrega base (a referência para a qual a busca de PR caminha),
base_tip_date, web e host (base do link, derivada de git remote), o modo de blame
e se o arquivo tem alterações não commitadas.
As linhas em ## commits são ordenadas por linhas possuídas. O veredito é a primeira linha — o commit
que possui a maior parte do intervalo. Mencione o segundo colocado quando o intervalo for genuinamente
compartilhado; uma função construída por quatro pessoas ao longo de três anos não deve ser reportada como
pertencendo a uma.
O script já constrói commit_url e pr_url para remotos Bitbucket, GitHub e GitLab.
Quando eles são -, não há remoto reconhecível — diga isso e omita os links em vez de inventar um host.
Apresente assim (os nomes abaixo são inventados):
### calculateTotal - src/cart/cart.service.ts:42-61
**Alice Moreno** · `a1b2c3d` · 2023-02-08 · possui 13 de 20 linhas
Ticket SHOP-88 · branch `SHOP-88-cart-totals` · PR #41
| Linhas | Autor | Commit | Data | Ticket | Branch | PR |
|---|---|---|---|---|---|---|
| 13 | Alice Moreno | a1b2c3d | 2023-02-08 | SHOP-88 | SHOP-88-cart-totals | #41 |
| 6 | Ben Carter | 9f8e7d6 | 2026-03-11 | SHOP-310 | SHOP-310-rounding-fix | não mesclado |
| 1 | Chen Wei | 4c5d6e7 | 2024-06-20 | — | release-2024-06 | #112 |
Em seguida, os links e, em seguida, uma linha de ressalva sempre que uma bandeira de confiança for acionada.
Rótulos de confiança e o que dizer sobre cada um
| Rótulo | Significado | Diga ao usuário |
|---|---|---|
ok | Branch de ticket mesclada via um PR | Nada necessário |
local-only | Commit não está em base ainda | "Ainda não mesclado à branch base" — não há PR |
no-pr-merge | Na linha principal, mas nenhuma mesclagem de PR o carrega | Commitado diretamente na linha principal, ou o assunto da mesclagem quebrou a convenção |
squash | Número do PR lido de um sufixo (#123) | Mesclado via squash, então o histórico por linha dentro daquele PR é achatado |
low-no-ticket | Nenhum ticket na mensagem ou na branch | Informe que nenhum ticket foi registrado. Não adivinhe um |
low-integration-branch | Mesclado via uma branch sem ticket no nome | O PR é onde a branch de integração pousou, não onde o código foi escrito. Confiança baixa |
uncommitted | Not Committed Yet | Edições locais não commitadas; excluídas do veredito |
Código mais antigo frequentemente chega através de branches de integração de longa duração (develop,
release-*, ui-dev). Para esses, a resposta honesta é "mesclado à linha principal via
PR #112, mas aquele PR é uma branch de integração — não é o PR no qual isso foi escrito."
Por que -w -C -M é o padrão
-w ignora espaços em branco, -M segue linhas movidas dentro do arquivo, -C segue linhas
copiadas de outro arquivo. Isso não é cosmético. Pegue um bloco escrito em 2023
e apenas re-indentado em 2026 (números ilustrativos):
- inteligente (
-w -C -M): 9 de 9 linhas vão para o autor original, 2023 — correto - bruto: 8 de 9 linhas vão para a pessoa que re-indentou, 2026 — credita a pessoa com código antigo
Portanto, o autor nomeado é a última pessoa a mudar o comportamento, não a última a tocar em
bytes. Diga isso quando for importante e ofereça raw como acompanhamento.
Regras de honestidade
- Nunca invente um ID de ticket. Nenhum ticket na mensagem do commit ou na branch: relate
—e diga que nenhum ticket foi registrado. - Nunca invente um autor. Se o blame for inconclusivo, diga quais commits são candidatos.
- Distinga "mesclado via" de "escrito em" sempre que uma bandeira de baixa confiança for acionada.
- Se
base_tip_dateparecer desatualizado, observe que a referência base não foi buscada recentemente e que mesclagens recentes podem ser perdidas. Sugiragit fetch origin— não execute sem permissão.
Limites que valem a pena mencionar quando relevante
- O "porquê" não está no git. A descrição do PR, revisores e título do ticket precisam da API e credenciais do host de código ou rastreador. O link do PR é o ponto de entrega.
- Mesclagens squash achatam o histórico. Se um PR foi squashado, um commit possui toda a funcionalidade e a atribuição por linha dentro dela se perde.
--ancestry-pathescolhe a primeira mesclagem que levou o commit à linha principal. Com branches de funcionalidade aninhadas, isso pode ser uma mesclagem intermediária, não o PR do próprio ticket.- Contribuidores de arquivo completo não seguem renomeações.
git shortlognão tem--follow; a lista de commits segue renomeações, o total de contribuidores não. -Cocasionalmente surpreende ao atribuir código copiado e colado ao arquivo de onde veio.- A detecção de PR reconhece três estilos de mensagem de mesclagem: Bitbucket
(
Merged in <branch> (pull request #N)), GitHub (Merge pull request #N from ...), e squash (... (#N)). Outros estilos caem emno-pr-merge.
Fallback se o script não estiver disponível
git blame -w -C -M -L <início>,<fim> --date=short -- <arquivo>
git log -1 --date=short --pretty=format:"%H%n%an%n%ad%n%s%n%b" <sha>
git merge-base --is-ancestor <sha> origin/main # não é ancestral -> não mesclado
git log --merges --ancestry-path --reverse --pretty=format:"%h|%s" <sha>..origin/main | grep -m1 "pull request"
git branch --contains <sha> # caso não mesclado
Analise o assunto da mesclagem para a branch e número do PR. Duas armadilhas: git shortlog precisa de um
HEAD explícito ou lê silenciosamente da entrada padrão e retorna nada, e nomes de branch podem ser
prefixados (feature/SHOP-88-...), então não ancore a regex do ticket ao início da
string.