Mi oficina quiere infinitas fusiones de sucursales como política; ¿Qué otras opciones tenemos?


119

Mi oficina está tratando de descubrir cómo manejamos las divisiones y fusiones de sucursales, y nos hemos encontrado con un gran problema.

Nuestro problema es con las ramas laterales a largo plazo, del tipo en el que hay algunas personas que trabajan en una rama lateral que se divide del maestro, que desarrollamos durante unos meses, y cuando alcanzamos un hito sincronizamos los dos.

Ahora, en mi humilde opinión, la forma natural de manejar esto es aplastar la rama lateral en una sola confirmación. mastersigue progresando hacia adelante; como debería: no estamos volcando retroactivamente meses de desarrollo paralelo en masterla historia de. Y si alguien necesita una mejor resolución para la historia de la rama lateral, bueno, por supuesto, todo sigue ahí, simplemente no está master, está en la rama lateral.

Aquí está el problema: trabajo exclusivamente con la línea de comando, pero el resto de mi equipo usa GUIS. Y he descubierto que el GUIS no tiene una opción razonable para mostrar el historial de otras ramas. Entonces, si llega a una confirmación de squash, diciendo "este desarrollo aplastado desde la rama XYZ", es un gran dolor ir a ver qué hay dentro XYZ.

En SourceTree, por lo que puedo encontrar, es un gran dolor de cabeza: si está conectado mastery desea ver el historial master+devFeature, debe verificar master+devFeature(tocar cada archivo que sea diferente), o bien desplácese por un registro que muestre TODAS las ramas de su repositorio en paralelo hasta que encuentre el lugar correcto. Y buena suerte para averiguar dónde estás allí.

Mis compañeros de equipo, con toda razón, no quieren tener un historial de desarrollo tan inaccesible. Por lo tanto, quieren que se fusionen estas grandes y largas ramas de desarrollo lateral, siempre con un compromiso de fusión. No quieren ningún historial al que no se pueda acceder inmediatamente desde la rama maestra.

Me gusta esa idea; significa una maraña interminable e inevitable de la historia paralela del desarrollo. Pero no veo qué alternativa tenemos. Y estoy bastante desconcertado; Esto parece bloquear casi todo lo que sé sobre una buena administración de sucursales, y será una frustración constante para mí si no puedo encontrar una solución.

¿Tenemos alguna opción aquí además de fusionar constantemente ramas laterales en master con merge-commits? O, ¿hay alguna razón por la que constantemente usar merge-commits no sea tan malo como me temo?


84
¿Grabar una fusión como una fusión es realmente tan malo? Puedo ver que el aplastamiento en una historia lineal tiene sus ventajas, pero me sorprende que no hacerlo vaya en contra de "casi todo lo que sabe sobre una buena administración de sucursales". ¿Cuáles son exactamente los problemas que temes?
IMSoP

65
OK, pero en mi mente una rama de larga duración es exactamente donde se hace querer un poco de visibilidad, por lo que culpa y biseca apenas no aterrizar sobre "el cambio se introdujo como parte de la Gran reescritura de 2016". No tengo la confianza suficiente para publicar como respuesta, pero mi instinto es que las tareas dentro de la rama de características deben ser eliminadas, para que tenga un historial breve del proyecto accesible desde la historia principal, sin tener que Echa un vistazo a una rama huérfana.
IMSoP

55
Dicho esto, es bastante posible, por lo que la pregunta se reduce a "Estoy tratando de usar git a un nivel más alto que mis compañeros de equipo; ¿cómo puedo evitar que me echen a perder?" Lo cual, justamente, honestamente no esperaba git log master+bugfixDec10thser el punto de ruptura: - /
Standback

26
"ramificaciones laterales a largo plazo: del tipo en el que hay algunas personas trabajando en una ramificación lateral que se divide del maestro, la desarrollamos durante unos meses, y cuando alcanzamos un hito los sincronizamos". ¿No se tira periódicamente de la rama maestra a la lateral? Hacer eso cada (pocos) compromisos para dominar tiende a simplificar la vida en muchos casos.
TafT

44
Es merge --no-fflo que quieres En mastersí mismo, tiene una confirmación que describe lo que cambió en la rama, pero todas las confirmaciones todavía existen y están relacionadas con HEAD.
CAD97

Respuestas:


243

