¿En qué casos podría ser perjudicial `git pull`?

409

Tengo un colega que dice que git pulles dañino y se enoja cuando alguien lo usa.

El git pullcomando parece ser la forma canónica de actualizar su repositorio local. ¿El uso git pullcrea problemas? ¿Qué problemas crea? ¿Hay una mejor manera de actualizar un repositorio git?

Richard Hansen
fuente
53
@ j.karlsson: meta.stackexchange.com/questions/17463/…
Richard Hansen el
8
O puede simplemente git pull --rebaseestablecer esta estrategia como predeterminada para nuevas sucursales git config branch.autosetuprebase
knoopx el
44
knoopx tiene razón, al agregar un --rebaseindicador para git pullsincronizar local con remoto, luego reproduce los cambios locales además de los locales actualizados. Luego, cuando empuja, todo lo que está haciendo es agregar sus nuevas confirmaciones al final del control remoto. Bastante simple.
Heath Lilley
44
Gracias @BenMcCormick. Ya lo había hecho, pero la discusión sobre la validez de la pregunta parece estar teniendo lugar en estos comentarios debajo de la pregunta. Y creo que hacer una pregunta para crear una plataforma para presentar su opinión personal como un hecho no es para qué es realmente la estructura de preguntas y respuestas de SO.
mcv
44
@RichardHansen, parece una forma de engañar al sistema de puntos, especialmente si su respuesta tiene una diferencia de tono tan drástica y un intervalo de tiempo tan corto. Usando su modelo de preguntas y respuestas, todos podríamos hacer preguntas y responderlas nosotros mismos utilizando nuestros conocimientos previos. En ese punto, deberías considerar escribir una publicación de blog, ya que eso es muchas veces más apropiado. Un Q&A busca específicamente el conocimiento de otras personas. Una publicación de blog exhibe la tuya.
Josh Brown

Respuestas:

546

Resumen

De forma predeterminada, git pullcrea confirmaciones de fusión que agregan ruido y complejidad al historial del código. Además, pullhace que sea fácil no pensar en cómo sus cambios podrían verse afectados por los cambios entrantes.

El git pullcomando es seguro siempre que solo realice fusiones de avance rápido. Si git pullestá configurado para hacer fusiones de avance rápido y cuando no es posible una fusión de avance rápido, Git saldrá con un error. Esto le dará la oportunidad de estudiar los commits entrantes, pensar en cómo podrían afectar sus commits locales y decidir el mejor curso de acción (fusionar, rebase, reset, etc.).

Con Git 2.0 y más reciente, puede ejecutar:

git config --global pull.ff only

para alterar el comportamiento predeterminado a solo avance rápido. Con las versiones de Git entre 1.6.6 y 1.9.x tendrás que acostumbrarte a escribir:

git pull --ff-only

Sin embargo, con todas las versiones de Git, recomiendo configurar un git upalias como este:

git config --global alias.up '!git remote update -p; git merge --ff-only @{u}'

