Quién Cambió
Responde "quién construyó esto, bajo qué ticket y cuándo" sin salir del terminal. Todo lo necesario ya está en el historial local de Git. Lo único que añade el alojamiento del código es la narrativa del PR, y la salida proporciona un enlace a ella.
Reemplaza el bucle manual: abrir el archivo, encontrar la funcionalidad, abrir la vista blame, copiar un hash, pegarlo en el alojamiento, navegar hasta el PR, leer el ticket y el autor.
Lo que obtiene el usuario
Nombre del autor, hash del commit, número de ticket, nombre de la rama, fecha, número de PR y enlaces en los que se puede hacer clic al commit y al PR. Un veredicto principal para el rango, seguido de una tabla de cada commit que lo tocó.
Ejecutar el script
bash ~/.claude/skills/who-changed/scripts/who-changed.sh <archivo> [inicio fin] [raw]
Si la skill está instalada por proyecto, la ruta es .claude/skills/who-changed/scripts/who-changed.sh.
<archivo>solo: modo de archivo completo. Principales contribuyentes más los últimos 12 commits.<archivo> <inicio> <fin>(o un número de línea): modo de rango de líneas. Propiedad de líneas por commit.- Añadir
raw: blame sin-w -C -M, para cuando el usuario quiera la verdad a nivel de byte.
Sobreescrituras de entorno: WHO_CHANGED_BASE establece la referencia hacia la que camina la búsqueda del PR (por defecto: origin/HEAD, luego origin/main, origin/master, main, master); WHO_CHANGED_TICKET_REGEX cambia el patrón del ticket (por defecto [A-Z][A-Z0-9]+-[0-9]+).
El script imprime un informe separado por tabuladores y nunca escribe en el repositorio ni en la red. Reformatee la salida para el usuario; no pegue el TSV sin procesar.
Paso 1: resolver el destino a un rango de líneas
El script acepta números, por lo que un nombre de función debe convertirse primero en un rango. El atajo -L :nombreFunción: de Git depende de un patrón de función específico del lenguaje, y para muchos lenguajes (miembros de clase en TypeScript en particular) falla con no match. Resuelva el rango usted mismo:
- Busque la declaración, por ejemplo
^\s*(public|private|protected)?\s*(async\s+)?<nombre>\s*[(<], y<nombre>\s*[:=]\s*(function|\()para propiedades de función flecha. Para HTML o CSS, busque el selector, clave o bloque literal. - Lea desde esa línea y empareje llaves desde el primer
{para encontrar la línea de cierre. - Si el nombre coincide en más de un lugar, enumere las coincidencias y pregunte cuál. Nunca elija la primera silenciosamente; los nombres comunes suelen existir en varios archivos.
Los números de línea provienen del árbol de trabajo, por lo que coinciden con lo que el usuario ve en su editor.
Dado un rango explícito (285-300, 285, L285-L300), úselo tal cual. Dada solo una ruta de archivo, use el modo de archivo completo.
Paso 2: leer la salida
El bloque ## meta contiene base (la referencia hacia la que camina la búsqueda del PR), base_tip_date, web y host (base del enlace, derivada de git remote), el modo blame y si el archivo tiene cambios sin commit.
Las filas en ## commits están ordenadas por líneas de propiedad. El veredicto es la primera fila: el commit que posee la mayor parte del rango. Mencione al subcampeón cuando el rango esté realmente compartido; una función construida por cuatro personas a lo largo de tres años no debe informarse como perteneciente a una sola.
El script ya construye commit_url y pr_url para repositorios remotos de Bitbucket, GitHub y GitLab. Cuando son -, no hay un remoto reconocible; dígalo y omita los enlaces en lugar de inventar un anfitrión.
Preséntelo así (los nombres a continuación son inventados):
### calculateTotal - src/cart/cart.service.ts:42-61
**Alice Moreno** · `a1b2c3d` · 2023-02-08 · posee 13 de 20 líneas
Ticket SHOP-88 · rama `SHOP-88-cart-totals` · PR #41
| Líneas | Autor | Commit | Fecha | Ticket | Rama | 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 | no fusionado |
| 1 | Chen Wei | 4c5d6e7 | 2024-06-20 | — | release-2024-06 | #112 |
Luego los enlaces, y luego una línea de advertencia cada vez que se active una bandera de confianza.
Etiquetas de confianza y qué decir sobre cada una
| Etiqueta | Significado | Dígale al usuario |
|---|---|---|
ok | Rama de ticket fusionada mediante un PR | Nada necesario |
local-only | El commit aún no está en base | "Aún no fusionado a la rama base" - no hay PR |
no-pr-merge | En la línea principal, pero ningún merge de PR lo transporta | Commiteado directamente a la línea principal, o el asunto del merge rompió la convención |
squash | Número de PR leído de un sufijo (#123) | Fusionado con squash, por lo que el historial línea por línea dentro de ese PR está aplanado |
low-no-ticket | Sin ticket en el mensaje ni en la rama | Indique que no se registró ningún ticket. No adivine uno |
low-integration-branch | Fusionado mediante una rama sin ticket en su nombre | El PR es donde aterrizó la rama de integración, no donde se escribió el código. Confianza baja |
uncommitted | Not Committed Yet | Ediciones locales sin commit; excluidas del veredicto |
El código antiguo a menudo llega a través de ramas de integración de larga duración (develop, release-*, ui-dev). Para esos, la respuesta honesta es "fusionado a la línea principal mediante PR #112, pero ese PR es una rama de integración; no es el PR en el que se escribió esto".
Por qué -w -C -M es el valor por defecto
-w ignora espacios en blanco, -M sigue líneas movidas dentro del archivo, -C sigue líneas copiadas desde otro archivo. Esto no es cosmético. Tome un bloque que fue escrito en 2023 y solo re-indentado en 2026 (números ilustrativos):
- inteligente (
-w -C -M): 9 de 9 líneas van al autor original, 2023 - correcto - raw: 8 de 9 líneas van a la persona que re-indentó, 2026 - les acredita código antiguo
Entonces, el autor nombrado es la última persona en cambiar comportamiento, no la última en tocar bytes. Dígalo cuando sea relevante y ofrezca raw como seguimiento.
Reglas de honestidad
- Nunca invente un ID de ticket. Sin ticket en el mensaje del commit ni en la rama: reporte
—y diga que no se registró ningún ticket. - Nunca invente un autor. Si el blame no es concluyente, diga qué commits son candidatos.
- Distinga "fusionado mediante" de "escrito en" cada vez que se active una bandera de baja confianza.
- Si
base_tip_dateparece desactualizada, note que la referencia base no se ha obtenido recientemente y que pueden faltar fusiones recientes. Sugieragit fetch origin; no lo ejecute sin preguntar.
Límites que vale la pena mencionar cuando sea relevante
- El "por qué" no está en Git. La descripción del PR, los revisores y el título del ticket necesitan la API y credenciales del alojamiento del código o del rastreador. El enlace al PR es el punto de traspaso.
- Las fusiones squash aplanan el historial. Si un PR fue squasheado, un solo commit posee toda la funcionalidad y la atribución línea por línea dentro de él se ha perdido.
--ancestry-pathelige el primer merge que llevó el commit a la línea principal. Con ramas de características anidadas, puede ser un merge intermedio, no el PR del propio ticket.- Los contribuyentes de archivo completo no siguen renombres.
git shortlogno tiene--follow; la lista de commits sí sigue renombres, el recuento de contribuyentes no. -Cocasionalmente sorprende al atribuir código copiado y pegado al archivo del que proviene.- La detección de PR reconoce tres estilos de mensaje de merge: Bitbucket (
Merged in <rama> (pull request #N)), GitHub (Merge pull request #N from ...) y squash (... (#N)). Otros estilos caen enno-pr-merge.
Alternativa si el script no está disponible
git blame -w -C -M -L <inicio>,<fin> --date=short -- <archivo>
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 # no es ancestro -> no fusionado
git log --merges --ancestry-path --reverse --pretty=format:"%h|%s" <sha>..origin/main | grep -m1 "pull request"
git branch --contains <sha> # caso no fusionado
Analice el asunto del merge para la rama y el número de PR. Dos trampas: git shortlog necesita un HEAD explícito o lee silenciosamente la entrada estándar y no devuelve nada, y los nombres de rama pueden tener prefijo (feature/SHOP-88-...), por lo que no fije la regex del ticket al inicio de la cadena.