Aunque uso Git en la línea de comando, tengo que estar de acuerdo con tus colegas. No es sensato aplastar grandes cambios en una sola confirmación. Estás perdiendo la historia de esa manera, no solo haciéndola menos visible.

El punto de control de la fuente es rastrear el historial de todos los cambios. ¿Cuándo cambió qué por qué? Con ese fin, cada confirmación contiene punteros a las confirmaciones principales, una diferencia y metadatos como un mensaje de confirmación. Cada confirmación describe el estado del código fuente y el historial completo de todos los cambios que llevaron a ese estado. El recolector de basura puede eliminar confirmaciones que no son accesibles.

Acciones como rebase, selección de cerezas o aplastar borrar o reescribir el historial. En particular, las confirmaciones resultantes ya no hacen referencia a las confirmaciones originales. Considera esto:

  • Aplasta algunas confirmaciones y observa en el mensaje de confirmación que el historial aplastado está disponible en la confirmación original abcd123.
  • Elimina [1] todas las ramas o etiquetas que incluyen abcd123 ya que están fusionadas.
  • Dejaste correr al recolector de basura.

[1]: Algunos servidores Git permiten que las ramas estén protegidas contra la eliminación accidental, pero dudo que quieras mantener todas tus ramas características por la eternidad.

Ahora ya no puede buscar esa confirmación, simplemente no existe.

Hacer referencia a un nombre de sucursal en un mensaje de confirmación es aún peor, ya que los nombres de sucursal son locales para un repositorio. Lo que está master+devFeatureen su caja local puede estar doodlediduhen el mío. Las ramas son solo etiquetas móviles que apuntan a algún objeto de confirmación.

De todas las técnicas de reescritura de historial, el rebase es el más benigno porque duplica los commits completos con todo su historial, y simplemente reemplaza un commit padre.

Que la masterhistoria incluya la historia completa de todas las ramas que se fusionaron es algo bueno, porque eso representa la realidad. [2] Si hubo un desarrollo paralelo, eso debería ser visible en el registro.

[2]: Por esta razón, también prefiero las confirmaciones de fusión explícitas sobre la historia linealizada pero en última instancia falsa resultante del rebase.

En la línea de comando, se git logesfuerza por simplificar el historial que se muestra y mantener todas las confirmaciones mostradas relevantes. Puede ajustar la simplificación del historial para satisfacer sus necesidades. Es posible que sienta la tentación de escribir su propia herramienta de registro git que recorra el gráfico de confirmación, pero generalmente es imposible responder "¿esta confirmación se confirmó originalmente en esta o aquella rama?". El primer padre de una confirmación de fusión es el anterior HEAD, es decir, la confirmación en la rama en la que se está fusionando. Pero eso supone que no hizo una fusión inversa de maestro en la rama de características, luego avanzó rápidamente maestro a la fusión.

La mejor solución para las sucursales a largo plazo que he encontrado es evitar las sucursales que solo se fusionan después de un par de meses. La fusión es más fácil cuando los cambios son recientes y pequeños. Idealmente, se fusionará al menos una vez por semana. La integración continua (como en Extreme Programming, no como en "configuremos un servidor Jenkins"), incluso sugiere múltiples fusiones por día, es decir, no para mantener ramas de características separadas sino compartir una rama de desarrollo como equipo. La fusión antes de que una característica sea QA requiere que la característica esté oculta detrás de un indicador de característica.

A cambio, la integración frecuente hace posible detectar problemas potenciales mucho antes y ayuda a mantener una arquitectura consistente: son posibles cambios de gran alcance porque estos cambios se incluyen rápidamente en todas las ramas. Si un cambio rompe algún código, solo romperá un par de días de trabajo, no un par de meses.

La reescritura de la historia puede tener sentido para proyectos verdaderamente grandes cuando hay múltiples millones de líneas de código y cientos o miles de desarrolladores activos. Es cuestionable por qué un proyecto tan grande tendría que ser un único repositorio git en lugar de dividirse en bibliotecas separadas, pero a esa escala es más conveniente si el repositorio central solo contiene "versiones" de los componentes individuales. Por ejemplo, el kernel de Linux emplea el aplastamiento para mantener manejable el historial principal. Algunos proyectos de código abierto requieren que los parches se envíen por correo electrónico, en lugar de una fusión a nivel de git.


