¿Cómo realizar un cambio de nombre sin ediciones posteriores en git?

Tengo un file que he cambiado de nombre y luego he editado. Me gustaría decirle a Git que organice el cambio de nombre, pero no las modificaciones de contenido. Es decir, deseo organizar la eliminación del antiguo nombre del file y la adición del contenido del file antiguo con el nuevo nombre del file.

Entonces tengo esto:

Changes not staged for commit: deleted: old-name.txt Untracked files: new-name.txt 

pero quieres esto:

 Changes to be committed: new file: new-name.txt deleted: old-name.txt Changes not staged for commit: modified: new-name.txt 

o esto:

 Changes to be committed: renamed: old-name.txt -> new-name.txt Changes not staged for commit: modified: new-name.txt 

(donde la medida de similitud debe ser del 100%).

No puedo pensar en una forma directa de hacer esto.

¿Hay syntax para get los contenidos de una revisión específica de un file específico, y agregar esto al área de transición git bajo una ruta específica?

La parte eliminada, con git rm , está bien:

 $ git rm old-name.txt 

Es la parte añadida del cambio de nombre con el que estoy luchando. (Podría save los nuevos contenidos, get una copy nueva (para los contenidos anteriores), mv en el shell, git add , y luego recuperar los nuevos contenidos, ¡pero eso parece una muy larga distancia!)

¡Gracias!

Git realmente no cambia de nombre. Todos se calculan de una manera "posterior al hecho": git compara una confirmación con otra y, en el momento de la comparación , decide si hubo un cambio de nombre. Esto significa que si git considera que algo "un cambio de nombre" cambia dinámicamente. Sé que estás preguntando acerca de un compromiso que aún no has hecho, pero tengan paciencia conmigo, esto realmente se relaciona (pero la respuesta será larga).


Cuando pides git (a través de git show o git log -p o git diff HEAD^ HEAD ) "lo que sucedió en el último commit", ejecuta un diff del commit anterior ( HEAD^ o HEAD~1 o el SHA raw real) 1 para la confirmación previa, cualquiera de estas hará para identificarla) y la confirmación actual ( HEAD ). Al hacer esa diferencia, puede descubrir que solía haber un old.txt y ya no existe; y no había new.txt pero ahora hay.

Estos nombres de file (files que solían estar allí pero no lo están, y los files que están allí ahora que no) se colocan en una stack marcada como "candidatos para cambiar el nombre". Luego, para cada nombre en la stack, git compara "contenido antiguo" y "contenido nuevo". La comparación de la coincidencia exacta es súper fácil debido a la forma en que git networkinguce el contenido a SHA-1; si la coincidencia exacta falla, git cambia a una diferencia opcional "son los contenidos al less similares" para verificar el cambio de nombre. Con git diff este paso opcional está controlado por el indicador -M . Con otros commands, es configurado por los valores de git config o codificado en el command.

Ahora, volvamos al área de preparación y al git status : lo que almacena git en el área de indexing / escenificación es básicamente "un prototipo para la siguiente confirmación". Cuando se git add algo, git almacena el contenido del file justo en ese punto, calculando el SHA-1 en el process y luego almacenando el SHA-1 en el índice. Cuando git rm algo, git almacena una nota en el índice que dice "este nombre de ruta se elimina deliberadamente en el siguiente compromiso".

El command git status , entonces, simplemente hace una diferencia, o realmente, dos diffs: HEAD vs index, para lo que se va a comprometer; e index vs work-tree, por lo que podría (pero aún no) va a ser cometido.

En esa primera diferencia, git usa el mismo mecanismo que siempre para detectar los cambios de nombre. Si hay una ruta en la confirmación HEAD que se ha ido en el índice, y una ruta en el índice que es nueva y no en la confirmación HEAD , es un candidato para la detección de cambio de nombre. El command de git status conecta la detección de nombre a "on" (y el límite de conteo de files a 200, con solo un candidato para la detección de cambio de nombre este límite es suficiente).


¿Qué significa todo esto para tu caso? Bueno, cambiaste el nombre de un file (sin usar git mv , pero realmente no importa porque el git status encuentra el cambio de nombre, o no lo encuentra, en el git status ), y ahora tienes una nueva versión del file nuevo .

Si git add la nueva versión, esa versión más reciente entra en el repository, y su SHA-1 está en el índice, y cuando el git status hace una diferencia, comparará el nuevo y el antiguo. Si son al less "50% similares" (el valor codificado para el git status ), git le dirá que el file se renombró.

Por supuesto, git add -los contenidos modificados no es exactamente lo que pediste: querías hacer una confirmación intermedia donde el file solo se renombra, es decir, una confirmación con un tree con el nuevo nombre, pero con los contenidos anteriores.

No tiene que hacer esto, debido a la detección de cambio de nombre dinámica anterior. Si quieres hacerlo (por el motivo que sea) … bueno, git no lo hace tan fácil.

