¿Qué sucede cuando empujo mientras alguien tira?

Sucede que mi compañero de git, con quien comparto un repository privado, tuvo que tirar mucho porque no lo hizo por un time. Quiero seguir trabajando durante su ~ 3h session de descarga, pero también quiero evitar problemas.

Por lo tanto, me gustaría saber:

  1. ¿Es seguro empujar mientras tira?
  2. ¿Es seguro modificar mi último compromiso mientras él tira?

TL; DR: sí a ambas preguntas, con la advertencia de que, en general, es una mala práctica modificar las confirmaciones que ya se han publicado.

  1. ¿Es seguro empujar mientras tira?

Para comenzar, 'pull' es dos operaciones: 'search' y luego 'fusionar'. En primer lugar, git 'obtendrá' todas las confirmaciones en el repository remoto (antepasados ​​del HEAD especificado) y las almacenará en su repository local. En segundo lugar, 'fusionará' el HEAD remoto especificado en el HEAD local (esto puede ser un avance rápido o un nuevo commit con múltiples padres).

La parte 'fusión' de esto es una operación local, y no es realmente relevante. Su compañero se 'fusionará' en function de lo que sea 'extraído' del repository.

Entonces, las preguntas se networkingucen a: ¿es seguro "search" mientras otra persona está haciendo un "empujón"? Y la respuesta es – sí, por supuesto! Al final de la 'búsqueda', su compañero tendrá todas sus confirmaciones o no. Si no lo hacen, las confirmaciones sobrantes se descubrirán a continuación 'push' o 'pull', según el flujo de trabajo habitual de git. Es lógicamente equivalente a que completen el 'pull', y luego 'push'ing commits nuevos.

  1. ¿Es seguro modificar mi último compromiso mientras él tira?

Eh, como una cuestión de process, es probable que no desee modificar commits que ya ha publicado en un repository remoto (google it). Pero si realmente quieres hacer eso, sí, puedes. Y su pareja, de nuevo, obtendrá esos cambios (los que publicó durante su 'recuperación'), o no lo hará. Si no, no importa, ya que esos cambios se descubrirán más adelante.

Por lo que vale, aquí hay una descripción más detallada, técnica-ish (pero aún de alto nivel) de push frente a fetch / pull , incorporando el caso de una fetch muy lenta (varias horas).

Antes de leer el rest de esta respuesta, es posible que desee ejecutar git ls-remote origin . Esta es una operación de solo lectura, por lo que es seguro hacerlo en cualquier momento. En un server ocupado, el resultado aquí puede ser bastante grande, por lo que es posible que desee verlo en un editor, pero verá una larga list de SHA-1 y pares de nombres. Cada SHA-1 identifica un object git específico en su repository (del remoto), y cada nombre es un nombre para ese object.

El opuesto más cercano al push es fetch , no pull

El command git pull es simplemente 1 un command de conveniencia que ejecuta git fetch seguido de git merge (o, si se indica, git rebase ). Entonces, realmente deberías comparar el push para fetch a fetch .

Restring que fetch y push implican dos gits

En general, 2 obtendrá o presionará desde / hacia un "control remoto", en otro host por completo. El otro host ejecuta otra instancia de git, que se ve en otro repository. En general, los dos repositorys están relacionados de alguna manera, por ejemplo, ambos fueron clonados originalmente desde una tercera location o uno fue clonado de la otra. Los detalles de la relación importan less que existe alguna relación, de modo que los dos repositorys tienen algo en común. (Esto no es realmente necesario, es solo el caso típico).

La llamada pasa por el teléfono de Internet o por un canal de comunicación similar. Por lo tanto, podemos hablar de "su git" diciendo algo a "su git", y "su git" diciendo algo a "su idiota". Las dos instancias de git convergen entre sí para descubrir qué hacer.

Refspecs son key

Tanto fetch como push utilizan un concepto key llamado "refspec". En esencia, un refspec es esencialmente un par de nombres de reference, como un par de nombres de twig o un par de nombres de label. El par está separado por dos puntos, por ejemplo, master:master . Un nombre "totalmente calificado", como refs/heads/master o refs/tags/v1.2 , evita la ambigüedad aquí, pero los nombres cortos generalmente funcionan bien (y son lo que la mayoría de la gente usa cuando especifica algo explícitamente) ya que git automáticamente se da count si el nombre es una twig o una label. 3

Puede usar más de un refspec a la vez, y refspecs permite múltiples coincidencias con una * syntax como se muestra aquí.