43
@Standback No me importa lo que haga un desarrollador localmente ... use confirmaciones "wip", confirmaciones adicionales para cada error tipográfico fijo, ... Eso está bien, es mejor comprometerse con demasiada frecuencia. Idealmente, el desarrollador limpia esas confirmaciones antes de que se envíen, como combinar todas las confirmaciones que solo arreglan algunos errores tipográficos. Sigue siendo una buena idea mantener las confirmaciones separadas si hacen cosas diferentes, por ejemplo, una confirmación para la funcionalidad real y pruebas relacionadas, otra para errores tipográficos no relacionados. Pero una vez que se envían los commits, reescribir o eliminar el historial es más problemático de lo que vale, al menos en mi experiencia.
amon

11
"En general, es imposible responder" ¿este compromiso se cometió originalmente en esta o aquella rama? "" Veo el hecho de que las "ramas" son solo indicadores para comprometerse sin una historia real propia como uno de los mayores defectos de diseño en git . Realmente quiero poder preguntarle a mi sistema de control de versiones "qué había en la rama x en la fecha y".
Peter Green

80
Nada es más frustrante que tratar de identificar la causa de un error en el código usando git bisect, solo para que llegue a un uber-commit de 10,000 líneas desde una rama lateral.
tpg2114

2
@Standback en mi humilde opinión, cuanto más pequeño es el commit, más legible y amigable es. Una confirmación que toca más de unos pocos puntos es imposible de comprender a primera vista, por lo que solo debe tomar la descripción al pie de la letra. Esa es la legibilidad de la descripción (por ejemplo, "característica X implementada"), no el commit (código) en sí. Prefiero tener 1000 de confirmaciones de una letra como "cambió http a https"
:)

2
cherry-pick no reescribe ni elimina el historial. solo crea nuevas confirmaciones que replican los cambios de las confirmaciones existentes
Sarge Borsch

110

Me gusta la respuesta de Amon , pero sentí que una pequeña parte necesitaba mucho más énfasis: puede simplificar fácilmente el historial mientras visualiza registros para satisfacer sus necesidades, pero otros no pueden agregar el historial mientras visualiza registros para satisfacer sus necesidades. Por eso es preferible mantener la historia tal como ocurrió.

Aquí hay un ejemplo de uno de nuestros repositorios. Usamos un modelo de solicitud de extracción, por lo que cada característica se parece a sus ramas de larga duración en el historial, a pesar de que generalmente solo se ejecutan una semana o menos. Los desarrolladores individuales a veces eligen aplastar su historial antes de fusionarse, pero a menudo combinamos características, por lo que es relativamente inusual. Aquí están los pocos commits principales gitk, la interfaz gráfica de usuario que viene incluida con git:

vista estándar de gitk

Sí, un poco enredado, pero también nos gusta porque podemos ver con precisión quién tuvo los cambios a qué hora. Refleja con precisión nuestra historia de desarrollo. Si queremos ver una vista de nivel superior, una solicitud de extracción fusionada a la vez, podemos ver la siguiente vista, que es equivalente al git log --first-parentcomando:

vista gitk con --first-parent

git logtiene muchas más opciones diseñadas para brindarte con precisión las vistas que deseas. gitkpuede tomar cualquier git logargumento arbitrario para construir una vista gráfica. Estoy seguro de que otras GUI tienen capacidades similares. Lea los documentos y aprenda a usarlos correctamente, en lugar de imponer su git logvista preferida a todos en el momento de la fusión.


24
¡No estaba familiarizado con esta opción! Esta es una gran ayuda para mí, y parece que aprender más opciones de registro me permitirá vivir con esto más fácilmente.
Standback

25
+1 para "Puede simplificar fácilmente el historial mientras visualiza registros para satisfacer sus necesidades, pero otros no pueden agregar el historial mientras visualiza registros para satisfacer sus necesidades". Reescribir la historia siempre es cuestionable. Si pensabas que era importante grabar en el momento de la confirmación, entonces era importante. Incluso si descubrió que estaba mal o lo volvió a hacer más tarde, eso es parte de la historia. Algunos errores solo tienen sentido cuando se puede ver con la culpa que esta línea quedó de una reescritura posterior. Cuando los errores se combinan con el resto de la epopeya, no puedes revisar por qué las cosas terminaron como están.
TafT

