summaryrefslogtreecommitdiff
path: root/es/2.md
blob: 4fc1c71e4abdcff37a9b6ff470576b9d776f1455 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
---
title: GIT
subtitle: Uso avanzado
license: CC-BY-NC-SA 4.0
author: Ekaitz Zarraga - ElenQ Technology
links-as-notes: true
lang: spanish
polyglossia-lang:
    name: spanish
how-to: pandoc -f markdown+smart -t beamer % -o pdf/2.pdf --pdf-engine=xelatex --template=./template.tex
...

## Licencia

- CC-BY-SA 4.0
- El contenido del documento ha sido adaptado de [**Pro Git (Scott Chacon, Ben
  Straub)**](https://git-scm.com/book/en/v2).

# Git en el servidor

## Git en el servidor: introducción

Para compartir el trabajo con el resto del equipo de desarrollo suele
utilizarse un servidor: siempre está disponible, facilita la gestión, etc.

En los servidores suelen utilizarse lo que se conoce como *bare repository*.
Éstos no tienen *working directory*: sólo tienen directorio `.git`.

- `git init --bare` construye un *bare repository*

> Los *bare repository* suelen nombrarse `loquesea.git` porque sólo contienen
> la propia carpeta `.git`, pero siguen siendo directorios, a pesar de su
> extensión.

## Protocolos

- **Local**: un repositorio que está en otro lugar del sistema de archivos de
  la máquina: `git clone /path/to/file`
- **HTTP**: tiene modos *smart* o *dumb*, el primero siendo capaz de gestionar
  permisos.
- **SSH**: es uno de los modos más utilizados. Necesita unas credenciales:\
  `git clone ssh://[<user>@]<host>/<project>.git`\
  También se puede usar el modo corto de SCP: `[<user>@]<host>:<project>.git`
- **Git**: Similar a SSH pero sin autenticación

## Protocolos — Un ejemplo

Mi [servidor](https://git.elenq.tech/):

- Está accesible desde la Web mediante `cgit`. Hay otras interfaces web como
  `gitweb`, que viene con el propio Git.
- Los proyectos públicos se pueden clonar sin problema con el protocolo Git.
- Para escribir, aporto acceso mediante SSH para tener control de permisos.

*En el libro se cuenta cómo montarlos*

## Workflows con Git

Git es un sistema distribuido, lo que permite muchos workflows distintos. Sin
embargo, hoy en día se usa sobre todo el workflow *centralizado*, debido al
éxito de las plataformas web que integran muchas partes del proceso de
desarrollo de software (incidencias, releases, etc) junto al control de
versiones: GitHub, GitLab, Gitea...

![Los clientes se sincronizan con el servidor](img/centralized_workflow.png){ height=140px }

## Mediante email

Muchos proyectos de software libre se basan en el email:

- `git format-patch <selector-de-commits>` crea parches a partir de los commits
  seleccionados
- `git send-email <parches>` envía los parches por email (requiere de
  configuración adicional)
- `git am <parches>` aplica los parches en el proyectos mediante commits
- `git apply <parches>` aplica los cambios de los parches sin commitear


# Herramientas avanzadas de Git

## Selectores de commits

Se pueden aplicar de forma generalizada en muchísimos comandos de Git.

- `git show <selector de commits>` para mostrar los objetos seleccionados.

> Ver la ayuda de `git show`

> Ver la ayuda de `git rev-parse` para leer la sintaxis de los selectores

## Selectores de commits — I

- **Hash SHA-1** el propio identificador de un commit es un selector válido.
  Como son muy largos pueden abreviarse (ver `git log --abbrev-commit`)

- **Cabezas de Ramas** (también llamadas *head*), son el nombre de la rama y se
  resuelven al commit al que la rama apunta. Ejemplo `git show <rama>`. Para
  ver cuál es el commit subyacente `git rev-parse <rama>`. *Cuidado, a veces
  funcionan como cabeza y otras como la rama completa*

- **El Reflog** es un registro histórico de los cambios aplicados en Git
  (similar al undo/redo de cualquier programa). Las entradas del Reflog pueden
  usarse como selectores. Es interesante porque pueden usar el tiempo:
  `HEAD@{yesterday}`

  - `git reflog` para listar las entradas del *reflog*.


## Selectores de commits — II

- **Ancestros** de los commits:

  - El acento circunflejo (`^`) indica el padre del selector utilizado. Por
    ejemplo: `git show HEAD^`. Los *merge commits* tienen muchos padres, y se
    puede elegir cuál de ellos con un número después del símbolo `^`.

  - La vírgula (`~`) sirve para obtener los ancestros del selector. Es similar
    al anterior, pero al añadir un número se elige la generación, no cuál de
    sus padres. `git show HEAD~2` elige el padre del padre mientras que
    `git show HEAD^2` elige el segundo padre.

> Usados por separado, son equivalentes

## Selectores de commits  — III

- **Rango de commits** indicados mediante `..` seleccionan una lista de commits
  entre los dos selectores indicados en los extremos.
  ![](img/double-dot.png){width=350px}\
  - `git log master..experiment` => D C\
  - `git log experiment..master` => F E

## Selectores de commits  — IV

- Los **puntos triples** actúan como un rango pero seleccionan los commits a
  los que se puede llegar desde las dos cabezas indicadas pero que las dos
  ramas no tienen en común.
  ![](img/double-dot.png){width=350px}\
  - `git log master...experiment` => F E D C\
  - Para ver de dónde vienen, `--left-right`:
    ```
    git log master...experiment --left-right
    < F
    < E
    > D
    > C
    ```

## Selectores de commits  — V

- **Puntos múltiples** pueden seleccionarse mediante `--not` o `^` (por
  delante) para retirar commits de un selector:
  - Los siguientes comandos son equivalentes:
    ```
    git log refA..refB
    git log ^refA refB
    git log refB --not refA
    ```
  - Los siguientes comandos obtienen los commits de `refA` y `refB` pero
    descartan los de `refC`:
    ```
    git log refA refB ^refC
    git log refA refB --not refC
    ```

## Staging interactivo

- `git add --interactive|-i` para gestionar el *stash* de forma interactiva
- `git add --patch|-p` para seleccionar los cambios manualmente

> Se pueden aplicar en muchos comandos diferentes. Son muy útiles.


## El stash

El *stash* es un saco donde guardar cambios. Los cambios no guardados en el
repositorio pueden guardarse ahí para poder realizar cambios en el repositorio
y luego recuperarlos, para que no se pierdan en el proceso.

- `git stash [push]` introduce los cambios en el stash y limpia el directorio
  de trabajo
- `git stash list` muestra las entradas del stash
- `git stash apply [<entrada del stash>]` aplica los cambios de la entrada del
  stash seleccionada (por defecto la última) en el *working directory*.
- `git stash drop [<entrada del stash>]` borra la entrada del stash
  seleccionada
- `git stash pop` = `git stash apply` + `git stash drop` (sólo aplica el `drop`
  si el `apply` ha ido bien)

Se usan mensajes *push* y *pop* porque las entradas del stash funcionan como
una pila (*stack*)

## El stash — opciones interesantes

- `git stash apply --index` introduce los cambios en la *staging area* (también
  se llama *index*). Por defecto no lo hace.
- `git stash --keep-index` los archivos indexados (en el *staging area*) no se
  mandan al stash, se mantienen en el *index*
- `git stash --include-untracked|-u` los archivos no-trackeados también se
  mandan al *stash*.
- `git stash --all|-a` los archivos ignorados (`.gitignore`) también se envían
  al *stash*
- `git stash --patch|-p` para seleccionar los cambios a enviar al *stash* de
  forma interactiva
- `git stash branch <rama> [<entrada del stash>]` crea una rama nueva y aplica
  el *stash* en ella. Si todo va bien borra la entrada del *stash* más tarde.

## Para limpiar el directorio

- `git stash --all` limpia el directorio enviando todo al *stash*. Luego se
  puede eliminar haciendo `drop`

- `git clean` hace esto mismo, sin pasar por el *stash*.
  - `-f` (*force*) es necesario porque si no sólo aplica un *dry-run* (una
    muestra de lo que pasaría sin llegar a aplicarlo)

> CUIDADO: aplicar siempre el `--dry-run|-n` en `git clean` primero por si
> acaso.

## Firmas

Para evitar commits falsos, se pueden firmar por GPG.

> GPG (Gnu Privacy Guard), es una implementación de PGP (Pretty Good Privacy)

Hay que configurar `user.signkey`

- `git commit -S` para firmar el commit
- `git tag -s` para firmar etiquetas
- `git merge --verify-signatures` comprueba las firmas en el merge
- `git tag -v` comprueba las firmas en una etiqueta

Si se utilizan firmas, todo el equipo de desarrollo debe utilizarlas.

## Búsquedas

- `git grep` para hacer búsquedas mediante expresiones regulares. Similar al
  comando `grep`, pero es más rápido y busca en el index y el histórico
- `git log` es muy potente para buscar en el histórico
  - `-S` funciones *pickaxe*
  - `-L` para ver la evolución de una línea de código o una función

## Reescribir la historia

- `git commit --amend` es como un mini-rebase
- `git rebase -i|--interactive <selector>` cambia los commits necesarios para
  llegar al selector.\
  Abre un editor de texto con los commits seleccionados y permite manipularlos
  con diversos comandos (los explica en el propio editor) que sirven para
  fusionar commits, cambiar los mensajes, editar los contenidos... En algunos
  casos se pausa para editar el contexto, permite continuar mediante
  `git rebase --continue` o cancelar con `--abort`. El propio proceso es
  bastante explicativo.

- `git filter-branch` es mucho más poderoso pero menos utilizado. Permite
  cambiar todos los commits de una rama

## Reset y checkout en profundidad — I

Para entenderlos en detalle es necesario comprender los estados de los archivos
en profundidad. Hay tres secciones disponibles:

1. HEAD: el snapshot del último commit
2. Index: el *staging area*, la propuesta del próximo commit
3. Working directory: el lugar donde realizamos los cambios

![Egoren arteko trantsizioak](img/reset-workflow.png){height=150px}

## Reset y checkout en profundidad — II

- `git reset` tiene tres modos:
  - `--soft`: Mover la rama apuntada por HEAD
  - `--mixed`: `--soft` + pasar los cambios al index
  - `--hard`: `--mixed` + aplica los cambios también en tu *working directory*

## Reset y checkout en profundidad — III

Empezando con un repositorio vacío:

![&nbsp;](img/reset-ex1.png)

## Reset y checkout en profundidad — IV

Pasar un archivo al *index*:

![&nbsp;](img/reset-ex2.png)

## Reset y checkout en profundidad — V

Escribirlo en el repositorio:

![&nbsp;](img/reset-ex3.png)

## Reset y checkout en profundidad — VI

Cambiar un archivo:

![&nbsp;](img/reset-ex4.png)

## Reset y checkout en profundidad — VII

Mandar los cambios al *index*

![&nbsp;](img/reset-ex5.png)

## Reset y checkout en profundidad — VIII

Añadir un commit nuevo:

![&nbsp;](img/reset-ex6.png)

## Reset y checkout en profundidad — IX

Añadir otro commit

![&nbsp;](img/reset-start.png)

## Reset y checkout en profundidad — X

`--soft`:

![&nbsp;](img/reset-soft.png)

## Reset y checkout en profundidad — XI

`--mixed` (es el que se aplica por defecto):

![&nbsp;](img/reset-mixed.png)

## Reset y checkout en profundidad — XII

`--hard`:

![&nbsp;](img/reset-hard.png)

## Reset y checkout en profundidad — XIII

Además de las tres opciones, se le pueden entregar archivos a `git reset`.

En este caso, el primer paso (mover el HEAD) no puede aplicarse del todo[^head]
pero el resto pueden aplicarse sin problemas. Esto habilita funcionamientos
interesantes:

- `git reset <archivo>` realmente hace\
  `git reset --mixed HEAD <archivo>`\
  1. ~~Mueve el HEAD~~
  2. Coloca el estado del archivo en el index
\
  Esto es: **quita el archivo del staging area**

[^head]: El HEAD no se puede mover a medias, o se mueve el repositorio completo
    o no se mueve.

## Reset y checkout en profundidad — XIV

A nivel gráfico:

![&nbsp;](img/reset-path1.png)

## Reset y checkout en profundidad — XV

En un ejemplo más complejo: `git reset <commit> -- <file>`

1. El HEAD no se puede mover
2. Se mueve el estado que `<file>` tenía en `<commit>` al index (`--mixed`).

![&nbsp;](img/reset-path3.png){height=220px}

## Reset y checkout en profundidad — XVI

Puede usarse este concepto para hacer un *squash* (combinar muchos commits en
uno) fácilmente [^rebase-interactive].

- `git reset --soft HEAD~<N>` lleva HEAD hacia atrás, manteniendo los cambios
  en el *index*. Después, se puede hacer `git commit` para aplicar los cambios
  que quedaron en el *index*, todos de una vez. En **un solo commit**.

[^rebase-interactive]: También se puede hacer con `git rebase --interactive|-i`

## Reset y checkout en profundidad — XVII

Gráficamente:

![&nbsp;](img/reset-squash-r1.png)

## Reset y checkout en profundidad — XVIII

Mover el HEAD manteniendo los cambios en el index:

![&nbsp;](img/reset-squash-r2.png)

## Reset y checkout en profundidad — XIX

Añadir el commit con los cambios que quedaron en el index

![&nbsp;](img/reset-squash-r3.png)

## Reset y checkout en profundidad — XX

`checkout` y `reset` son similares pero no son iguales:

- `git checkout` no manipula el HEAD, sino que lo reasigna a la referencia que
  se le indique, sin transformar la rama subyacente.

- `git checkout <branch>` y `git reset --hard <branch>` son casi iguales, pero
  el checkout no pisa el directorio de trabajo directamente.

- `git checkout <archivo>` **pisa el estado del archivo** en el *working
  directory*, parecido a `git reset --hard`. CUIDADO

A ambos se les puede enviar `--patch` para realizarlos por partes.

## Reset y checkout en profundidad — XXI

Efecto en el HEAD:

![&nbsp;](img/reset-checkout.png)


## Merges avanzados, gestión de conflictos — I

Antes de hacer un *merge* es interesante limpiar el *working directory* (`git
stash`), de este modo, si algo falla es fácil volver atrás.

- `git merge --abort` deshace el merge en caso de conflicto.

  > CUIDADO: Si hay cambios en el directorio de trabajo, no sabrá abortar.

## Merges avanzados, gestión de conflictos — II

En los conflictos, Git aporta 3 archivos:

1. Stage 1: el *common ancestor* (*base*), el commit al que se puede acceder
   subiendo por ambas ramas. Para verlo puede usarse `git merge-base`
2. Stage 2: Nuestra (*ours*) versión, lo que está en nuestra rama
3. Stage 3: Su (*theirs*) versión, lo que está en la rama que estamos
   mergeando.

- `git show :<stageN>:<file>` para ver los archivos
  - `:<stageN>:<file>` obtiene el hash del blob señalado

- `git diff` muestra los cambios:
    - `git diff -1|--base`
    - `git diff -2|--ours`
    - `git diff -3|--theirs`

## Merges avanzados, gestión de conflictos — III

- `git show :<stageN>:<file>` se puede utilizar para obtener el contenido de
  los diferentes archivos y aplicarlos directamente.

- `git merge-file` puede utilizarse para aplicar el merge a mano. Aplica
  algoritmos de merging.

## Merges avanzados, gestión de conflictos — IV

Git tiene herramientas mejores:

- `git checkout --conflict` te devuelve al estado del conflicto, con los
  marcadores, para deshacer los cambios que se hayan podido realizar por error
  al tratar de corregir el conflicto

- `git checkout --ours/--theirs` para ver uno o otro estado

- `git log --oneline --left-right --merge` facilita mostrar el contexto

- `git diff` muestra un *combined diff* cuando se aplica en un conflicto

- `git show -p|--patch` junto con `--cc` muestra el *combined diff*

## Merges avanzados, gestión de conflictos — V

Este es el aspecto de un *combined diff*:

```
diff --cc hello.rb
index 0399cd5,59727f0..0000000
--- a/hello.rb
+++ b/hello.rb
@@@ -1,7 -1,7 +1,7 @@@
  #! /usr/bin/env ruby

  def hello
-   puts 'hola world'
 -  puts 'hello mundo'
++  puts 'hola mundo'
  end
```

Muestra dos columnas con símbolos `+` y `-`, mostrando qué ha ocurrido en cada
lado del merge.

## Merges avanzados, gestión de conflictos — VI

Puede ser interesante usar `diff3` para los conflictos. Por defecto se usa
`merge`. `diff3` muestra una parte adicional en el conflicto, que muestra la
*base*.

```
<<<<<<< ours
  puts 'hola world'
||||||| base
  puts 'hello world'
=======
  puts 'hello mundo'
>>>>>>> theirs
```

- `git checkout --conflict=diff3` para mostrar el conflicto en 3 partes

- `git config --global merge.conflictstyle diff3` recomiendo configurarlo para
  que funcione así siempre


## Deshaciendo merges — I

Dos modos:

1. `git rest --hard HEAD~` (sobreescribe el histórico)
2. `git revert -m 1 HEAD` añade un commit que deshace los cambios. Esto da
   problemas a la hora de volver a combinar la rama.


## Deshaciendo merges — II

`git revert -m 1 HEAD` añade un commit que deshace los cambios. `^M` y `C6`
tienen los mismos contenidos, pero los cambios en `topic` pueden accederse
desde `master`, para Git, ambas están mergeadas. CUIDADO

![](img/undomerge-revert.png)

## Deshaciendo merges — III

Lo que es peor, si se siguen añadiendo cambios en `topic` y se intenta hacer un
merge nuevo, Git no añadirá los cambios previos, pensando que ya fueron
combinados (y lo fueron) pero sin saber que se deshicieron. CUIDADO

![](img/undomerge-revert2.png)

## Deshaciendo merges — IV

Para evitar el problema, hay que deshacer de nuevo el commit que se deshizo.
Esto puede hacerse con un nuevo `revert`.

![](img/undomerge-revert3.png)

Ahora todos los cambios de `topic` se verán en `master`.

## Preferencias en los merges

`git merge` tiene muchas opciones y estrategias:

- `-X` añade opciones. Por ejemplo `-Xours` resuelve los cambios a nuestro
  favor.

- `-s` selecciona estrategias. Por ejemplo `-s ours` en lugar de hacer un merge
  sólo se queda con nuestros cambios. Es interesante para engañar a Git.

Las estrategias no son lo mismo que las opciones. Las estrategias *seleccionan*
qué algoritmo de merge utilizar. Las opciones *configuran* el algoritmo.

## Subtree