¿Cómo funciona git log –follow <filename> work?

Estoy tratando de elegir una identificación para el historial de un file; me gustaría que fuera o hacer reference al "object" cuyos detalles git log --follow <filename> . Me pregunto:

¿Cómo sabe Git que un file es una variante de otro en los siguientes commits? El nombre es el mismo, es una pista fuerte, por supuesto, pero también rastrea el cambio de nombre en la confirmación. ¿Mantiene los resultados de sus cálculos en alguna parte para que git log se refiera a (¿dónde?), O git log repite estos cálculos cada vez? (¿Y qué cálculos son estos?)

Idealmente, me gustaría acceder o volver a crear el historial (list de commits / blob shas) con nodegit.

Tanto otras personas como yo hemos descrito esto en diferentes (y ningún enlace) detalle en otro lugar, por ejemplo, esta respuesta a ¿Qué es la heurística de git para asignar modificaciones de contenido a routes de files? o mi respuesta a Git Diff de los mismos files en dos directorys siempre da como resultado "renombrado" . Los detalles son ligeramente diferentes para git log --follow que para git diff , ya que git diff generalmente trata con un tree completo – lleno de files laterales izquierdo y derecho, pero el git log --follow solo funciona con una ruta particular. 1

En cualquier caso, el siguiente cambio de nombre ocurre cuando se comparan dos commits específicos. Para un git diff general, son dos commits R (lado derecho) y L (lado izquierdo -usted elige los dos), 2 pero para git log son específicamente padre e hijo. Llamemos a estos P y C por conveniencia. Con git log --follow , Git ejecuta un paso after-diff (llamado desde diff_tree_sha1 ; ver notas al pie) que recorta todo en un solo file. La diferencia se hace con R = C y L = P. El caso general es realmente más fácil de describir, así que comenzaremos con eso.

Normalmente, al comparar R vs L , Git:

  • empareja todos los files de tree que tienen el mismo nombre completo de ruta, luego
  • coloca los files restantes (routes) en una queue de vinculación.

Puede modificar esto un poco con el indicador -B (par- b reaking), que en realidad toma dos integers opcionales ( -B n / m ). Solo el n integer es importante para la detección de cambio de nombre. 3 También puede modificarlo con el indicador -C ; esto solo toma una n opcional, y activa la detección de copy . En todos los casos, la detección de cambio de nombre debe estar activada. La detección de cambio de nombre se habilita a través de -M , que también toma un integer opcional n , o automáticamente en el caso de git log --follow y otros commands como el git status o el git diff --stat post-merge git diff --stat .

En cualquier caso, el integer n aquí es una similitud (o desemejanza) valor métrico para todas estas varias opciones. Aquí es donde llegamos a la carne del código de detección de cambio de nombre.

Supongamos que tenemos, para empezar, una git diff <commit1> <commit2> básica de git diff <commit1> <commit2> o git diff <tree1> <tree2> . Esto termina llamando a builtin_diff_tree en builtin_diff_tree builtin/diff.c , que llama a diff_tree_sha1 (que veremos más adelante) y luego log_tree_diff_flush en log-tree.c . Esto casi inmediatamente llama a diffcore_std en diff.c , que ejecuta las diffcore_break , diffcore_rename y diffcore_merge_broken si se seleccionan las opciones correctas ( -B , -M o -C , y -B otra vez).

Estas tres funciones operan en una queue de synchronization . ¿Cómo se configura la queue de vinculación? Lo dejo en otra sección ya que es complicado. Por ahora, supongamos que la queue de vinculación ya tiene path/to/file emparejado con path/to/file cuando hay una path/to/file en L y R , y de lo contrario tiene una path/to/L-only y no emparejado path/to/R-only para casos donde hay una ruta de file que ocurre solo en L o solo en R.

La function diffcore_break está en diffcore-break.c . Su trabajo es encontrar files ya emparejados cuyo índice de similitud (al comparar las versiones L y R ) esté por encima de algún umbral. Si ese es el caso, rompe el emparejamiento. La function diffcore_merge está justo debajo de ella en el mismo file; vuelve a join a un par roto si ninguno de los dos ha encontrado un "mejor compañero". El cálculo del índice de disimilaridad es similar, pero no es lo mismo que, el cálculo de similitud. 4