@Standback Related, una cosa que me ayuda a mantener esta estructura, es usar merge --no-ff- no use fusiones de avance rápido, en su lugar siempre cree una confirmación de fusión para que --first-parenttenga algo con lo que trabajar
Izkata

34

Nuestro problema es con las ramas laterales a largo plazo, del tipo en el que hay algunas personas que trabajan en una rama lateral que se divide del maestro, que desarrollamos durante unos meses, y cuando alcanzamos un hito sincronizamos los dos.

Mi primer pensamiento es: ni siquiera hagas esto a menos que sea absolutamente necesario. Tus fusiones deben ser desafiantes a veces. Mantenga las ramas independientes y lo más efímeras posible. Es una señal de que necesita dividir sus historias en fragmentos de implementación más pequeños.

En el caso de que tenga que hacer esto, entonces es posible fusionarse en git con la opción --no-ff para que las historias se mantengan distintas en su propia rama. Las confirmaciones seguirán apareciendo en el historial combinado, pero también se pueden ver por separado en la rama de características para que al menos sea posible determinar de qué línea de desarrollo formaron parte.

Tengo que admitir que cuando comencé a usar git, me pareció un poco extraño que las confirmaciones de rama aparecieran en el mismo historial que la rama principal después de la fusión. Fue un poco desconcertante porque no parecía que esos compromisos pertenecieran a esa historia. Pero en la práctica, no es algo realmente doloroso, si se considera que la rama de integración es solo eso: todo su propósito es combinar las ramas de características. En nuestro equipo, no aplastamos, y hacemos compromisos de fusión frecuentes. Usamos --no-ff todo el tiempo para asegurarnos de que sea fácil ver el historial exacto de cualquier característica si queremos investigarla.


3
Estoy totalmente de acuerdo; todo el mundo prefiere quedarse cerca del maestro. Pero ese es un tema diferente, que es mucho más grande y mucho menos bajo mi propia humilde influencia :-P
Standback

2
+1 paragit merge --no-ff
0xcaff

1
También +1 para git merge --no-ff. Si usa gitflow, esto se convierte en el predeterminado.
David Hammen

10

Déjame responder tus puntos directa y claramente:

Nuestro problema es con las ramas laterales a largo plazo, del tipo en el que hay algunas personas que trabajan en una rama lateral que se divide del maestro, que desarrollamos durante unos meses, y cuando alcanzamos un hito sincronizamos los dos.

Por lo general, no desea dejar sus ramas sin sincronización durante meses.

Su rama de características se ha ramificado de algo dependiendo de su flujo de trabajo; vamos a llamarlo masterpor simplicidad. Ahora, cada vez que te comprometes a dominar, puedes y debes git checkout long_running_feature ; git rebase master. Esto significa que sus ramas están, por diseño, siempre sincronizadas.

git rebaseTambién es lo correcto hacer aquí. No es un truco o algo extraño o peligroso, sino completamente natural. Pierde un poco de información, que es el "cumpleaños" de la rama de características, pero eso es todo. Si alguien considera que eso es importante, podría proporcionarlo guardándolo en otro lugar (en su sistema de tickets o, si la necesidad es grande, en un git tag...).

Ahora, en mi humilde opinión, la forma natural de manejar esto es aplastar la rama lateral en una sola confirmación.

No, absolutamente no quieres eso, quieres un commit de fusión. Una confirmación de fusión también es una "confirmación única". De alguna manera, no inserta todos los commits de rama individuales "en" maestro. Es un compromiso único con dos padres: el masterjefe y el jefe de la sucursal en el momento de la fusión.

Asegúrese de especificar la --no-ffopción, por supuesto; fusionarse sin --no-ff, en su caso, debería estar estrictamente prohibido. Lamentablemente, --no-ffno es el predeterminado; pero creo que hay una opción que puede configurar que lo haga así. Vea git help mergelo que --no-ffhace (en resumen: activa el comportamiento que describí en el párrafo anterior), es crucial.

No estamos volcando retroactivamente meses de desarrollo paralelo en la historia del maestro.

Absolutamente no: nunca está volcando algo "en la historia" de alguna sucursal, especialmente no con un compromiso de fusión.

Y si alguien necesita una mejor resolución para la historia de la rama lateral, bueno, por supuesto, todo sigue ahí, simplemente no está en el maestro, está en la rama lateral.