El refspec por defecto para una fetch desde un origin nombre remoto es:

 +refs/heads/*:refs/remotes/origin/* 

Ignoraremos el signo más aquí. 4 Este refspec en particular dice: "tomar todas las twigs" (en el refs/heads/ nombre-espacio) "y copyrlas de su repository a mi repository, pero en mi repository, cambie el nombre a refs/remotes/origin/ ".

Así es como funcionan las "twigs remotas" AKA "twigs de rastreo remoto", y cómo surgen en primer lugar: tu git copy las twigs de git, pero las coloca en un espacio de nombre diferente, para que (tus copys de) sus twigs no afectará tus twigs.

(Cuando usas git pull algo de esto está oculto para ti. Este "ocultamiento" es bastante imperfecto, así que debes saberlo. En gits modernos-1.8.4 y posteriores, las twigs remotas se actualizan en todas las operaciones de búsqueda, incluyendo Las versiones anteriores de git no actualizan las twigs remotas en este tipo de operaciones de búsqueda, pero todavía las actualizan en busca de "regulares" y en todas las operaciones de inserción, lo cual es un poco raro, y es por eso que los gits más nuevos solo siempre actualiza.)

Refspecs para push son complicados

El refspec pnetworkingeterminado habitual para fetch es fácil de describir: "consígueme todas sus twigs, pero haz que sean rastreas remotas en mi repository". Esto es limpio, simple y efectivo, y ha sido el pnetworkingeterminado desde que se deviseon los "controles remotos".

El valor pnetworkingeterminado para push es complicado y configurable, y el valor pnetworkingeterminado ha cambiado en git 2.0.

El antiguo impulso pnetworkingeterminado se denomina matching y el nuevo valor pnetworkingeterminado se llama simple (aunque sigue siendo un poco complicado). La nueva regla simple es:

  • encontrar la twig "ascendente", y
  • presione a la misma twig en el control remoto, pero solo si tiene el mismo nombre .

Es decir, supongamos que está en el master sucursal, y el "ascendente" para eso es " master sucursal en el origin remoto". Entonces git push significa git push refs/heads/master:refs/heads/master .

Para el valor pnetworkingeterminado anterior ( matching ), su git y su git hablan un poco para ver qué twigs tiene y qué twigs tienen. Entonces todas las twigs que ambos tienen, que tienen el mismo nombre, se ponen en su refspec. Entonces, si tienes una twig llamada betty y tienen una twig llamada betty , eso agrega refs/heads/betty:refs/heads/betty a tus push refspecs. Luego, si ambos tienen twigs llamadas fnetworking , se agregarán, y así sucesivamente. Tu git intenta empujar todas las twigs correspondientes a su git.

Vale la pena mencionar dos cosas más aquí:

  • Al recuperar, "sus" nombres de reference van a la izquierda del refspec, y los suyos van a la derecha. Su master convierte en su origin/master porque el refspec tiene refs refs/heads/master a la izquierda y refs/remotes/origin/master a la derecha. Pero en el impulso estos se invierten: su nombre de twig o label va a la izquierda y el de ellos a la derecha.

  • Al presionar, puede omitir "su" lado del refspec para decirle al control remoto que elimine una reference. Para solicitar a su git que elimine el develop sucursal, puede usar git push :refs/heads/develop .

Crucial: fetch y push no son simétricos

Además de las diferencias sintácticas algo obvias ( fetch no tiene una forma de eliminar, y los bits del lado izquierdo vs derecho), hay una cosa más que es absolutamente crucial aquí. Cuando usas git fetch , copys sus twigs a tus "twigs remotas", pero cuando presionas, pides enviar tus twigs a sus twigs, no a ningún tipo de "twig remota".

Lo que esto significa es que si aún no tiene todas las confirmaciones que tienen en su sucursal local, y les pide que tomen esa sucursal a través de git push , les pedirá que pierdan algún compromiso (s), específicamente, lo que se comprometa ellos tienen que no.

Normalmente, un control remoto rechazará ("rechazará") un empuje que perdería compromisos ("avance no rápido"). Puede anular esto con --force pero eso generalmente es lo que no debe hacer.

A diferencia de push , git fetch pone "sus" twigs en las sucursales de seguimiento remoto. Por lo tanto, search solo toma lo que tienen cada vez. Esto no puede interrumpir tus twigs ya que ni siquiera toca tus twigs. 5

Esta es la razón por la que fusionas o rebases

Supongamos que has hecho algún trabajo, y alguien más también ha hecho algún trabajo. Supongamos también que ambos usan git push para actualizar un tercer repository (tal vez en github, por ejemplo), y ambos trabajan en el develop sucursal. Considérelo como una especie de carrera: ambos hicieron algunos cambios y los comprometieron localmente, y ahora están en una carrera para ver quién puede avanzar primero.

Digamos que el otro chico gana la carrera. Hace su git push , que llama a su control remoto, este también es su control remoto, y les pide (el control remoto) que tome su twig de develop y la convierta en su twig de develop . Como lo que ha hecho es simplemente agregar lo que tenían antes, aceptan su impulso.

Ahora vienes, habiendo perdido la carrera, y pides a tu control remoto que tome tu twig de develop y la convierta en su twig de develop . Controlan como de costumbre, pero esta vez, encuentran un compromiso, o varias confirmaciones, que tienen, que usted no hace. Estos son precisamente los compromisos que empujó el otro tipo, cuando ganó la carrera.

El control remoto rechazará su impulso como un avance rápido.

Ahora puede usar git fetch para get esos commits desde el control remoto. Ingresarán en su twig de origin/develop de rastreo remoto.

Ahora tiene todas sus confirmaciones, más sus propios compromisos. Si dibujamos una parte de su gráfico de compromiso, puede verse más o less así:

  o <-- HEAD=develop / ... - o - o \ * <-- origin/develop 

Aquí, el compromiso marcado * es el que empujó el otro cuando ganó la carrera. Ahora es su trabajo coordinar sus cambios y sus cambios.

Hay muchos artículos (aquí en stackoverflow y en otros lugares) sobre cómo lidiar con esto (fusionar o rebasear, cuándo usar uno, y así sucesivamente).

Si prefiere fusionar o rebase, puede hacerlo manualmente:

 git fetch git rebase 

o puede usar el script git pull , quizás con --rebase o un elemento de configuration, para combinar los dos pasos. (Creo que es mejor comenzar haciéndolos por separado, ya que obtendrás un mejor model mental del flujo de trabajo, y eventualmente sabrás intuitivamente cuándo es seguro usar git pull como un método conveniente de hacer ambas operaciones, todo de una vez. Además, cuando los haces por separado, puedes mirar y ver lo que sucedió desde la última resynchronization, ya que el git log ..@{u} 6 te mostrará lo que hay en la ruta ascendente que no está en tu sucursal. )

¿Qué tal una fetch muy lenta? Para el caso, ¿qué tal un push muy lento?

El elemento key aquí es que cuando haces fetch o push , primero git averigua qué objects del repository transferir, luego comienza a hacer las transferencias. Luego, para un push , git atómicamente hace su verificación, y permite o rechaza la parte de actualización de label de la operación.

Consideremos fetch primero, y supongamos que git puede usar un "package delgado". Tu git llama a su git, los dos gits se dan count de que necesitas muchos megabytes o incluso gigabytes de commits, treees, blobs y / o tags (estos son los cuatro types de objects en el repository) y su git los empaqueta a todos como un "package delgado".

En este punto, su git comienza el lento process de transferir toda esta información a través del teléfono de Internet. Si alguien más viene y hace un (exitoso) push , ese empujón junto con sus objects, va al repository remoto, pero su git y su git ya han decidido lo que viene, y esos nuevos objects y / o configuration de tags no estan incluidos

Cuando finaliza tu búsqueda, tu git expande el package delgado (aquí es donde ves el post "resolver deltas") y actualiza tus tags, en function de lo que hayas traído. Es como si el impulso aún no hubiera sucedido: obtienes una instantánea atómica de lo que tenían cuando comenzaste tu búsqueda.

(Esta es la razón por la que es posible que desee ejecutar una segunda fetch inmediatamente después de una fetch larga y de varias horas en un repository ocupado: puede recoger cualquier cambio que haya ocurrido durante ese período. Con un poco de suerte, esta vez solo tendrá unos pocos artículos pequeños que traer, que tomarán solo unos pocos miles de milisegundos, probablemente no el time suficiente para que sigan apareciendo más cambios).

Si tiene un push muy lento, la situación es similar: envía al control remoto un "package delgado" que no es muy fino, y una vez que obtiene todo (y resuelve deltas), luego verifica si se trata de un ataque rápido. hacia adelante, o está permitido de otra manera. Esta comprobación y (si se permite) la actualización se hace atómicamente (el control remoto usa un file de locking para lograr esta atomicidad, y de hecho, una fetch que ejecuta utiliza un file de locking de su lado, por la misma razón). Para cada actualización de label (twig o label), la inserción tiene éxito o falla. 7

(Si está utilizando un protocolo "tonto", los detalles cambian, pero la estrategia general sigue siendo la misma. Las actualizaciones son, o en general deberían aparecer, atómicas).


1 O no, simplemente, ya que tiene muchos casos de esquina especiales que trata de manejar, más la lógica de hacer rebase lugar de merge .

2 Puede hacer operaciones de git en su máquina local que no involucren una segunda instancia de git. Sin embargo, el principio es el mismo; es solo que ahora su git local juega los roles "local" y "remoto", hablando consigo mismo.

3 En pocas palabras, es un nombre de twig si está en refs/heads/ y es una label si está en refs/tags/ . Git tiene la oportunidad de ver en qué se encuentra (en ambos lados) durante la "llamada telefónica" entre las dos instancias de git. Si el nombre puede ser ambiguo, si hay twigs y tags llamadas bob , por ejemplo, puede deletrear explícitamente cuál (es) desea (n).

4 El signo más simplemente establece la bandera de "fuerza" para ese refspec particular. Esta es la misma bandera de fuerza que puede establecer con --force , excepto que es por refspec en lugar de global.

5 Esto pasa por alto el hecho de que las tags de git usan un solo espacio de nombre global. Es decir, cuando git fetch desde un control remoto, incluso sin agregar --tags , git puede actualizar tus refs/tags/ inputs locales. En particular, a less que especifique --no-tags , su git verá si alguno de los nuevos identificadores de object SHA-1 que usted trae a queueción corresponde a cualquier label en el control remoto (vea la salida de git ls-remote : toda la label SHA) -1 ID está disponible en todo momento). Si es así, su git creará una label correspondiente. Como no hay un espacio de nombres de "tags remotas" (a less que lo reinventes una vez más), no es completamente seguro git fetch , ya que puede agregar una label "sorpresa" (una que no esperabas). En la práctica, sin embargo, dado que las tags nunca se mueven, esto no es un problema.

6 La syntax @{u} significa @{upstream} , que significa "encontrar la twig upstream que estoy rastreando", que en este ejemplo sería origin/develop . Una vez que hayas hecho la git fetch , el origin/develop apunta al último compromiso presente en el control remoto, ya que acabas de recuperarlo, y la syntax .. significa "encontrar confirmaciones accesibles desde el especificador del lado derecho que no son accesibles desde el especificador del lado izquierdo ". El lado izquierdo vacío significa HEAD que significa que la punta de su twig actual se develop , por lo que le pide a git que registre las confirmaciones que están en origin/develop que no están en develop .

7 Para las pulsaciones regulares, esto es bastante sencillo: esperas presionar un avance rápido, donde el control remoto, por ejemplo, tendrá twigs refs/heads/B apuntando a cometer 1234567... Has cometido fedcba9... cuyo antecesor es 1234567... y pides enviar este commit a sus refs/heads/B Una vez que tienen el package, verifican si sus refs/heads/B actuales son un antecesor de lo que está pidiendo que lo configuren. O bien, se ha solicitado una operación de avance rápido en la label, o no, y se rechazó la inserción.

Sin embargo, para empujes forzados o cuando se elimina una twig, es posible que desee asegurarse de que el control remoto refs/heads/B apunta a un compromiso específico , es decir, que nadie más ha ganado una "carrera de empuje" contra su fuerza. presionar o borrar la operación. Esto fue, en un punto, imposible en git, pero desde 1.8.5, git ha adquirido la opción --force-with-lease for push. Aquí, especificas el SHA-1 al que crees que el control remoto tendrá su label apuntando, para cuando tu empuje haya llegado completamente y se esté ejecutando atómicamente. Si está en lo correcto, la actualización está permitida. Si resulta que la label tiene algún otro valor, en su lugar se rechaza su actualización forzada.

Esto no es algo que la mayoría de la gente normalmente necesita, pero permite actualizaciones atómicas que no son rápidas.

8 ¿Qué, nunca? Bueno, casi nunca!

Debes recordar que una twig es solo una reference a una confirmación en git . Es decir, actualizar una twig es en realidad una operación muy rápida y fácil, que también es muy fácil de implementar atómicamente. Sí, también necesita transmitir un número de blobs en una operación push / fetch , pero estos son inconsistentes para el estado del repository: no importa si tiene algunos blobs en un repository que no están (indirectamente) conectados a branches , lo que importa es qué blobs son accesibles a través de las twigs.

Eso significa que puede esperar que todas push operaciones push y fetch / pull sean atómicas, lo que le da a las operaciones push / fetch que afectan a un repository dado un order total. O bien, la push ocurre antes de la fetch , o la push ocurre después de la fetch , no hay una tercera opción.

Con respecto a su pregunta sobre la modificación de un compromiso: Esa es una operación puramente local. Crea un nuevo compromiso con los mismos antepasados ​​que el que está modificando, y apunta su twig a ese nuevo compromiso. Eso es todo, no hay otro repository involucrado.

Sin embargo, nunca debe modificar un compromiso que ya ha enviado: eso es reescribir el historial, y las consecuencias de reescribir el historial son graves. Nunca intente hacer esto sin entender exactamente lo que está haciendo primero.