y usando en git uplugar de git pull. Prefiero este alias sobre git pull --ff-onlyporque:

  • funciona con todas las versiones (no antiguas) de Git,
  • recupera todas las ramas ascendentes (no solo la rama en la que está trabajando actualmente), y
  • limpia las origin/*ramas viejas que ya no existen aguas arriba.

Problemas con git pull

git pullno está mal si se usa correctamente. Varios cambios recientes en Git han hecho que sea más fácil de usar git pullcorrectamente, pero desafortunadamente el comportamiento predeterminado de un plano git pulltiene varios problemas:

  • introduce no linealidades innecesarias en la historia
  • hace que sea fácil reintroducir accidentalmente confirmaciones que fueron intencionalmente refundadas
  • modifica tu directorio de trabajo de maneras impredecibles
  • pausar lo que estás haciendo para revisar el trabajo de otra persona es molesto con git pull
  • hace que sea difícil rebase correctamente en la rama remota
  • no limpia las ramas que se eliminaron en el repositorio remoto

Estos problemas se describen con mayor detalle a continuación.

Historia no lineal

Por defecto, el git pullcomando es equivalente a ejecutar git fetchseguido de git merge @{u}. Si hay confirmaciones no aceleradas en el repositorio local, la parte de git pullfusión crea una confirmación de fusión.

No hay nada inherentemente malo en los compromisos de fusión, pero pueden ser peligrosos y deben tratarse con respeto:

  • Los compromisos de fusión son inherentemente difíciles de examinar. Para comprender lo que está haciendo una fusión, debe comprender las diferencias con todos los padres. Una diferencia convencional no transmite bien esta información multidimensional. En contraste, una serie de confirmaciones normales es fácil de revisar.
  • La resolución de conflictos de fusión es complicada, y los errores a menudo pasan desapercibidos durante mucho tiempo porque los compromisos de fusión son difíciles de revisar.
  • Las fusiones pueden reemplazar silenciosamente los efectos de las confirmaciones regulares. El código ya no es la suma de confirmaciones incrementales, lo que lleva a malentendidos sobre lo que realmente cambió.
  • Las confirmaciones de fusión pueden alterar algunos esquemas de integración continua (por ejemplo, autocompilar solo la ruta del primer padre bajo la convención asumida de que los segundos padres señalan trabajos incompletos en progreso).

Por supuesto, hay un momento y un lugar para las fusiones, pero comprender cuándo las fusiones deben y no deben usarse puede mejorar la utilidad de su repositorio.

Tenga en cuenta que el propósito de Git es hacer que sea fácil compartir y consumir la evolución de una base de código, no registrar con precisión el historial exactamente como se desarrolló. (Si no está de acuerdo, considere el rebasecomando y por qué se creó). Las confirmaciones de fusión creadas por git pullno transmiten semántica útil a los demás, solo dicen que alguien más empujó al repositorio antes de que haya terminado con sus cambios. ¿Por qué esos compromisos de fusión si no son significativos para los demás y podrían ser peligrosos?

Es posible configurar git pulluna nueva versión en lugar de fusionar, pero esto también tiene problemas (discutido más adelante). En su lugar, git pulldebe configurarse para hacer solo fusiones de avance rápido.

Reintroducción de Comisiones Rebajadas

Supongamos que alguien rebasa una rama y la fuerza la empuja. Esto generalmente no debería suceder, pero a veces es necesario (p. Ej., Eliminar un archivo de registro de 50GiB que se introdujo y se introdujo accidentalmente). La fusión realizada por git pullfusionará la nueva versión de la rama ascendente en la versión anterior que todavía existe en su repositorio local. Si presiona el resultado, las horquillas y las antorchas comenzarán a aparecer.

Algunos pueden argumentar que el verdadero problema son las actualizaciones forzadas. Sí, generalmente es aconsejable evitar los empujes forzados siempre que sea posible, pero a veces son inevitables. Los desarrolladores deben estar preparados para lidiar con las actualizaciones de fuerza, porque a veces sucederán. Esto significa no fusionarse ciegamente en los viejos commits a través de un ordinario git pull.

Modificaciones de directorio de trabajo sorpresa

No hay forma de predecir cómo se verá el directorio o índice de trabajo hasta que git pullse haga. Puede haber conflictos de fusión que tiene que resolver antes de que pueda hacer cualquier otra cosa, podría introducir un archivo de registro de 50GiB en su directorio de trabajo porque alguien lo presionó accidentalmente, podría cambiar el nombre de un directorio en el que está trabajando, etc.

git remote update -p(o git fetch --all -p) le permite ver los compromisos de otras personas antes de que decida fusionar o cambiar de base, lo que le permite formar un plan antes de tomar medidas.

Dificultad para revisar los compromisos de otras personas

Suponga que está haciendo algunos cambios y alguien más quiere que revise algunas confirmaciones que acaba de enviar. git pullLa operación de fusión (o rebase) modifica el directorio y el índice de trabajo, lo que significa que el directorio y el índice de trabajo deben estar limpios.

Podrías usar git stashy luego git pull, pero ¿qué haces cuando termines de revisar? Para volver a donde estabas, debes deshacer la fusión creada por git pully aplicar el alijo.

git remote update -p(o git fetch --all -p) no modifica el directorio o índice de trabajo, por lo que es seguro ejecutarlo en cualquier momento, incluso si tiene cambios organizados o no organizados. Puede pausar lo que está haciendo y revisar el compromiso de otra persona sin preocuparse por ocultar o terminar el compromiso en el que está trabajando. git pullno te da esa flexibilidad.

Rebasándose en una rama remota

Un patrón de uso común de Git es hacer una git pullpara incorporar los últimos cambios seguidos de una git rebase @{u}para eliminar la confirmación de fusión que se git pullintrodujo. Es bastante común que Git tiene algunas opciones de configuración para reducir estos dos pasos a un solo paso a contar git pullpara realizar un rebase en lugar de una fusión (ver branch.<branch>.rebase, branch.autosetuprebasey pull.rebaseopciones).

Desafortunadamente, si tiene una confirmación de fusión no acelerada que desea conservar (p. Ej., Una confirmación que fusiona una rama de función empujada master), ni un rebase-pull ( git pullcon branch.<branch>.rebaseset to true) ni un merge-pull (el git pullcomportamiento predeterminado ) seguido de un rebase funcionará. Esto se debe a que git rebaseelimina las fusiones (linealiza el DAG) sin la --preserve-mergesopción. La operación de rebase-pull no se puede configurar para preservar las fusiones, y una fusión-extracción seguida de un git rebase -p @{u}no eliminará la fusión causada por la fusión-extracción. Actualización: Git v1.8.5 agregado git pull --rebase=preservey git config pull.rebase preserve. Esto se git pulldebe hacer git rebase --preserve-mergesdespués de recuperar las confirmaciones anteriores. (¡Gracias a funkaster por el aviso !)

Limpieza de ramas eliminadas

git pullno elimina las ramas de seguimiento remoto correspondientes a las ramas que se eliminaron del repositorio remoto. Por ejemplo, si alguien elimina una rama foodel repositorio remoto, aún verá origin/foo.

Esto lleva a los usuarios a resucitar accidentalmente ramas muertas porque creen que todavía están activas.

Una mejor alternativa: usar en git uplugar degit pull

En lugar de git pull, recomiendo crear y usar el siguiente git upalias:

git config --global alias.up '!git remote update -p; git merge --ff-only @{u}'

Este alias descarga todas las últimas confirmaciones de todas las ramas ascendentes (podando las ramas muertas) e intenta adelantar la rama local a la última confirmación en la rama ascendente. Si tuvo éxito, entonces no hubo compromisos locales, por lo que no hubo riesgo de conflicto de fusión. El avance rápido fallará si hay confirmaciones locales (no eliminadas), lo que le brinda la oportunidad de revisar las confirmaciones anteriores antes de tomar medidas.

Esto todavía modifica su directorio de trabajo de maneras impredecibles, pero solo si no tiene ningún cambio local. A diferencia git pull, git upnunca lo llevará a un aviso esperando que solucione un conflicto de fusión.

Otra opción: git pull --ff-only --all -p

La siguiente es una alternativa al git upalias anterior :

git config --global alias.up 'pull --ff-only --all -p'

Esta versión de git uptiene el mismo comportamiento que el git upalias anterior , excepto:

  • el mensaje de error es un poco más críptico si su rama local no está configurada con una rama ascendente
  • se basa en una característica no documentada (el -pargumento, que se pasa a fetch) que puede cambiar en futuras versiones de Git

Si está ejecutando Git 2.0 o más reciente

Con Git 2.0 y versiones más recientes, puede configurar git pullpara hacer solo fusiones de avance rápido de forma predeterminada:

git config --global pull.ff only

Esto hace git pullque actúe como git pull --ff-only, pero todavía no obtiene todos los commits ascendentes ni limpia las origin/*ramas viejas , por lo que todavía prefiero git up.

Richard Hansen
fuente
66
@brianz: git remote update -pes equivalente a git fetch --all -p. Tengo la costumbre de escribir git remote update -pporque alguna vez fetchno tenía la -popción. En cuanto a la guía !, vea la descripción de alias.*in git help config. Dice: "Si la expansión del alias tiene el prefijo de un signo de exclamación, se tratará como un comando de shell".
Richard Hansen
13
Git 2.0 agrega una pull.ffconfiguración que parece lograr lo mismo, sin alias.
Danny Thomas
51
Algunas de las razones suenan como "tirar puede causar problemas cuando otros hacen cosas locas". No, es una locura como rebasar una confirmación de un repositorio ascendente que causa problemas. El rebase de IMO solo es seguro cuando lo haces localmente en un commit que aún no se ha enviado. Al igual que, por ejemplo, cuando tira antes de presionar, el cambio de las confirmaciones locales ayuda a mantener su historial lineal (aunque el historial lineal no es tan importante). Aún así, git upsuena como una alternativa interesante.
mcv
16
La mayoría de sus puntos se deben a que está haciendo algo mal: está tratando de revisar el código en su propia rama de trabajo . Esa no es una buena idea, simplemente cree una nueva rama, extraiga --rebase = preservar y luego arroje esa rama (o combínela si lo desea).
funkaster
55
El punto de @funkaster aquí tiene mucho sentido, especialmente en relación con: "Dificultad para revisar los compromisos de otras personas". Este no es el flujo de revisión que utilizan la mayoría de los usuarios de Git, es algo que nunca he visto recomendado en ninguna parte y es la causa de todo el trabajo adicional innecesario descrito debajo del título, no git pull.
Ben Regenspan
195

Mi respuesta, extraída de la discusión que surgió en HackerNews:

Me siento tentado de responder la pregunta utilizando la Ley de titulares de Betteridge: ¿Por qué se git pullconsidera perjudicial? No lo es

  • Las no linealidades no son intrínsecamente malas. Si representan la historia real, están bien.
  • La reintroducción accidental de commits rebase arriba es el resultado de reescribir incorrectamente la historia arriba. No puede reescribir el historial cuando el historial se replica a lo largo de varios repositorios.
  • Modificar el directorio de trabajo es un resultado esperado; de utilidad discutible, es decir, frente al comportamiento de hg / monotone / darcs / other_dvcs_predating_git, pero nuevamente no es intrínsecamente malo.
  • Hacer una pausa para revisar el trabajo de otros es necesario para una fusión, y nuevamente es un comportamiento esperado en git pull. Si no desea fusionar, debe usar git fetch. Nuevamente, esta es una idiosincrasia de git en comparación con los dvcs populares anteriores, pero es un comportamiento esperado y no intrínsecamente malo.
  • Hacer que sea difícil rebase contra una rama remota es bueno. No reescribas el historial a menos que sea absolutamente necesario. Por mi vida no puedo entender esta búsqueda de una historia lineal (falsa)
  • No limpiar ramas es bueno. Cada repositorio sabe lo que quiere mantener. Git no tiene noción de relaciones maestro-esclavo.
Sergio Carvalho
fuente
13
Estoy de acuerdo. No hay nada inherentemente dañino al respecto git pull. Sin embargo, podría entrar en conflicto con algunas prácticas dañinas, como querer reescribir la historia más de lo estrictamente necesario. Pero git es flexible, por lo que si desea usarlo de una manera diferente, hágalo. Pero eso es porque usted (bueno, @ Richard Hansen) quiere hacer algo inusual en git, y no porque git pullsea ​​dañino.
mcv
28
No podría estar más de acuerdo. ¿La gente aboga git rebasey considera git pullperjudicial? De Verdad?
Victor Moroz
10
Sería bueno ver a alguien crear un gráfico, con la moralidad como eje, y clasificar los comandos git como buenos, malos o en algún punto intermedio. Este gráfico diferiría entre los desarrolladores, aunque diría que mucho sobre uno usa git.
michaelt
55
Mi problema con git pullsin la --rebaseopción es la dirección de fusión que crea. Cuando observa el diferencial, todos los cambios en esa fusión ahora pertenecen a la persona que realizó la extracción, en lugar de la persona que realizó los cambios. Me gusta un flujo de trabajo donde la fusión está reservada para dos ramas separadas (A -> B), por lo que la confirmación de fusión es clara de lo que se introdujo, y la reorganización está reservada para actualizarse en la misma rama (A remota> A local )
Craig Kochis
44
Entonces, ¿qué te hace saber si alguien hizo un tirón unos segundos antes que alguien más o al revés? Creo que esto es solo ruido y solo ofusca la historia realmente relevante. Esto incluso disminuye el valor de la historia. Una buena historia debe ser a) limpia yb) en realidad tener la historia importante.
David Ongaro
26

No se considera dañino si está utilizando Git correctamente. Veo cómo te afecta negativamente dado tu caso de uso, pero puedes evitar problemas simplemente no modificando el historial compartido.

Hunt Burdick
fuente
10
Para elaborar esto: si todos trabajan en su propia rama (que en mi opinión es la forma correcta de usar git), git pullno hay ningún tipo de problema. Ramificar en git es barato.
AlexQueue
18

La respuesta aceptada reclama

La operación de rebase-pull no se puede configurar para preservar fusiones

pero a partir de Git 1.8.5 , que data de esa respuesta, puedes hacer

git pull --rebase=preserve

o

git config --global pull.rebase preserve

o

git config branch.<name>.rebase preserve

Los documentos dicen

Cuando preserve,también se pasa --preserve-mergesa 'git rebase' para que los compromisos de fusión comprometidos localmente no se aplanen ejecutando 'git pull'.

Esta discusión previa tiene información y diagramas más detallados: git pull --rebase --preserve-merges . También explica por qué git pull --rebase=preserveno es lo mismo que git pull --rebase --preserve-merges, lo que no hace lo correcto.

Esta otra discusión previa explica qué hace realmente la variante preservar-fusiones de rebase, y cómo es mucho más complejo que un rebase regular: ¿Qué hace exactamente "rebase --preserve-merges" de git (y por qué?)

Marc Liyanage
fuente
0

Si va al antiguo repositorio de git, obtenga el alias que sugieren que es diferente. https://github.com/aanand/git-up

git config --global alias.up 'pull --rebase --autostash'

Esto funciona perfecto para mi.

Nathan Redblur
fuente