Detección de similitud

La function diffcore_rename más interesante está en diffcore-rename.c . Tiene un atajo de caso especial para – --follow que podemos ignorar por ahora. A continuación, busca los nombres exactos , es decir, los files cuyos hashes de blobs coinciden, aunque sus nombres no coincidan. Hay algunas partes difíciles para usar "el siguiente file" si múltiples fonts L tienen el mismo hash que un destino R no emparejado.

Luego, verifica cuántas inputs no pareadas hay , porque va a (en efecto) hacer num ( L ) por num ( R ) comparaciones de files para calcular sus similitudes, y esto tomará mucho time y espacio . Incluso networkingucirá automáticamente la --find-copies-harder un caso " --find-copies-harder " que es "demasiado difícil". Luego, para cada emparejamiento posible de L y R , calcula un índice de similitud y un puntaje de nombre .

El código de índice de similitud está en la similitud de estimate_similarity en diffcore-rename.c . Se basa en la function diffcore_count_changes en diffcore-delta.c , que dice esto (lo estoy copyndo directamente del file porque es una de las métricas principales):

  * Idea here is very simple. * * Almost all data we are interested in are text, but sometimes we have * to deal with binary data. So we cut them into chunks delimited by * LF byte, or 64-byte sequence, whichever comes first, and hash them. * * For those chunks, if the source buffer has more instances of it * than the destination buffer, that means the difference are the * number of bytes not copied from source to destination. If the * counts are the same, everything was copied from source to * destination. If the destination has more, everything was copied, * and destination added more. * * We are doing an approximation so we do not really have to waste * memory by actually storing the sequence. We just hash them into * somewhere around 2^16 hashbuckets and count the occurrences. 

Sin embargo, aquí hay un bit secreto: el índice de similitud ignora los caracteres si el file se considera "no binary" y el \r es seguido inmediatamente por \n .

El puntaje final del índice de similitud es:

 score = (int)(src_copied * MAX_SCORE / max_size); 

donde src_copied es la cantidad de trozos troceados (de 64 bytes o up-to-newline) que ocurrieron en la fuente y luego se max_size en el destino, y max_size es el tamaño, en bytes, de la mancha que sea más grande. (Este recuento de bytes no tiene en count los caracteres eliminados '\r' . Estos simplemente se eliminan de los trozos de 64 o de nueva línea que se procesan en hash).

El "puntaje de nombre" es realmente solo 1 (mismo nombre base) o 0 (nombre base diferente), es decir, 1 si el file L es dir/oldbase y el file R es differentdir/oldbase , pero 0 si el file L es dir/oldbase y el file R es anything/newbase . Esto se usa para hacer que Git favorezca a newdir/oldbase sobre anything/newbase cuando esos dos files son igualmente similares.

Creando la queue de vinculación

El código diff_tree_sha1 llama (a través de una serie de funciones) ll_diff_tree_paths (ambos están en tree-diff.c ; me tree-diff.c solo a la function final aquí). Este es un código de código complicado y extremadamente optimizado (Git pasa mucho time aquí) por lo que solo haremos un resumen rápido e ignoraremos las complicaciones (ver nota 2). Este código se ve en parte en los nombres de ruta completos de cada blob en cada tree (estos son los elementos P1, …, Pn en el comentario en la parte superior), y en parte en los valores hash de blob para cada uno de estos nombres. Para los files que tienen el mismo nombre y el mismo contenido, no hace nada (excepto en el modo --find-copies-harder , en cuyo caso --find-copies-harder en queue todos los nombres de file). Para files que tienen el mismo nombre y diferentes contenidos, o ningún nombre L o R , llama (a través de pointers de function, almacenados en opt->pathchange , opt->change , y opt->add_remove ) lo que eventualmente se networkinguce a diff_change o diff_addremove , ambos en diff.c Estos llaman a diff_queue , que coloca el par de files (uno de los cuales es ficticio si el file es nuevo o eliminado) en la queue de vinculación.