Con un commit de fusión, todavía está allí. No en el maestro, sino en la rama lateral, claramente visible como uno de los padres del compromiso de fusión, y guardado por la eternidad, como debería ser.

¿Ves lo que he hecho? Todas las cosas que describe para su confirmación de squash están ahí con la --no-ffconfirmación de fusión .

Aquí está el problema: trabajo exclusivamente con la línea de comando, pero el resto de mi equipo usa GUIS.

(Comentario lateral: también trabajo casi exclusivamente con la línea de comandos (bueno, eso es mentira, generalmente uso emacs magit, pero esa es otra historia: si no estoy en un lugar conveniente con mi configuración individual de emacs, prefiero el comando línea también). Pero, por favor, hágase un favor y probar al menos git guiuna vez. es por lo tanto más eficiente para recoger trozos de líneas, etc., para añadir / deshacer añade.)

Y he descubierto que el GUIS no tiene una opción razonable para mostrar el historial de otras ramas.

Eso es porque lo que estás tratando de hacer está totalmente en contra del espíritu de git. gitse construye desde el núcleo en un "gráfico acíclico dirigido", lo que significa que hay mucha información en la relación padre-hijo de los commits. Y, para las fusiones, eso significa verdaderos compromisos de fusión con dos padres y un hijo. Las GUI de sus colegas estarán bien tan pronto como use las no-ffconfirmaciones de fusión.

Entonces, si llega a una confirmación de squash, diciendo "este desarrollo aplastado de la rama XYZ", es un gran dolor ir a ver qué hay en XYZ.

Sí, pero eso no es un problema de la GUI, sino de la confirmación de squash. El uso de una calabaza significa que deja la cabeza de la rama característica colgando y creando una nueva confirmación master. Esto rompe la estructura en dos niveles, creando un gran desastre.

Por lo tanto, quieren que se fusionen estas grandes y largas ramas de desarrollo lateral, siempre con un compromiso de fusión.

Y tienen toda la razón. Pero no se "fusionaron en ", que se acaba de fusionarse. Una fusión es algo realmente equilibrado, no tiene un lado preferido que se fusione "en" el otro ( git checkout A ; git merge Bes exactamente el mismo que, git checkout B ; git merge Aexcepto por pequeñas diferencias visuales, como las ramas que se intercambian, git logetc.).

No quieren ningún historial al que no se pueda acceder inmediatamente desde la rama maestra.

Lo cual es completamente correcto. En un momento en que no hay características no fusionadas, tendrías una sola rama mastercon un rico historial que encapsula todas las líneas de confirmación de características que alguna vez hubo, volviendo a la git initconfirmación desde el principio de los tiempos (ten en cuenta que evité específicamente usar el término " ramas "en la última parte de ese párrafo porque el historial en ese momento ya no es" ramas ", aunque el gráfico de compromiso sería bastante ramificado).

Odio esa idea;

Entonces te dolerá un poco, ya que estás trabajando en contra de la herramienta que estás utilizando. El gitenfoque es muy elegante y poderoso, especialmente en el área de ramificación / fusión; si lo hace bien (como se aludió anteriormente, especialmente con --no-ff) es a pasos agigantados superiour a otros enfoques (por ejemplo, el desorden de subversión de tener estructuras de directorio paralelas para ramas).

significa una maraña interminable e inevitable de la historia paralela del desarrollo.

Sin fin, paralelo, sí.

Innavigable, maraña - no.

Pero no veo qué alternativa tenemos.

¿Por qué no trabajar gittodos los días como lo hacen el inventor de sus colegas y el resto del mundo?

¿Tenemos alguna opción aquí además de fusionar constantemente ramas laterales en master con merge-commits? O, ¿hay alguna razón por la que constantemente usar merge-commits no sea tan malo como me temo?

No hay otras opciones; No es tan malo.


10

Aplastar una rama lateral a largo plazo te haría perder mucha información.

Lo que haría es tratar de volver a formar al maestro en la rama lateral a largo plazo antes de fusionar la rama lateral en el maestro . De esa manera, mantiene cada commit en master, mientras hace que el historial de commit sea lineal y más fácil de entender.

