El flujo de trabajo que hasta ahora hemos visto es:
> git init # inicializa un repositorio
> git add <archivo> # da a conocer a git un archivo nuevo o un cambio en un archivo
> git status # resume los cambios actuales
> git commit -m "mensaje" # saca una foto instantánea del estado actual del proyecto
> git push # sube los cambios a un repositorio central (local o remoto)
> git log # muestra la bitácora del proyecto
(Por ahora ">" indicará la línea de comandos.)
Ahora veremos un modelo posible de colaboración, que iremos complicando poco a poco.
Para empezar, la instrucción que sirve para hacer una copia local de un repositorio remoto es:
> git clone https://github.com/Usuario/proyecto.git
En la instrucción de arriba, hemos considerado un proyecto remoto que está en GitHub, por ejemplo. Sin embargo, el comando puede usar en otras situaciones, por ejemplo, un proyecto remoto en una máquina a la que tenemos acceso con ssh
, o git
, o un proyecto en otro directorio.
Entre otras cosas que quedan configuradas cuando uno hace la clonación de un proyecto, es dónde se encuentra el proyecto original (origin
). Para verificar esto usamos:
> git remote -v
La situación que consideraremos es la siguiente: Alicia (Alice) y Beto (Bob) colaboran en un proyecto (el que acabamos de clonar). Ambos tienen la misma versión del código.
Alicia:
Alicia edita el archivo archivo.txt
, y hace algún cambio que le parece conveniente. Siguiendo el esquema de trabajo que describimos arriba, Alicia sube los cambios a su repositorio local con git add
y git commit
, y finalmente los sube al repositorio central: git push
.
Beto:
Beto, por su parte y de manera independiente, hace cambios al mismo archivo en que trabajó Alicia. De la misma manera que lo hizo Alicia, Beto actualiza su repositorio local (git add
y git commit
) y los sube al repositorio que comparten con git push
.
Sin embargo, como él editó el mismo archivo en el que Alicia hizo cambios, pero usando una versión atrasada que no incluye los cambios de Alicia, entonces git
detecta que hubo cambios divergentes entre la versión local de Beto, en la rama master
, y la del repositorio remoto origin/master
. Esto hace que git
no permita subir los cambios que propone Beto, hasta que Beto resuelva los conflictos que hayan surgido.
Ejercicio 1: Trabajando en grupos de dos al menos, traten de reproducir la situación descrita arriba. La pregunta concreta es qué significa eso del conflicto que se debe resolver.
Para llevar esto a cabo, una posibilidad es a partir de lo que hicimos la vez pasada. Sin embargo, hay ciertas sutilezas que tienen que ver con que git init
por default crea lo que se llama un repositorio non-bare (no vacío), y git no permite subir los cambios a un repositorio no vacío. Suponiendo que el repositorio que creamos anteriormente está en ~/Documentos/claseLuisDavid
, entonces:
(a) Primero crearemos un proyecto bare (vacío) a partir del anterior:
> git clone --bare -l ~/Documentos/claseLuisDavid repo_vacio
(Si inspeccionan el repositorio repo_vacio
verán que contiene los archivos que
normalmente se encuentran en el directorio escondido ".git", y nada más, o sea, no
tiene los archivos propios del proyecto.)
(b) A partir de este repositorio vacío, clonaremos a dos directorios independientes
(Alicia/
y Beto/
):
> git clone repo_vacio Alicia
> git clone repo_vacio Beto
(c) Desde el directorio Alicia/
hagan un cambio importante y súbanlo al repo
(git push
); traten ahora de hacer lo mismo desde Beto/
La segunda opción es clonar dos veces el repo que subieron a GitHub, a dos máquinas distintas o a dos directorios distintos. En este caso hay que adecuar la instrucción (b), usando la dirección del repo en GitHub. Otra sutileza es que el proyecto que clonan les debe permitir subir los cambios a ambos usuarios (en el caso de que hayan clonado a cuentas distintas o a máquinas distintas). Esto se puede configurar en los Settings
del repo en GitHub; para que esto funcione ambos usuarios deben estar dados de alta en GitHub.
(A la larga, la segunda opción es más útil que la primera.)
Para entender el problema, ejecutaremos la instrucción > git log --oneline lo que muestra el último cambio que hizo Beto. Por otro lado, para ver cómo está el repositorio remoto hacemos: > git log --oneline origin/master La segunda instrucción no muestra los cambios de Alicia. Esto muestra que el repositorio de Beto no está actualizado respecto al repositorio central.
La manera en que Beto debe resolver el conflicto es, pues, actualizando su versión local respecto al repositorio remoto. Esto se hace usando: > git fetch origin y para ver el estado del repositorio > git log --oneline origin/master
Entonces, para resolver el problema, Beto ha de implementar los cambios del repositorio remoto en su repositorio primero, ya que el de referencia es siempre el remoto (rama master
):
> git merge origin/master
lo que hace manifiesto, nuevamente, el conflicto.
Ejercicio 2: Edita el archivo con conflictos, y resuélvelos. Después haz git add
y git commit
. ¿Puedes subir (push) los cambios al repositorio central?
La moraleja de esto es: antes de hacer cualquier cambio, hay que mantenerse actualizado respecto al repositorio central. Esto se hace con la combinación git fetch
y git merge
cuando sea necesario, en particular, antes de subir algún cambio.
Una manera "corta" y combinada de hacer los dos pasos arriba descritos (git fetch
y git merge
) es con el comando:
> git pull
Ejercicio 3: Actualicen el directorio Alicia/
respecto a los últimos cambios hechos por Beto.
El concepto de una rama ("branch") en git provee una forma sencilla y eficiente de trabajar en nuevas ideas, o de colaborar en un proyecto común, evitando romper cosas que a priori ya funcionan.
Para empezar, listemos las ramas existentes de un proyecto (por ejemplo, en el directorio Alicia/
):
> git branch
o usando
> git branch -v
que brinda además el hash del último commit. Lo que esto indica es que existe únicamente la rama master
, que es la rama que se crea por default (y en algún sentido es la principal), y el asterisco indica que estamos trabajando en esa rama.
Para crear una nueva rama, ejecutamos:
> git branch <nombre_rama>
donde <nombre_rama>
es el nombre de la rama, que es más o menos arbitrario y flexible. Un
ejemplo es: git branch alicia
, que es el que usaré en este ejemplo; otra posibilidad podría ser git branch alicia/nuevaidea
.
Después de ejecutar alguna de estas instrucciones, git branch -v
nos informa que ambas ramas, master
y alicia
existen, ambas están en el (mismo) último commit, y el asterisco indica que estamos en la rama master
aún.
Para cambiarnos de rama, ejecutamos:
> git checkout <nombre_rama>
Nuevamente, existe un atajo para crear y cambiarnos de rama de un golpe: git checkout -b <nombre_rama>
.
Ejercicio 4: (a) Creen una rama, cámbiense a la nueva rama, y verifiquen que están en la
nueva rama. (b) Pregunta: la instrucción git status
, ¿da alguna información sobre en qué rama están?
Ejercicio 5: Hagan algunos cambios en el repositorio, tanto en los archivos que ya existen y creando un nuevo archivo y, una vez terminados, guarden este punto en la historia del desarrollo. ¿Cómo pueden verificar que el branch alicia
donde hicieron los cambios tiene al menos un commit más que el branch master
?
** Ejercicio 6:** (a) Cámbiense de rama a master
. ¿Qué pasó con los cambios? (b) Vuelvan a cambiarse al branch alicia
, y respondan la misma pregunta que antes.
El punto importante hasta el momento es que la historia de los dos branches (locales) ha divergido, y ambas historias están en ambas ramas.
Supongamos ahora que ya están satisfechos con los cambios que han hecho, después de muchas pruebas exhaustivas y otras fallidas (tal vez en otras ramas). Ahora queremos poner estos cambios en la rama master
. Para esto, primero nos cambiamos a master
, que es la rama a donde queremos pasar los cambios, y después hacemos un merge
, o sea, fundimos las dos historias:
> git checkout master
> git merge <nombre_rama>
Ejercicio 7: Pasen los cambios de alicia
a master
.
Ejercicio 8: Ya que los cambios que hicimos en alicia
están en master
, borren la rama alicia
. Para esto, la instrucción git branch -d alicia
es particularmente útil.
Ejercicio 9: Usen LearnGitBranching para jugar con esto y ver gráficamente qué significan las ramas y la divergencia de las historias. En esta misma liga hay otros tutoriales que pueden ser interesantes.
Nota: Vale la pena incluir un archivo LICENSE.md
, donde definen la manera en que uno puede usar el contenido de su proyecto. Para código, se recomienda la licencia MIT.
master
, y quieres que éstos se evalúen para usarse en el proyecto colaborativo central, debes primero actualizar tu fork (en GitHub) con tus cambios. Para esto, subirás tus cambios a tu repositorio en GitHub usando git push origin <mi_rama>
, donde <mi_rama>
es la rama donde hiciste los cambios que quieres subir.hub pull-request
en la línea de comandos si tienes instalado hub
(una versión extendida de git
que funciona específicamente con GitHub).Ejercicio 10: Ensaya lo anterior con el repositorio creado por otro compañero en GitHub. Ciertamente todo esto por ahora es de prueba, pero será vital de ahora en adelante :-)
Para evitar que GitHub te esté pidiendo tu usuario y contraseña todo el tiempo, es necesario usar claves de SSH (SSH keys). En Linux y Mac, el procedimiento es como sigue. NB: No hacer esto desde una máquina/cuenta pública.
Utiliza el comando ssh-keygen
para generar claves nuevas. Te pedirá que pongas una clave ("passphrase"); esta clave tendrás que ponerla sólo una vez por sesión.
;ssh-keygen
Esto generará claves en el directorio escondido ~/.ssh
en tu directorio hogar:
;ls ~/.ssh
github_rsa github_rsa.pub id_rsa id_rsa.pub known_hosts
Ahora, copia la clave pública; esto se puede hacer a mano (copiando el contenido del archivo id_rsa.pub
), o usando un programa. E.g. en Mac, puedes usar pbcopy
para copiar el contenido de un archivo al clipboard:
;pbcopy < ~/.ssh/id_rsa.pub
Ahora, hay que dar de alta las claves en GitHub:
Settings
(arriba, del lado derecho)SSH keys
Add SSH key
Ya deberías poder hacer transacciones con GitHub sin que te pida tu usuario cada vez.
Una vez más, no hagas esto en un máquina o cuenta pública.
Normalmente hacemos un fork de un repositorio de interés en GitHub, es decir, una copia del repositorio en tu propia cuenta de GitHub.
Al hacer git clone ...
de tu fork, git
provee un remote (es decir, un nombre para un repositorio remoto) llamado origin
, que apunta a tu fork. Esto lo podemos ver con
; git remote -v
origin https://github.com/dpsanders/MetodosNumericosAvanzados.git (fetch) origin https://github.com/dpsanders/MetodosNumericosAvanzados.git (push)
Vemos que origin
apunta al fork del repositorio MetodosNumericosAvanzados
en mi cuenta de GitHub (con usuario dpsanders
).
Sin embargo, para mantener actualizado nuestro fork con respecto al repositorio original, debemos darle a conocer a git
que también existe dicho repositorio. Si hacemos
;git help remote
GIT-REMOTE(1) Git Manual GIT-REMOTE(1) NNAAMMEE git-remote - Manage set of tracked repositories SSYYNNOOPPSSIISS _g_i_t _r_e_m_o_t_e [-v | --verbose] _g_i_t _r_e_m_o_t_e _a_d_d [-t <branch>] [-m <master>] [-f] [--[no-]tags] [--mirror=<fetch|push>] <name> <url> _g_i_t _r_e_m_o_t_e _r_e_n_a_m_e <old> <new> _g_i_t _r_e_m_o_t_e _r_e_m_o_v_e <name> _g_i_t _r_e_m_o_t_e _s_e_t_-_h_e_a_d <name> (-a | --auto | -d | --delete | <branch>) _g_i_t _r_e_m_o_t_e _s_e_t_-_b_r_a_n_c_h_e_s [--add] <name> <branch>... _g_i_t _r_e_m_o_t_e _s_e_t_-_u_r_l [--push] <name> <newurl> [<oldurl>] _g_i_t _r_e_m_o_t_e _s_e_t_-_u_r_l _-_-_a_d_d [--push] <name> <newurl> _g_i_t _r_e_m_o_t_e _s_e_t_-_u_r_l _-_-_d_e_l_e_t_e [--push] <name> <url> _g_i_t _r_e_m_o_t_e [-v | --verbose] _s_h_o_w [-n] <name>... _g_i_t _r_e_m_o_t_e _p_r_u_n_e [-n | --dry-run] <name>... _g_i_t _r_e_m_o_t_e [-v | --verbose] _u_p_d_a_t_e [-p | --prune] [(<group> | <remote>)...] DDEESSCCRRIIPPTTIIOONN Manage the set of repositories ("remotes") whose branches you track. OOPPTTIIOONNSS -v, --verbose Be a little more verbose and show remote url after name. NOTE: This must be placed between remote and subcommand. CCOOMMMMAANNDDSS With no arguments, shows a list of existing remotes. Several subcommands are available to perform operations on the remotes. _a_d_d Adds a remote named <name> for the repository at <url>. The command git fetch <name> can then be used to create and update remote-tracking branches <name>/<branch>. With -f option, git fetch <name> is run immediately after the remote information is set up. With --tags option, git fetch <name> imports every tag from the remote repository. With --no-tags option, git fetch <name> does not import tags from the remote repository. With -t <branch> option, instead of the default glob refspec for the remote to track all branches under the refs/remotes/<name>/ namespace, a refspec to track only <branch> is created. You can give more than one -t <branch> to track multiple branches without grabbing all branches. With -m <master> option, a symbolic-ref refs/remotes/<name>/HEAD is set up to point at remote's <master> branch. See also the set-head command. When a fetch mirror is created with --mirror=fetch, the refs will not be stored in the _r_e_f_s_/_r_e_m_o_t_e_s_/ namespace, but rather everything in _r_e_f_s_/ on the remote will be directly mirrored into _r_e_f_s_/ in the local repository. This option only makes sense in bare repositories, because a fetch would overwrite any local commits. When a push mirror is created with --mirror=push, then git push will always behave as if --mirror was passed. _r_e_n_a_m_e Rename the remote named <old> to <new>. All remote-tracking branches and configuration settings for the remote are updated. In case <old> and <new> are the same, and <old> is a file under $GIT_DIR/remotes or $GIT_DIR/branches, the remote is converted to the configuration file format. _r_e_m_o_v_e, _r_m Remove the remote named <name>. All remote-tracking branches and configuration settings for the remote are removed. _s_e_t_-_h_e_a_d Sets or deletes the default branch (i.e. the target of the symbolic-ref refs/remotes/<name>/HEAD) for the named remote. Having a default branch for a remote is not required, but allows the name of the remote to be specified in lieu of a specific branch. For example, if the default branch for origin is set to master, then origin may be specified wherever you would normally specify origin/master. With -d or --delete, the symbolic ref refs/remotes/<name>/HEAD is deleted. With -a or --auto, the remote is queried to determine its HEAD, then the symbolic-ref refs/remotes/<name>/HEAD is set to the same branch. e.g., if the remote HEAD is pointed at next, "git remote set-head origin -a" will set the symbolic-ref refs/remotes/origin/HEAD to refs/remotes/origin/next. This will only work if refs/remotes/origin/next already exists; if not it must be fetched first. Use <branch> to set the symbolic-ref refs/remotes/<name>/HEAD explicitly. e.g., "git remote set-head origin master" will set the symbolic-ref refs/remotes/origin/HEAD to refs/remotes/origin/master. This will only work if refs/remotes/origin/master already exists; if not it must be fetched first. _s_e_t_-_b_r_a_n_c_h_e_s Changes the list of branches tracked by the named remote. This can be used to track a subset of the available remote branches after the initial setup for a remote. The named branches will be interpreted as if specified with the -t option on the _g_i_t _r_e_m_o_t_e _a_d_d command line. With --add, instead of replacing the list of currently tracked branches, adds to that list. _s_e_t_-_u_r_l Changes URL remote points to. Sets first URL remote points to matching regex <oldurl> (first URL if no <oldurl> is given) to <newurl>. If <oldurl> doesn't match any URL, error occurs and nothing is changed. With _-_-_p_u_s_h, push URLs are manipulated instead of fetch URLs. With _-_-_a_d_d, instead of changing some URL, new URL is added. With _-_-_d_e_l_e_t_e, instead of changing some URL, all URLs matching regex <url> are deleted. Trying to delete all non-push URLs is an error. _s_h_o_w Gives some information about the remote <name>. With -n option, the remote heads are not queried first with git ls-remote <name>; cached information is used instead. _p_r_u_n_e Deletes all stale remote-tracking branches under <name>. These stale branches have already been removed from the remote repository referenced by <name>, but are still locally available in "remotes/<name>". With --dry-run option, report what branches will be pruned, but do not actually prune them. _u_p_d_a_t_e Fetch updates for a named set of remotes in the repository as defined by remotes.<group>. If a named group is not specified on the command line, the configuration parameter remotes.default will be used; if remotes.default is not defined, all remotes which do not have the configuration parameter remote.<name>.skipDefaultUpdate set to true will be updated. (See ggiitt--ccoonnffiigg(1)). With --prune option, prune all the remotes that are updated. DDIISSCCUUSSSSIIOONN The remote configuration is achieved using the remote.origin.url and remote.origin.fetch configuration variables. (See ggiitt--ccoonnffiigg(1)). EEXXAAMMPPLLEESS +o Add a new remote, fetch, and check out a branch from it $ git remote origin $ git branch -r origin/HEAD -> origin/master origin/master $ git remote add staging git://git.kernel.org/.../gregkh/staging.git $ git remote origin staging $ git fetch staging ... From git://git.kernel.org/pub/scm/linux/kernel/git/gregkh/staging * [new branch] master -> staging/master * [new branch] staging-linus -> staging/staging-linus * [new branch] staging-next -> staging/staging-next $ git branch -r origin/HEAD -> origin/master origin/master staging/master staging/staging-linus staging/staging-next $ git checkout -b staging staging/master ... +o Imitate _g_i_t _c_l_o_n_e but track only selected branches $ mkdir project.git $ cd project.git $ git init $ git remote add -f -t master -m master origin git://example.com/git.git/ $ git merge origin SSEEEE AALLSSOO ggiitt--ffeettcchh(1) ggiitt--bbrraanncchh(1) ggiitt--ccoonnffiigg(1) GGIITT Part of the ggiitt(1) suite Git 11/26/2014 GIT-REMOTE(1)
vemos que hay un subcomando add
de remote. Así que hacemos
; git remote add upstream https://github.com/lbenet/MetodosNumericosAvanzados.git
El nombre usual que se le asigna al repositorio original es upstream
.
Ahora podemos actualizar nuestro repositorio local con
; git pull upstream master
From https://github.com/lbenet/MetodosNumericosAvanzados * branch master -> FETCH_HEAD
Already up-to-date.
* [new branch] master -> upstream/master
(el cual jala la rama master
del repositorio apuntado por upstream
).
Ahora al hacer
; git push
To https://github.com/dpsanders/MetodosNumericosAvanzados.git 24f59f5..ebcb8b0 master -> master
empuja los cambios a origin
, o sea, a nuestro propio fork.