Por lo tanto, la versión corta (si no usamos -C o --find-copies-harder ), la queue de emparejamiento tiene files no apareados solo cuando no hay un file fuente original en L correspondiente a un file en R , o no tiene un destino file en R correspondiente a un file fuente en L. Con -C , tiene todos los files fuente, o todos los files fuente modificados, también listdos para que puedan escanearse en busca de copys (la elección aquí se basa en si usó --find-copies-harder ).

Reducir esto a un file, para – --follow

Ya diffcore-rename.c un atajo en el código diffcore-rename.c : omite todos los nombres de files R que no son el nombre de file que nos importa. Parece que hay algunos hacks similares en ll_diff_tree_paths , aunque no estoy seguro de si se aplican aquí. El código también se maneja de una manera diferente, como se señala en la nota 2 al pie. Cuando diferenciamos al padre P del niño C y buscamos un nuevo nombre en nuestra queue de emparejamiento, cambiamos el nombre del file que estamos usando como un restricción en nuestro git log -- <path> : reemplazamos el nuevo nombre en C con la ruta de la fuente de cambio de nombre en P. Luego continuamos difuminando como siempre, así que la próxima vez que comparemos un par de P y C , searchemos oldpath lugar de newpath . Si detectamos que oldpath es renombrado de reallyoldpath , volvemos a poner ese nombre en su lugar nuevamente, como antes.

Tenga en count que toda la maquinaria -B , -C y -M aplica en teoría , pero los accesos directos pueden (no es del todo claro para mí si lo hacen) que algunos de ellos no funcionen.


1 Cuando se utiliza --follow , Git usa el código general de diffcore para ejecutar tanto el rompimiento de pares como la detección de copys. El código general se llama desde el código que quiere hacer la simplificación. Vea la function try_to_follow_renames en tree-diff.c , que llama a diffcore_std en diff.c Esto finalmente llama a diff_resolve_rename_copy que maneja las queues de synchronization. Entonces try_to_follow_renames recorta el resultado hasta el único file interesante; esto se testing más tarde a través de diff_might_be_rename como se llama desde diff_tree_sha1 . Creo que todo esto proviene de log_tree_commit , llamado desde cmd_log_walk o log_show_early . Esto último parece ser un truco no documentado destinado a ser utilizado por algunas GUI (s).

2 La coincidencia de tree en git diff realidad acepta una única confirmación en el lado derecho de salida, y una list de confirmaciones en el lado izquierdo de la input, para propósitos de diferencias combinadas. Así es como Git logra mostrar los commit de fusión. Sin --follow no está claro cómo funciona. Sin embargo, --follow trabajando con commits de fusión. Consulte find_paths_generic en combine-diff.c , que también llama a diff_tree_sha1 . Tenga en count que el log --follow hack sucede como resultado de llamar a diff_tree_sha1 , y este código de event handling combinación combinado-diff llama a esa function una vez por padre. Sin embargo, si se cambiará el nombre seguido, se habrá cambiado cuando pase por el segundo padre. Quizás esto es un error. ¿Qué sucede si ese segundo padre decide que el nuevo nombre da como resultado otro cambio de nombre diferente? Lógicamente, debería elegir hasta un nuevo nombre por cada fork parental, trabajando en order topológico, y considerar resolverlos de nuevo de alguna manera, siempre y cuando las bifurcaciones se reincorporen.

3 El segundo, m , valor en -B n / m le dice a Git cuándo no ejecutar un diff real, y en su lugar solo describe el cambio en un file no patentado como "eliminar todas las líneas originales, replacelas por todas las líneas nuevas" ". Esto supone que el primer valor -B terminó no rompiendo el emparejamiento, o el emparejamiento se re-pegó-juntos debido al valor -M , o se pegó a una fuente diferente como una copy -C .

4 Ver should_break para más detalles. Esto también usa el código diffcore-delta.c , pero de una manera diferente, usando el recuento "agregado".

git log repite los cálculos todo el time.

La decisión se basa en el contenido del file. Cuando un file desaparece y aparece otro file, Git compara los contenidos y decide que el file se renombró si los contenidos son iguales o muy similares (en cierta medida).