Aquesta entrada descriu un flux de treball simplificat per treballar en equips de desenvolupament petits, des de la línia de comanda i sense crear branques.
git és una eina que permet treballar amb repositoris de codi locals i remots.
Els canvis sobre els arxius d’un repositori s’agrupen en commits. Un commit és l’acte d’emmagatzemar un conjunt de canvis al repositori.
En l’entorn local, tenim tres espais:
Un repositori pot tenir branques (branches). Les branques permeten divergir de la línia principal de desenvolupament i fer feina sense afectar-la. En aquest document no les utilitzarem, per tal de tenir un flux de treball més senzill. Però cal saber que master és el nom de la branca que git crea per defecte quan es crea un repositori.
Opcionalment, podem tenir repositoris remots, i comunicar-nos per pujar o baixar coses. Un repositori remot és com un de local, però no té working directory. Se’n diu “bare”.
Ens interessa tenir-ne de remots per poder tenir un lloc on compartim el codi amb la resta de membres del grup. El flux de treball serà treballar en local i compartir en remot la feina, un cop la tenim enllestida.
A un repositori local podem emparellar un de remot. El nom que git dona al principal repositori remot és origin. Un cop els hem emparellat, el codi NO se sincronitza automàticament. Tenim disponibles una sèrie d'operacions:
Reproduirem l'escenari base, amb dos usuaris i un repositori remot compartit. Els dos usuaris fan canvis en local i els sincronitzen amb el repositori remot.
Instal·la la teva eina git de línia de comanda al teu sistema operatiu.
Intenta executar-la: “git --version”:
$ git --version
git version 2.20.1
Primer, has de crear un repositori buit a github o a gitlab.
Quan l’hagis creat, pots obtenir un URL del tipus:
Les següents tres comandes són interessants per treballar: les dues primeres, calen per indicar el teu usuari i correu que es guarda a l’activitat del repositori. El tercer serveix per guardar les credencials el primer cop que s’introdueixen. Compte: es guarden en text pla a $HOME/.git-credentials
.
$ git config --global user.email "elteu@correu.com"
$ git config --global user.name "elteunom"
$ git config --global credential.helper store
A partir d'ara es parla de github, però les comandes són exactament les mateixes canviant l'URL pel de gitlab.
Clonarem el repositori buit que hem creat a github:
1$ git clone https://github.com/usuari/repositori.git
Això crea una carpeta "repositori" amb el working directori i la carpeta .git a dins.
Per mostrar l'estat:
1$ git status
On branch master
No commits yet
nothing to commit (create/copy files and use "git add" to track)
També pots mirar l'aparellament amb el repositori remot:
1$ git remote -v
origin https://github.com/usuari/repositori.git (fetch)
origin https://github.com/usuari/repositori.git (push)
Pots veure les branques locals i remotes així:
1$ git branch
* master
1$ git branch -r
origin/HEAD -> origin/master
origin/master
Per afegir contingut, cal preparar el commit. Primer, crea o copia al working directory tot el contingut que vulguis.
Imaginem que afegim un arxiu així:
$ echo "Hola, món!" > arxiu.txt
Si mostres l'estat:
1$ git status
No commits yet
Untracked files:
(use "git add <file>..." to include in what will be committed)
arxiu.txt
nothing added to commit but untracked files present (use "git add" to track)
Els missatges expliquen que tenim un arxiu fora del control del repositori (untracked). Per afegir-lo:
1$ git add arxiu.txt
1$ git status
On branch master
No commits yet
Changes to be committed:
(use "git rm --cached <file>..." to unstage)
new file: arxiu.txt
Ara explica que tenim un nou arxiu (new file) al staging area. Per afegir-lo al commit:
1$ git commit -m "primer commit"
[master (root-commit) 17466a8] primer commit
1 file changed, 1 insertion(+)
create mode 100644 arxiu.txt
1$ git status
On branch master
Your branch is based on 'origin/master', but the upstream is gone.
(use "git branch --unset-upstream" to fixup)
nothing to commit, working tree clean
També pots mirar el log, el lloc on es guarden els moviments del repositori:
1$ git log
commit 17466a86c10203150c8502e3aaedb8066c9d9b67 (HEAD -> master)
Author: elteunom <elteu@correu.com>
Date: Sun Apr 26 19:39:54 2020 +0200
primer commit
També hi ha un format en una línia d'aquesta comanda:
1$ git log --graph --oneline
* 17466a8 (HEAD -> master) primer commit
Està dient que tenim un commit "17466a8" i que és l'actual.
Cal fer un push:
1$ git push
Enumerating objects: 3, done.
Counting objects: 100% (3/3), done.
Writing objects: 100% (3/3), 216 bytes | 216.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0)
To https://github.com/usuari/repositori.git
* [new branch] master -> master
Nou estat:
1$ git status
On branch master
Your branch is up to date with 'origin/master'.
nothing to commit, working tree clean
Ara ens diu que estem sincronitzats amb origin/master. Nou log:
1$ git log --graph --oneline
* 17466a8 (HEAD -> master, origin/master) primer commit
Ara ens apareix origin/master.
Simularem que tenim un segon usuari amb un altre repositori.
Per simplificar, els dos usuaris poden compartir credencials de github. Alternativament (recomanable), crea tants usuaris com calgui, i fes que siguin col.laboradors del projecte. Això es pot fer tant a github com a gitlab:
Creem un segon workspace directory. Per distingir els dos, tindrem dos prompts diferents: 1$ i 2$.
2$ git clone https://github.com/usuari/repositori.git
Cloning into 'repositori'...
remote: Enumerating objects: 3, done.
remote: Counting objects: 100% (3/3), done.
remote: Total 3 (delta 0), reused 3 (delta 0), pack-reused 0
Unpacking objects: 100% (3/3), done.
Ara, modificarem l'arxiu i mirem l'estat:
2$ echo "Com ba tot?" >> arxiu.txt
2$ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: arxiu.txt
no changes added to commit (use "git add" and/or "git commit -a")
Ens diu que hi ha un arxiu modificat (modified), però no està al staging area.
Ens hem equivocat! Volíem escriure "Com va tot?". Podríem editar l'arxiu un altre cop i esborrar la nova línia, però aprofitarem per recuperar l'arxiu abans de fer la modificació. Com que no hem fet el commit, es pot recuperar així:
2$ git reset --hard
HEAD is now at 17466a8 primer commit
2$ echo "Com va tot?" >> arxiu.txt
Ara, afegim l'arxiu al staging area i fem el commit:
2$ git add arxiu.txt
2$ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
modified: arxiu.txt
2$ git commit -m "afegim pregunta"
[master b475802] afegim pregunta
1 file changed, 1 insertion(+)
2$ git status
On branch master
Your branch is ahead of 'origin/master' by 1 commit.
(use "git push" to publish your local commits)
nothing to commit, working tree clean
Ara, afegim els canvis al repositori remot:
2$ git push
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Writing objects: 100% (3/3), 263 bytes | 263.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0)
To https://github.com/usuari/repositori.git
17466a8..b475802 master -> master
2$ git status
On branch master
Your branch is up to date with 'origin/master'.
nothing to commit, working tree clean
Ja tenim tots els canvis en remot. Mirem el log:
2$ git log --graph --oneline
* b475802 (HEAD -> master, origin/master, origin/HEAD) afegim pregunta
* 17466a8 primer commit
Com es veu, l'últim commit (b475802: "afegim pregunta") es mostra com l'actual.
Ara retornem al primer usuari (1$). Si mirem l'estat i el log:
1$ git status
On branch master
Your branch is up to date with 'origin/master'.
nothing to commit, working tree clean
1$ git log --graph --oneline
* 17466a8 (HEAD -> master, origin/master) primer commit
Com es pot veure, l'estat diu que està actualitzat amb origin/master (repositori remot), i al log no hi ha el nou commit que s'ha pujat al repositori remot ("primera pregunta").
Per poder veure'l, cal baixar-se els canvis del remot. Això es pot fer amb un fetch:
1$ git fetch
remote: Enumerating objects: 5, done.
remote: Counting objects: 100% (5/5), done.
remote: Total 3 (delta 0), reused 3 (delta 0), pack-reused 0
Unpacking objects: 100% (3/3), done.
From https://github.com/usuari/repositori
17466a8..b475802 master -> origin/master
Si es mira l'estat i el log:
1$ git status
On branch master
Your branch is behind 'origin/master' by 1 commit, and can be fast-forwarded.
(use "git pull" to update your local branch)
nothing to commit, working tree clean
1$ git log --graph --oneline
* 17466a8 (HEAD -> master) primer commit
El log no ha canviat, perquè fetch no integra els canvis al repositori, però l'estat sí: ara ens diu que estem per darrere d'origin/master, i que hauríem de fer un git pull. Com que un pull és un fetch + merge, farem només el merge.
El merge intentarà barrejar automàticament el contingut remot recuperat i el que tenim al working directory.
1$ git merge
Updating 17466a8..b475802
Fast-forward
arxiu.txt | 1 +
1 file changed, 1 insertion(+)
El merge ha funcionat: ha afegit una nova línia. Com es veu, aquesta operació és immediata: no necessita anar al repositori remot. L'arxiu s'ha actualitzat, i l'estat i el log estan igualats amb els de l'usuari 2:
1$ cat arxiu.txt
Hola, món!
Com va tot?
1$ git status
On branch master
Your branch is up to date with 'origin/master'.
nothing to commit, working tree clean
1$ git log --graph --oneline
* b475802 (HEAD -> master, origin/master) afegim pregunta
* 17466a8 primer commit
Les etiquetes (o tags) és una forma senzilla d'identificar un cert commit o estat dins del repositori. Es poden posar locals i pujar-les en remot. A github, quan es pugen en remot, s'associen a una release que permet descarregar un arxiu empaquetat. A gitlab també es pot fer, però la secció es diu "tags".
Per afegir un tag al commit actual i mostrar-lo:
1$ git tag v1.0
1$ git tag
v1.0
Per pujar una etiqueta:
1$ git push origin v1.0
Total 0 (delta 0), reused 0 (delta 0)
To https://github.com/usuari/repositori.git
* [new tag] v1.0 -> v1.0
Si volem veure les etiquetes des de l'altre repositori:
2$ git fetch
From https://github.com/usuari/repositori
* [new tag] v1.0 -> v1.0
2$ git tag
v1.0
El checkout ens permet recuperar qualsevol working directori per a un commit. És la veritable raó de ser dels repositoris: poder viatjar en el temps.
Per exemple, podem recuperar un arxiu concret d'un commit. De l'últim (-- significa que no indiquem el commit), o d'un concret:
1$ git checkout -- arxiu.txt
1$ git checkout 17466a8 arxiu.txt
Podem recuperar tot un commit, per exemple, el "primer commit":
1$ git checkout 17466a8
Note: checking out '17466a8'.
You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.
If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -b with the checkout command again. Example:
git checkout -b <new-branch-name>
HEAD is now at 17466a8 primer commit
1$ git status
HEAD detached at 17466a8
nothing to commit, working tree clean
1$ cat arxiu.txt
Hola, món!
Com es veu, tornem al contingut de l'arxiu abans del segon commit.
El problema d'aquesta comanda és que estem en estat "detached HEAD": no tenim commit actual i no podríem treballar amb el staging area.
Sempre podem retornar al master:
1$ git checkout master
Switched to branch 'master'
Your branch is up to date with 'origin/master'.
I si volem recuperar cert commit al master, per exemple, el "primer commit":
1$ git reset --hard 17466a8
HEAD is now at 17466a8 primer commit
També podem fer referència a una etiqueta:
1$ get reset --hard v1.0
Si volem que el reset quedi al repositori remot:
1$ git push
To https://github.com/usuari/repositori.git
! [rejected] master -> master (non-fast-forward)
error: failed to push some refs to 'https://github.com/usuari/repositori.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
Això falla perquè no es pot fer un push d'un commit antic: git sempre comprova que siguin commits més nous. Podem ometre aquesta comprovació amb el paràmetre --force:
1$ git push --force
Total 0 (delta 0), reused 0 (delta 0)
To https://github.com/usuari/repositori.git
+ b475802...17466a8 master -> master (forced update)
Després de fer això, si anem a l'altre repositori i fem fetch:
2$ git fetch
From https://github.com/usuari/repositori
+ b475802...17466a8 master -> origin/master (forced update)
2$ git log --graph --oneline
* b475802 (HEAD -> master, tag: v1.0) afegim pregunta
* 17466a8 (origin/master) primer commit
2$ git status
On branch master
Your branch is ahead of 'origin/master' by 1 commit.
(use "git push" to publish your local commits)
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: arxiu.txt
no changes added to commit (use "git add" and/or "git commit -a")
Veiem que el repositori remot està al primer commit, però el local està al segon: ens diu "your branch is ahead".
Això es pot resoldre canviant al commit remot en local:
2$ git reset --hard origin/master
2$ git status
On branch master
Your branch is up to date with 'origin/master'.
nothing to commit, working tree clean
2$ git log --graph --oneline
* 17466a8 (HEAD -> master, origin/master, origin/HEAD) primer commit
Una altra eina molt útil és git diff. Ens permet comparar dos commits locals o remots.
Per exemple, per comparar el HEAD amb el tag v1.0 cal utilitzar un paràmetre amb el commit:
$ git diff v1.0
diff --git a/arxiu.txt b/arxiu.txt
index 0027e65..b4b62f7 100644
--- a/arxiu.txt
+++ b/arxiu.txt
@@ -1,2 +1,2 @@
Hola, món!
-Com va tot?
+segona línia 13
En aquest cas, ens diu que el canvi del tag v1.0 al HEAD és que s'ha esborrat una línia ("Com va tot?") i s'ha afegit una altra ("segona línia 13").
Si volem comparar dos commits, afegirem dos paràmetres. Per exemple, si hem fet prèviament un git fetch, podem utilitzar aquesta comanda per veure si el master local i el remot estan sincronitzats:
$ git diff master origin/master
Una altra comanda útil és la d'esborrar arxius o carpetes. Les següents comandes permeten esborrar un arxiu o una carpeta. L'opció --cached permet esborrar només del repositori, no del workind directory. L'opció -r permet, per carpetes, fer la feina de forma recursiva.
$ git rm arxiu.txt
$ git rm --cached arxiu1.txt
$ git rm -r carpeta
Alguns tipus de fitxers no haurien de ser part del repositori de codi, i es poden indicar afegint un patró a l'arxiu .gitignore que hi ha a les carpetes. En general, seria millor no afegir certs tipus d'arxius:
/node_modules
o /packages
.o
, .pyc
, i .class
/bin
, /out
, o /target
.log
, .lock
, o .tmp
.DS_Store
o Thumbs.db
.idea/workspace.xml
El conflicte més típic entre dos usuaris és que modifiquin la mateixa línia d'un arxiu. Quan els dos vulguin fer un push, el segon no podrà fer-lo amb un missatge d'error.
El primer fa:
1$ echo "segona línia 1" >> arxiu.txt
1$ git add arxiu.txt
1$ git commit -m "segona 1"
[master 8bf099d] segona 1
1 file changed, 1 insertion(+)
1$ git push
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Writing objects: 100% (3/3), 262 bytes | 262.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0)
To https://github.com/usuari/repositori.git
17466a8..8bf099d master -> master
1$ git status
On branch master
Your branch is up to date with 'origin/master'.
nothing to commit, working tree clean
I el segon:
2$ echo "segona línia 2" >> arxiu.txt
2$ git add arxiu.txt
2$ git commit -m "segona 2"
[master eacb48e] segona 2
1 file changed, 1 insertion(+)
2$ git push
To https://github.com/usuari/repositori.git
! [rejected] master -> master (fetch first)
error: failed to push some refs to 'https://github.com/usuari/repositori.git'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally. This is usually caused by another repository pushing
hint: to the same ref. You may want to first integrate the remote changes
hint: (e.g., 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
Com es veu, es diu que cal fer primer pull, ja que no pots fer push si no has integrat els canvis remots al teu repositori.
Provem de fer-ho. Primer el fetch:
2$ git fetch
remote: Enumerating objects: 5, done.
remote: Counting objects: 100% (5/5), done.
remote: Total 3 (delta 0), reused 3 (delta 0), pack-reused 0
Unpacking objects: 100% (3/3), done.
From https://github.com/usuari/repositori
17466a8..8bf099d master -> origin/master
2$ git status
On branch master
Your branch and 'origin/master' have diverged,
and have 1 and 1 different commits each, respectively.
(use "git pull" to merge the remote branch into yours)
nothing to commit, working tree clean
Ens demana el pull, farem el merge (ja hem fet el fetch). És important fer sempre el merge en un repositori on no hi hagi canvis pendents del commit.
2$ git merge
Auto-merging arxiu.txt
CONFLICT (content): Merge conflict in arxiu.txt
Automatic merge failed; fix conflicts and then commit the result.
$ git status
On branch master
Your branch and 'origin/master' have diverged,
and have 1 and 1 different commits each, respectively.
(use "git pull" to merge the remote branch into yours)
You have unmerged paths.
(fix conflicts and run "git commit")
(use "git merge --abort" to abort the merge)
Unmerged paths:
(use "git add <file>..." to mark resolution)
both modified: arxiu.txt
no changes added to commit (use "git add" and/or "git commit -a")
Ja tenim el conflicte a arxiu.txt. Això es tradueix en el fet que git modifica l'arxiu del conflicte, afegint informació perquè el desenvolupador sàpiga què ha de barrejar manualment:
2$ cat arxiu.txt
Hola, món!
<<<<<<< HEAD
segona línia 2
=======
segona línia 1
>>>>>>> refs/remotes/origin/master
En aquest punt, ens podríem fer enrere (no ho farem) fins a l'estat anterior del merge fent git merge --abort
.
Ens diu que teníem "segona línia 2" (HEAD) i que al remot tenim "segona línia 1" (refs/remotes/origin/master). Per resoldre el conflicte manualment, hem d'editar aquest arxiu i decidir què fem, esborrant les línies delimitadores (<,=,>) i tot el que no ens interessi.
En el nostre cas, decidim que ni una línia ni l'altra: "segona línia 12". Per tant, l'arxiu quedarà:
2$ cat arxiu.txt
Hola, món!
segona línia 12
2$ git add arxiu.txt
2$ git commit -m "resolt!"
[master 6fada39] resolt!
2$ git status
On branch master
Your branch is ahead of 'origin/master' by 2 commits.
(use "git push" to publish your local commits)
nothing to commit, working tree clean
2$ git push
Enumerating objects: 10, done.
Counting objects: 100% (10/10), done.
Delta compression using up to 8 threads
Compressing objects: 100% (2/2), done.
Writing objects: 100% (6/6), 523 bytes | 523.00 KiB/s, done.
Total 6 (delta 0), reused 0 (delta 0)
To https://github.com/usuari/repositori.git
8bf099d..6fada39 master -> master
2$ git status
On branch master
Your branch is up to date with 'origin/master'.
nothing to commit, working tree clean
2$ git log --graph --oneline
* 6fada39 (HEAD -> master, origin/master) resolt!
|\
| * 8bf099d segona 1
* | eacb48e segona 2
|/
* 17466a8 primer commit
Es poden veure els dos commits en paral·lel, i com finalment hi ha un commit (6fada39) que resol el problema.
Ara tornem al repositori 1:
1$ git fetch
remote: Enumerating objects: 10, done.
remote: Counting objects: 100% (10/10), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 6 (delta 0), reused 6 (delta 0), pack-reused 0
Unpacking objects: 100% (6/6), done.
From https://github.com/usuari/repositori
8bf099d..6fada39 master -> origin/master
1$ git status
On branch master
Your branch is behind 'origin/master' by 2 commits, and can be fast-forwarded.
(use "git pull" to update your local branch)
nothing to commit, working tree clean
1$ git log --graph --oneline
* 8bf099d (HEAD -> master) segona 1
* 17466a8 primer commit
1$ git merge
Updating 8bf099d..6fada39
Fast-forward
arxiu.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
1$ git status
On branch master
Your branch is up to date with 'origin/master'.
nothing to commit, working tree clean
1$ git log --graph --oneline
* 6fada39 (HEAD -> master, origin/master, origin/HEAD) resolt!
|\
| * 8bf099d segona 1
* | eacb48e segona 2
|/
* 17466a8 primer commit
I ja tenim els dos repositoris sincronitzats després del conflicte.
Aquest és el flux de treball proposat:
A continuació, es mostren una sèrie de captures de pantalla de com es veu el projecte a github.