La manera más sencilla es tal como sugiere: mueva los contenidos modificados a otro lugar, use git checkout -- old-name.txt , luego git mv old-name.txt new-name.txt , luego commit. El git mv cambiará el nombre del file en el área index / staging y cambiará el nombre de la versión del tree de trabajo.

Si git mv tenía una opción de --cached como lo hace git rm , podrías simplemente git mv --cached old-name.txt new-name.txt y luego git commit . El primer paso sería cambiar el nombre del file en el índice, sin tocar el tree de trabajo. Pero no es así: insiste en sobrescribir la versión del tree de trabajo e insiste en que el nombre anterior debe existir en el tree de trabajo para comenzar.

El método de un solo paso para hacer esto sin tocar el tree de trabajo es usar git update-index --index-info , pero eso también es un poco desorderado (lo mostraré en un momento). Afortunadamente, hay una última cosa que podemos hacer. Configuré la misma situación que tenía, cambiando el nombre al nuevo y modificando el file:

 $ git status On branch master Changes not staged for commit: (use "git add/rm <file>..." to update what will be committed) (use "git checkout -- <file>..." to discard changes in working directory) deleted: old-name.txt Untracked files: (use "git add <file>..." to include in what will be committed) new-name.txt 

Lo que hacemos ahora es, primero, poner manualmente el file de nuevo bajo su antiguo nombre, luego usar git mv para volver a cambiar al nuevo nombre :

 $ mv new-name.txt old-name.txt $ git mv old-name.txt new-name.txt 

Esta vez, git mv actualiza el nombre en el índice, pero mantiene los contenidos originales como el índice SHA-1, pero mueve la versión del tree de trabajo (contenido nuevo) a su lugar en el tree de trabajo:

 $ git status On branch master Changes to be committed: (use "git reset HEAD <file>..." to unstage) renamed: old-name.txt -> new-name.txt Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git checkout -- <file>..." to discard changes in working directory) modified: new-name.txt 

Ahora simplemente git commit a hacer una confirmación con el cambio de nombre en su lugar, pero no los nuevos contenidos.

(¡Tenga en count que esto depende de que no haya un nuevo file con el nombre anterior!)


¿Qué pasa con el uso de git update-index ? Bueno, primero hagamos que las cosas vuelvan al estado "cambiado en tree de trabajo, índice coincide con HEAD commit":

 $ git reset --mixed HEAD # set index=HEAD, leave work-tree alone 

Ahora veamos qué hay en el índice para old-name.txt :

 $ git ls-files --stage -- old-name.txt 100644 2b27f2df63a3419da26984b5f7bafa29bdf5b3e3 0 old-name.txt 

Entonces, lo que necesitamos git update-index --index-info es borrar la input para old-name.txt pero hacer una input por lo demás idéntica para new-name.txt :

 $ (git ls-files --stage -- old-name.txt; git ls-files --stage -- old-name.txt) | sed -e \ '1s/^[0-9]* [0-9a-f]*/000000 0000000000000000000000000000000000000000/' \ -e '2s/old-name.txt$/new-name.txt/' | git update-index --index-info 

(nota: Rompí lo anterior con fines de publicación, era una sola línea cuando lo escribí, en sh / bash, debería funcionar así, dadas las barras invertidas que agregué para continuar con el command "sed") .

Hay otras forms de hacerlo, pero simplemente extrayendo la input del índice dos veces y modificando la primera en una eliminación y la segunda con el nuevo nombre parecía la más fácil aquí, de ahí el command sed . La primera sustitución cambia el modo de file (100644 pero cualquier modo se convertiría en ceros a la vez) y SHA-1 (coincide con cualquier SHA-1, reemplaza con todos los ceros especiales SHA-1 de git), y el segundo deja el modo y SHA-1 solo mientras se reemplaza el nombre.

Cuando finaliza el índice de actualización, el índice ha registrado la eliminación de la ruta anterior y la adición de la nueva ruta (con el mismo modo y SHA-1 que en la ruta anterior).

Tenga en count que esto podría fallar si el índice tuviera inputs old-name.txt para old-name.txt ya que podría haber otras etapas (1 a 3) para el file.

@torek dio una respuesta muy clara y completa. Hay muchos detalles muy útiles allí; vale la pena una lectura adecuada.

Pero, por el bien de los que están apurados, el quid de la solución muy simple que se dio fue este:

Lo que hacemos ahora es, primero, poner manualmente el file de nuevo bajo su antiguo nombre, luego usar git mv para volver a cambiar al nuevo nombre:

 $ mv new-name.txt old-name.txt $ git mv old-name.txt new-name.txt 

(Era solo la parte trasera que me faltaba, para hacer posible el git mv ).

Por favor, vote la respuesta de torek si lo encuentra útil.