Si no pudiera hacer eso fácilmente en cada confirmación, lo dejaría ser no lineal , para mantener claro el contexto de desarrollo. En mi opinión, si tengo una fusión problemática durante la reubicación del maestro en la rama lateral, significa que la no linealidad tiene un significado en el mundo real. Eso significa que será más fácil entender lo que sucedió en caso de que necesite profundizar en la historia. También obtengo el beneficio inmediato de no tener que hacer un rebase.


: + por mencionar rebase. Si es o no el mejor enfoque aquí (personalmente no he tenido grandes experiencias con él, aunque no lo he usado mucho), definitivamente parece ser lo más parecido a lo que OP realmente parece querer, específicamente, un compromiso entre un volcado de historial fuera de servicio y ocultar completamente ese historial.
Kyle Strand

1

Personalmente, prefiero hacer mi desarrollo en una bifurcación, luego extraer solicitudes para fusionarlas en el repositorio principal.

Eso significa que si quiero volver a basar mis cambios en la parte superior del maestro, o aplastar algunas confirmaciones de WIP, puedo hacerlo totalmente. O simplemente puedo solicitar que toda mi historia se fusione también.

Lo que me gusta hacer es hacer mi desarrollo en una rama, pero frecuentemente rebase contra master / dev. De esa forma obtengo los cambios más recientes de master sin tener un montón de commits de fusión en mi rama, o tener que lidiar con una carga completa de conflictos de fusión cuando es el momento de fusionar con master.

Para responder explícitamente a su pregunta:

¿Tenemos alguna opción aquí además de fusionar constantemente ramas laterales en master con merge-commits?

Sí, puede fusionarlos una vez por rama (cuando la función o corrección está "completa") o si no le gusta que la fusión se confirme en su historial, simplemente puede hacer una fusión de avance rápido en master después de hacer una nueva versión final.


3
Lo siento, hago lo mismo, pero no veo cómo esto responde la pregunta: - /
Standback

2
Se agregó una respuesta explícita a su pregunta planteada.
Wayne Werner

Entonces, eso está bien para mis propios compromisos, pero yo (y mi equipo) nos ocuparemos del trabajo de todos . Mantener mi propia historia limpia es fácil; trabajar en un desordenado historial de desarrollo es el problema aquí.
Standback

¿Quieres decir que quieres requerir otros desarrolladores para ... qué? ¿No rebase y simplemente aplastar al fusionar? ¿O simplemente estaba publicando esta pregunta para quejarse de la falta de disciplina de sus compañeros de trabajo?
Wayne Werner

1
el rebase regular no es útil si varios desarrolladores están trabajando en esa rama de características.
Paŭlo Ebermann

-1

El control de revisión es basura en la basura.

El problema es que el trabajo en progreso en la rama de características puede contener una gran cantidad de "intentemos esto ... no, eso no funcionó, lo reemplazamos con eso" y todos los commits excepto el final "que" acaban contaminando la historia inútilmente.

En última instancia, se debe mantener el historial (parte de él podría ser útil en el futuro), pero solo se debe combinar una "copia limpia".

Con Git, esto se puede hacer bifurcando la rama de la característica primero (para mantener todo el historial), luego (interactivamente) rebase la rama de la rama de la característica del maestro y luego fusionando la rama rebaseada.


1
Solo para aclarar: lo que está diciendo es reescribir el historial en (una copia de) la rama de características, por lo que tiene un historial corto y limpio, lo que elimina todo el desarrollo inicial. Y luego, fusionar la rama reescrita en master es más simple y más limpio. ¿Te entendí correctamente?
Standback

1
Si. Y cuando te fusiones con master, solo será un avance rápido (suponiendo que hayas rebaseado recientemente antes de fusionarte).
DepressedDaniel

Pero, ¿cómo se guarda esta historia? ¿Dejas que la característica original se ramifique?
Paŭlo Ebermann

@ PaŭloEbermann Bingo.
DepressedDaniel

Bueno, como alguien más dijo en un comentario: las ramas son solo punteros en el gráfico de historia gity son locales. Lo que se nombra fooen un repositorio se puede nombrar baren uno diferente. Y están destinados a ser temporales: no desea saturar su repositorio con miles de ramas de características que deben mantenerse vivas para evitar perder el historial. Y necesitaría mantener esas ramas para mantener el historial como referencia, ya gitque eventualmente eliminará cualquier confirmación que ya no sea accesible por una rama.
cmaster
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.