Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Workflow Git

Aquesta entrada és una guia pràctica de git des de la línia de comanda, pensada per a qui comença i per a equips petits (2-3 persones). L’objectiu no és només llistar comandes, sinó donar la intuïció de com funciona git per sota, perquè deixi de semblar màgia.

Comença pels conceptes, recorre el cicle de treball habitual amb dos exemples complets (amb i sense conflictes) i acaba amb les operacions del dia a dia. Els dubtes més freqüents es recullen a Preguntes freqüents de Git. El focus és treballar directament sobre la branca principal, l’enfocament més senzill. Quan l’equip creix, calen branques, revisió de codi i validació automàtica: tot això es tracta a Git en equip, que continua on acaba aquest document.

Conceptes

En local

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 una fotografia de l’estat de tots els arxius en un moment donat, amb un missatge i un autor. S’identifica amb un hash, un codi com 17466a8 calculat a partir del seu contingut.

En l’àmbit local, tenim tres espais:

  • El working directory és el lloc on tens el teu codi. A l’arrel del teu working directory tindràs sempre una carpeta anomenada .git on es guarden els altres dos espais.
  • El staging area és una capsa on pots ficar i treure arxius. El proper commit serà una fotografia del contingut d’aquesta capsa, que no conté només els arxius que hi acabes de ficar: hi ha la versió de tots els arxius que git segueix. Després del commit, la capsa no es buida, sinó que queda igual que el commit que acabes de fer.
  • El repositori és el lloc on s’emmagatzemen els commits creats a partir del staging area. Podem revisar i recuperar qualsevol arxiu de qualsevol commit del passat. El commit actual d’un repositori es diu HEAD: és el marcador de “ets aquí” de git, que apunta al commit on et trobes ara mateix (normalment a través d’una branca) i respecte del qual actuen gairebé totes les comandes.

Per què hi ha un pas intermedi entre editar i fer commit? L’staging area és, de fet, l’esborrany del proper commit: et deixa triar amb cura quins canvis hi entren. Pots haver editat deu arxius i fer commit només de tres, deixant la resta per a un commit posterior. Així pots separar la feina en commits petits i coherents (veure commits atòmics) en lloc de barrejar-ho tot en un de sol.

Un repositori pot tenir branques (branches). Les branques permeten divergir de la línia principal de desenvolupament i fer feina sense afectar-la. En els exemples d’aquest document treballarem directament sobre la branca principal per simplificar, però les alternatives es presenten a estratègies de branques. El nom habitual de la branca principal és main: és el que fan servir per defecte github i gitlab, i el que git crea si configures init.defaultBranch (veure configuració). Repositoris antics poden tenir-la encara amb el nom històric master.

En remot

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”: pensa-hi com un magatzem de pur emmagatzematge, on ningú no edita arxius directament, sinó que només rep i serveix commits. Per això no li cal working directory.

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: és només un sobrenom convencional (un àlies) per a l’URL del remot, perquè no l’hagis de reescriure cada cop. No té res d’especial, li podries posar qualsevol altre nom. Un cop els hem emparellat, el codi NO se sincronitza automàticament. Tenim disponibles una sèrie d’operacions:

  • fetch: guarda en local els canvis remots (sense integrar-los).
  • merge: barreja els canvis remots que tenim en local amb els locals.
  • pull: és el mateix que fer un fetch i després un merge.
  • push: puja tots els canvis locals al repositori remot.

Una intuïció important: el teu repositori local és una còpia completa i independent, i el remot és només una altra còpia amb què decideixes sincronitzar-te quan vols. Com que res no es sincronitza sol, git fetch sempre és segur: només baixa els canvis remots a la teva còpia local, sense tocar la teva feina ni el teu working directory. Per això pots fer fetch sempre que vulguis per mirar què ha canviat (amb git log o git diff) abans de decidir si ho integres amb un merge.

Quan git compara la teva branca amb la del remot, fa servir tres paraules que veuràs sovint a git status. Totes surten de comparar els dos punters i comptar els commits que els separen:

  • ahead (al davant): tens commits locals que el remot encara no té. Els has de pujar amb push.
  • behind (al darrere): el remot té commits que tu no tens. Els has de baixar i integrar (pull, o fetch + merge).
  • diverged (han divergit): cada banda té commits propis que l’altra no té. Cal combinar-les amb un merge.

Git afegeix, no esborra

Una idea clau per entendre git: la majoria d’operacions només afegeixen informació, no en destrueixen. Quan fas un commit, git desa una nova fotografia (snapshot) de l’estat dels arxius i la suma a la història; els commits anteriors queden intactes. Les branques i les etiquetes són només punters a commits, i crear-les o moure-les no esborra res. Fins i tot quan “esborres” un arxiu (git rm) o resols un conflicte, el que passa és que es crea un commit nou que registra aquell canvi: el contingut antic continua al repositori, recuperable tornant a un commit anterior.

Aquest caràcter additiu és el que fa que git sigui segur: gairebé sempre pots tornar a un estat anterior, perquè res del passat no s’ha sobreescrit. Hi ha poques excepcions, que veurem a reescriure la història.

Configuració inicial

Eina git

Instal·la la teva eina git de línia de comanda al teu sistema operatiu.

Intenta executar-la:

git --version

Crear el repositori

Primer, has de crear un repositori buit a github o a gitlab.

Quan l’hagis creat, pots obtenir un URL del tipus:

https://github.com/usuari/repositori.git o bé https://gitlab.com/usuari/repositori.git.

Configuració

Les dues comandes següents calen per indicar el teu usuari i correu, que es guarden a l’activitat (els commits) del repositori:

git config --global user.email "elteu@correu.com"
git config --global user.name "elteunom"

El flag –global indica que els canvis apliquen a tots els repositoris. Si no s’indica, només aplica al repositori en què ens trobem.

És recomanable també indicar que les branques principals dels repositoris nous es diguin main (el nom estàndard actual), en lloc del nom històric master:

git config --global init.defaultBranch main

Si es volen ignorar els canvis fets al mode dels arxius, es pot fer:

git config --global core.filemode false

Si treballeu en sistemes operatius diferents (Windows, macOS, Linux), els finals de línia poden ser un problema: Windows fa servir CRLF i la resta LF, i això pot fer que git marqui arxius sencers com a modificats sense cap canvi real. La pràctica recomanada és normalitzar-los amb un arxiu .gitattributes a l’arrel del repositori:

* text=auto

Amb text=auto, git guarda sempre els arxius de text amb LF al repositori i els adapta al sistema de cada persona en treure’ls. Com que l’arxiu .gitattributes es versiona, la regla val per a tot l’equip, a diferència de la configuració per usuari git config --global core.autocrlf, que cadascú ha d’aplicar pel seu compte.

La comanda per esborrar una entrada és:

git config --global --unset <key>

Autenticació amb el remot

Per pujar canvis a github o gitlab cal autenticar-se. Des de fa anys aquestes plataformes ja no accepten la contrasenya del compte per a operacions git: cal fer servir un dels dos mecanismes següents.

Claus SSH (recomanat). Generes un parell de claus i registres la pública al teu compte (github/gitlab > Settings > SSH keys). No has de guardar cap credencial: l’autenticació la fa la clau. Després clones amb un URL SSH del tipus git@github.com:usuari/repositori.git.

ssh-keygen -t ed25519 -C "elteu@correu.com"
cat ~/.ssh/id_ed25519.pub   # enganxa aquest contingut al teu compte

HTTPS amb token. Si fas servir URLs https://, en comptes de contrasenya has d’introduir un personal access token (PAT) que generes al teu compte. Perquè git no te’l demani cada cop, fa servir un credential helper:

  • L’opció més segura és el gestor de credencials del sistema operatiu (per exemple git-credential-manager, o el keychain de macOS/Windows), que guarda el token xifrat. En molts sistemes ja ve configurat per defecte.
  • Alternativament, l’eina gh de github (gh auth login) gestiona l’autenticació per tu.

Evita git config --global credential.helper store: guarda el token en text pla a $HOME/.git-credentials. Pot servir per a una prova ràpida, però no és recomanable. Una opció intermèdia és una cache temporal en memòria (per defecte 900 segons):

git config --global credential.helper cache

Clonar el repositori

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 directory i la carpeta .git a dins.

Per mostrar l’estat:

1$ git status
On branch main
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)

Just després de clonar un repositori buit encara no hi ha cap branca (no hi ha cap commit), de manera que les comandes per llistar-les no mostren res. Un cop haguem fet el primer commit i push (ho veurem a continuació), ja podrem veure les branques locals i remotes:

1$ git branch
* main
1$ git branch -r
  origin/main

Gitignore

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:

  • cachés de dependències, com els continguts de /node_modules o /packages
  • codi compilat, com .o, .pyc, i .class
  • carpetes de sortida de compilació, com /bin, /out, o /target
  • arxius generats en temps d’execució com .log, .lock, o .tmp
  • arxius amagats del sistema, com .DS_Store o Thumbs.db
  • arxius de configuració personal dels IDE, com .idea/workspace.xml

Cada línia del .gitignore és un patró. Un exemple:

# arxius i carpetes a ignorar
*.log
build/
/secret.txt
temp/*.tmp
!important.log

Les regles bàsiques per llegir-lo:

  • *.log: el * substitueix qualsevol text, així que ignora tots els arxius acabats en .log, a qualsevol carpeta.
  • build/: la barra final indica una carpeta sencera (amb tot el que conté).
  • /secret.txt: la barra inicial ancora el patró a l’arrel del repositori (només aquell arxiu, no els que es diguin igual en subcarpetes).
  • temp/*.tmp: només els .tmp que hi ha dins de temp/.
  • !important.log: el ! fa una excepció, no ignoris aquest arxiu encara que una regla anterior (*.log) el tapi.
  • Les línies que comencen amb # són comentaris, i han d’anar en una línia pròpia (no al final d’un patró).

Flux de treball

Aquest és el flux de treball de referència que es detalla en els escenaris següents. Serveix com a guia ràpida de la sessió de treball habitual amb git: sincronitzar, resoldre conflictes si n’hi ha, treballar en local i pujar els canvis.

  1. Obtenir canvis remots, en dos passos:
    • Obtenir-los amb git fetch
    • Barrejar-los amb git merge
  2. Si el merge genera conflicte:
    • Editar arxius conflictius
    • Fer git add de les solucions
    • Fer git commit
  3. Fer canvis en local:
    • Modificar els arxius del working directory
    • Afegir-los al staging area (git add)
    • Fer commit (git commit)
  4. Pujar canvis locals:
    • Fer git push

Escenari base

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.

Afegir contingut

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
On branch main
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 main
No commits yet
Changes to be committed:
  (use "git rm --cached <file>..." to unstage)
  new file:   arxiu.txt

L’estat mostra que l’arxiu és al staging area (Changes to be committed). Ara ja podem crear el commit:

1$ git commit -m "primer commit"
[main (root-commit) 17466a8] primer commit
 1 file changed, 1 insertion(+)
 create mode 100644 arxiu.txt
1$ git status
On branch main
Your branch is based on 'origin/main', but the upstream is gone.
  (use "git branch --unset-upstream" to fixup)
nothing to commit, working tree clean

L’avís “the upstream is gone” no és cap error: la branca main està vinculada a origin/main, però al remot encara no hi ha cap branca perquè no hi hem pujat res. Desapareixerà amb el primer push.

El missatge del commit (-m) hauria de descriure el canvi de manera que un company l’entengui sense llegir el codi. Aquí fem servir un missatge simple per començar; a Git en equip veurem com escriure’n de bons.

També pots mirar el log, el lloc on es guarden els canvis del repositori:

1$ git log
commit 17466a86c10203150c8502e3aaedb8066c9d9b67 (HEAD -> main)
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 -> main) primer commit

El log mostra el commit “17466a8” amb el seu missatge. HEAD -> main indica que és el commit actual de la branca main.

Pujar contingut

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]      main -> main

Nou estat:

1$ git status
On branch main
Your branch is up to date with 'origin/main'.
nothing to commit, working tree clean

Ara l’estat confirma que estem sincronitzats amb origin/main (el repositori remot). Si mirem el log:

1$ git log --graph --oneline
* 17466a8 (HEAD -> main, origin/main) primer commit

Ara apareix origin/main al costat del commit, confirmant que tant el repositori local com el remot apunten al mateix commit.

Treballar amb un segon usuari

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:

  • github: cal afegir un col·laborador des del projecte > Settings > Manage access > Invite a collaborator.
  • gitlab: cal afegir un membre des del projecte > Settings > Members > Invite member. Selecciona “Maintainer” com a perfil.

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 main
Your branch is up to date with 'origin/main'.
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <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 descartar la modificació i tornar l’arxiu a la versió de l’últim commit, com suggereix el mateix git status:

2$ git restore arxiu.txt
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 main
Your branch is up to date with 'origin/main'.
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
  modified:   arxiu.txt
2$ git commit -m "afegim pregunta"
[main b475802] afegim pregunta
 1 file changed, 1 insertion(+)
2$ git status
On branch main
Your branch is ahead of 'origin/main' 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  main -> main
2$ git status
On branch main
Your branch is up to date with 'origin/main'.
nothing to commit, working tree clean

Ja tenim tots els canvis en remot. Mirem el log:

2$ git log --graph --oneline
* b475802 (HEAD -> main, origin/main, origin/HEAD) afegim pregunta
* 17466a8 primer commit

Com es veu, l’últim commit (b475802: “afegim pregunta”) es mostra com l’actual.

Rebre els canvis al primer usuari

Ara retornem al primer usuari (1$). Si mirem l’estat i el log:

1$ git status
On branch main
Your branch is up to date with 'origin/main'.
nothing to commit, working tree clean
1$ git log --graph --oneline
* 17466a8 (HEAD -> main, origin/main) primer commit

Com es pot veure, l’estat diu que està actualitzat amb origin/main (repositori remot), i al log no hi ha el nou commit que s’ha pujat al repositori remot (“afegim 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  main     -> origin/main

Si es mira l’estat i el log:

1$ git status
On branch main
Your branch is behind 'origin/main' 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 -> main) primer commit

L’estat ha canviat: ara ens diu que estem per darrere d’origin/main, i que hauríem de fer un git pull. El log, en canvi, sembla igual. El commit nou sí que és al repositori local, però git log només mostra la història de la branca on ets. Amb --all hi apareixen totes les branques, incloses les remotes:

1$ git log --graph --oneline --all
* b475802 (origin/main, origin/HEAD) afegim pregunta
* 17466a8 (HEAD -> main) primer commit

El fetch ha mogut origin/main fins al commit nou, i main s’ha quedat on era. Com que un pull és un fetch + merge, ara només ens cal el merge, que combinarà els commits d’origin/main amb els de main i actualitzarà el working directory amb el resultat.

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 main
Your branch is up to date with 'origin/main'.
nothing to commit, working tree clean
1$ git log --graph --oneline
* b475802 (HEAD -> main, origin/main, origin/HEAD) afegim pregunta
* 17466a8 primer commit

Escenari amb conflicte

Què passa quan dos desenvolupadors modifiquen la mateixa línia d’un arxiu? Git no pot decidir quina versió és correcta, i genera un conflicte que cal resoldre manualment. Vegem-ho pas a pas.

Partim de l’estat final de l’escenari base: tots dos usuaris són a afegim pregunta. Simulem la situació: els dos usuaris afegeixen una línia diferent al final de l’arxiu. El primer fa push sense problemes, però el segon trobarà l’error.

El primer fa:

1$ echo "segona línia 1" >> arxiu.txt 
1$ git add arxiu.txt 
1$ git commit -m "segona 1"
[main 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
   b475802..8bf099d  main -> main
1$ git status
On branch main
Your branch is up to date with 'origin/main'.
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"
[main eacb48e] segona 2
 1 file changed, 1 insertion(+)
2$ git push
To https://github.com/usuari/repositori.git
 ! [rejected]        main -> main (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 not
hint: have locally. This is usually caused by another repository pushing to
hint: the same ref. If you want to integrate the remote changes, use
hint: '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
   b475802..8bf099d  main     -> origin/main
2$ git status
On branch main
Your branch and 'origin/main' have diverged,
and have 1 and 1 different commits each, respectively.
  (use "git pull" if you want to integrate the remote branch with yours)
nothing to commit, working tree clean

Ens demana el pull, farem el merge (ja hem fet el fetch).

Important: fes sempre el merge amb el working directory net (sense canvis pendents de commit). Si tens canvis sense cometre, primer fes commit o guarda’ls amb git stash.

2$ git merge
Auto-merging arxiu.txt
CONFLICT (content): Merge conflict in arxiu.txt
Automatic merge failed; fix conflicts and then commit the result.
2$ git status
On branch main
Your branch and 'origin/main' have diverged,
and have 1 and 1 different commits each, respectively.
  (use "git pull" if you want to integrate the remote branch with 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 per a reflectir les dues versions, afegint tres delimitadors:

<<<<<<< HEAD
La versió local
=======
La versió remota
>>>>>>> nom_de_la_branca

En el nostre cas:

2$ cat arxiu.txt
Hola, món!
Com va tot?
<<<<<<< HEAD
segona línia 2
=======
segona línia 1
>>>>>>> refs/remotes/origin/main

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/main). 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”. Editem l’arxiu:

2$ cat arxiu.txt
Hola, món!
Com va tot?
segona línia 12

Després d’editar-lo, cal fer git add per marcar el conflicte com a resolt i ja podem fer commit i push:

2$ git add arxiu.txt
2$ git commit -m "resolt!"
[main 6fada39] resolt!
2$ git status
On branch main
Your branch is ahead of 'origin/main' 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  main -> main
2$ git status
On branch main
Your branch is up to date with 'origin/main'.
nothing to commit, working tree clean
2$ git log --graph --oneline
*   6fada39 (HEAD -> main, origin/main) resolt!
|\  
| * 8bf099d segona 1
* | eacb48e segona 2
|/  
* b475802 afegim pregunta
* 17466a8 primer commit

Es poden veure els dos commits en paral·lel, i com finalment hi ha un commit (6fada39) que resol el problema. El mateix historial, dibuixat com a graf:

gitGraph
    commit id: "primer commit"
    commit id: "afegim pregunta"
    branch origin/main
    commit id: "segona 1"
    checkout main
    commit id: "segona 2"
    merge origin/main id: "resolt!"

El graf dibuixa origin/main com una línia pròpia per distingir les dues bandes, però recorda que és només un punter, no una branca de feina separada.

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  main     -> origin/main
1$ git status
On branch main
Your branch is behind 'origin/main' 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 -> main) segona 1
* b475802 afegim pregunta
* 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 main
Your branch is up to date with 'origin/main'.
nothing to commit, working tree clean
1$ git log --graph --oneline
*   6fada39 (HEAD -> main, origin/main, origin/HEAD) resolt!
|\  
| * 8bf099d segona 1
* | eacb48e segona 2
|/  
* b475802 afegim pregunta
* 17466a8 primer commit

I ja tenim els dos repositoris sincronitzats després del conflicte.

Què és realment un conflicte

Un conflicte no vol dir que git hagi fallat: vol dir que git es nega a endevinar. Quan dues branques canvien zones diferents d’un arxiu (o arxius diferents), git pot aplicar els dos canvis i fa el merge automàticament. El conflicte apareix només quan les dues parts modifiquen la mateixa zona de manera incompatible: aleshores no hi ha cap manera mecànica de saber quina versió és la bona, perquè això depèn de la intenció de cada persona. git prefereix aturar-se i preguntar abans que triar a l’atzar i esborrar feina.

La intuïció clau: git fusiona línies de text, no entén el significat del codi. Per això pot passar també el contrari: un merge “sense conflictes” pot ser incorrecte si dues persones canvien parts diferents però que interactuen (per exemple, una canvia el nom d’una funció i l’altra la crida des d’un altre arxiu). Aquests són conflictes semàntics, que git no detecta: els veuràs quan el codi falli, no en fer el merge.

Com prevenir-los

Com que un conflicte neix de tocar el mateix codi en paral·lel, prevenir-los és sobretot una qüestió organitzativa, no tècnica:

  • Comunicació: acordar qui toca què evita que dues persones editin la mateixa zona alhora. És la mesura més efectiva.
  • Integrar sovint: sincronitzar amb freqüència (pull sovint, branques de poca durada) fa que les diferències siguin petites. Molts merges petits són més fàcils que un de gran i tardà.
  • Canvis petits i acotats: com més focalitzat és un canvi, menys probable és que xoqui amb el d’un altre.
  • Convencions compartides (format del codi, finals de línia): eviten conflictes espuris causats només per reformatats o per la barreja CRLF/LF (veure .gitattributes).

Com resoldre’ls

Quan el conflicte ja hi és, tens diverses vies:

  • Editar a mà els marcadors (<<<<<<<, =======, >>>>>>>), com hem fet a l’escenari, i decidir què es queda.
  • Triar una banda sencera, arxiu per arxiu: git restore --ours arxiu o git restore --theirs arxiu agafen la versió sencera d’una banda per a aquell arxiu, quan saps que substitueix completament l’altra (després cal fer git add per marcar-lo com a resolt).
  • Triar una banda per a tot el merge: git merge -X ours <branca> o git merge -X theirs <branca> resolen automàticament tots els conflictes a favor d’una banda (la teva o la de l’altra branca). Com que és una opció del merge, s’indica en llançar-lo: si ja ets dins d’un conflicte, fes abans git merge --abort i torna a fusionar amb l’opció. Compte: només decideixen els trossos en conflicte; la resta de canvis de totes dues bandes es combinen igualment (no substitueixen tota una branca per l’altra).
  • Eines de merge visuals: l’editor o IDE (per exemple VS Code) mostren les dues versions de costat i ajuden a combinar-les; git mergetool n’obre una.
  • Fer-se enrere: git merge --abort torna a l’estat previ si vols replantejar el merge.

Però la via més important sovint no és tècnica: si dues persones han canviat la mateixa lògica, la resolució correcta necessita que totes dues acordin què ha de fer el codi. git et pot ensenyar les dues versions, però només les persones saben quina és la combinació bona. Resoldre un conflicte és, en part, una conversa.

Fast-forward o commit de merge?

Hauràs notat que el merge de l’escenari base era un fast-forward (“Fast-forward”, sense crear cap commit), mentre que aquí ha calgut un commit de merge (el resolt!, amb la forma de diamant al log). Per què la diferència?

  • Fast-forward: passa quan la teva branca no ha avançat respecte del punt on parteix la remota. Aleshores integrar és trivial: git només llisca el punter de la branca cap endavant fins a l’últim commit remot. No cal cap commit nou perquè no hi ha res a combinar.
  • Commit de merge: passa quan les dues bandes han avançat des que es van separar (han divergit). git no pot limitar-se a moure un punter: ha de combinar les dues línies de feina i guardar el resultat en un commit nou, que té dos pares (un de cada banda). És aquest commit el que pot necessitar resoldre conflictes.

A l’escenari base, l’usuari 1 no havia fet cap commit propi, així que el merge només va lliscar main fins a afegim pregunta. La història queda lineal:

gitGraph
    commit id: "primer commit"
    commit id: "afegim pregunta"

A l’escenari amb conflicte, en canvi, cada usuari havia fet un commit a sobre de afegim pregunta, i les dues línies es van unir al commit de merge resolt!, com mostra el graf de l’escenari amb conflicte.

La intuïció: el fast-forward és “no t’has mogut, només m’actualitzo”; el commit de merge és “tots dos hem fet feina, ho ajunto i ho deixo registrat”. Per això un historial amb merges sovint té forma de diamant, i un de només fast-forwards és lineal.

Preferir el fast-forward

De vegades el fast-forward no és només el que passa, sinó el que vols que passi: que una branca no divergeixi mai i es limiti a seguir l’altra. El cas més clar és el teu main local respecte d’origin/main. Quan baixes la feina dels companys, gairebé mai no vols un commit de merge entre tots dos: vols que el teu local simplement es posi al dia. Si un dia no es pot fer fast-forward, vol dir que tots dos heu avançat pel vostre compte (heu divergit), i és el senyal per aturar-se a mirar què ha passat abans d’ajuntar res.

Aquesta preferència es pot fer explícita amb --ff-only: git integra només si és un fast-forward i, si no, s’atura en comptes de crear un commit de merge tot sol. Fixa’t que reacciona a la divergència, no al conflicte: que les branques hagin divergit no vol dir que el merge tingui conflictes (pot ser net o no), però --ff-only s’atura igualment, perquè el que vols garantir és que la branca només avanci en línia recta.

Si vols garantir aquest comportament, val més deixar-lo configurat per defecte que no pas recordar el flag cada vegada: amb git config --global pull.ff only (per als pull) i git config --global merge.ff only (per als merge), veure les preguntes freqüents, la comprovació és automàtica i git et frena sol quan apareix una divergència. Hi guanyes una història lineal, més fàcil de llegir i de desfer, i un punt de control que t’obliga a integrar de manera conscient (amb merge o rebase) en lloc de generar bombolles de merge sense voler.

Tot i així, és una configuració per defecte, no una imposició: quan puntualment vulguis un commit de merge, el pots forçar amb git pull --no-ff (o git merge --no-ff <branca>), que té prioritat sobre la configuració. Així el commit de merge passa a ser un acte deliberat en lloc d’una cosa que passa sola.

Operacions habituals

Més enllà del cicle bàsic de sincronitzar i pujar canvis, hi ha un conjunt d’operacions que faràs sovint, de més segures a més delicades: comparar canvis, esborrar arxius del control de versions, etiquetar versions, recuperar estats anteriors i desfer canvis ja publicats. Els exemples continuen des del final de l’escenari amb conflicte: tots dos usuaris són a resolt!.

Comparar canvis amb diff

git diff permet comparar dos commits locals o remots. Per exemple, per comparar el primer commit amb l’estat actual (resolt!):

1$ git diff 17466a8
diff --git a/arxiu.txt b/arxiu.txt
index cdff2f6..99d4a86 100644
--- a/arxiu.txt
+++ b/arxiu.txt
@@ -1 +1,3 @@
 Hola, món!
+Com va tot?
+segona línia 12

En aquest cas, ens diu que des del primer commit s’han afegit dues línies (“Com va tot?” i “segona línia 12”). Les línies amb + són les afegides i les que tenen -, les esborrades.

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 main local i el remot estan sincronitzats:

git diff main origin/main

Esborrar arxius del repositori

Per esborrar un arxiu o una carpeta del repositori:

git rm arxiu_vell.txt
git rm --cached arxiu_local.txt
git rm -r carpeta

L’opció --cached esborra l’arxiu del repositori però el manté al working directory. L’opció -r permet esborrar carpetes de forma recursiva. Després, cal fer git commit per confirmar l’eliminació.

Etiquetes

Les etiquetes (o tags) són 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

Tags anotats

L’exemple anterior crea un tag lleuger (lightweight). Git també permet crear tags anotats, que inclouen un missatge descriptiu, l’autor i la data. Són més informatius i recomanables per a versions o fites del projecte:

1$ git tag -a v1.0 -m "Primera versió estable amb funcionalitat bàsica"
1$ git push origin v1.0

Un bon missatge de tag descriu el contingut del desplegament. Evita repetir el nom del tag com a missatge:

# Poc informatiu
git tag -a v1.0 -m "v1.0"

# Informatiu: descriu el contingut
git tag -a v1.0 -m "Primera versió: API REST amb endpoints /users i /products"

Convencions de noms per a tags

ConvencióExempleÚs recomanat
Semantic versioningv1.0.0, v1.1.0, v1.1.1Projectes amb API pública o versionat formal
Sprint-basedsprint-1, sprint-2Projectes acadèmics o amb sprints definits
Data-based2026-03-15Menys recomanat (no descriu el contingut)

El semantic versioning (vMAJOR.MINOR.PATCH) és l’estàndard professional: incrementa MAJOR per canvis incompatibles, MINOR per noves funcionalitats, i PATCH per correccions.

Viatjar en el temps

Git permet recuperar l’estat de qualsevol commit del passat. És la veritable raó de ser dels repositoris: poder viatjar en el temps. Des de git 2.23 hi ha dues comandes específiques per fer-ho, que substitueixen els usos sobrecarregats del vell git checkout:

  • git restore: recupera el contingut d’arxius al working directory.
  • git switch: canvia de branca o es mou a un commit concret.

Per exemple, podem recuperar un arxiu concret. Tornar-lo a l’últim commit, o agafar-lo d’un commit determinat:

1$ git restore arxiu.txt
1$ git restore --source=17466a8 arxiu.txt

Podem moure tot el working directory a un commit del passat, per exemple, el “primer commit”:

1$ git switch --detach 17466a8
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”: el HEAD apunta directament a un commit en lloc d’apuntar a una branca. La intuïció: normalment el HEAD porta a la mà una etiqueta de branca, i és aquesta etiqueta la que subjecta els commits nous que vas fent; aquí, en canvi, estàs dret sobre un commit sense cap etiqueta a la mà, així que els commits que facis no tenen res que els retingui. Podem mirar i experimentar, però qualsevol commit que fem no pertanyerà a cap branca i es podria perdre. Si volguéssim conservar la feina feta aquí, podríem crear una branca amb git switch -c <nom-branca>.

Sempre podem retornar al main:

1$ git switch main
Previous HEAD position was 17466a8 primer commit
Switched to branch 'main'
Your branch is up to date with 'origin/main'.

I si volem recuperar cert commit al main, per exemple, el “primer commit”:

1$ git reset --hard 17466a8
HEAD is now at 17466a8 primer commit

En lloc del hash, també podem fer servir el nom d’una etiqueta, per exemple git reset --hard v1.0.

Si volem que el reset quedi al repositori remot:

1$ git push
To https://github.com/usuari/repositori.git
 ! [rejected]        main -> main (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. If you want to integrate the remote changes,
hint: use 'git pull' before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

Això falla perquè el push no avança la branca (no és fast-forward): estaríem reescrivint l’historial ja publicat, i git ho bloqueja. Podem ometre la comprovació forçant el push. Fes servir sempre --force-with-lease en lloc de --force: només força si ningú no ha pujat canvis nous al remot mentrestant, i així evita esborrar sense voler la feina d’un company.

1$ git push --force-with-lease
Total 0 (delta 0), reused 0 (delta 0)
To https://github.com/usuari/repositori.git
 + 6fada39...17466a8 main -> main (forced update)

Compte: reescriure l’historial d’una branca compartida (reset --hard més push forçat sobre main) afecta tothom qui ja tingui aquells commits. En un equip, fes-ho només si és imprescindible i avisant abans. Sobre branques publicades, és preferible desfer canvis amb un commit nou (git revert) que no pas reescriure el passat.

Després de fer això, si anem a l’altre repositori i fem fetch:

2$ git fetch
From https://github.com/usuari/repositori
 + 6fada39...17466a8 main     -> origin/main  (forced update)
2$ git log --graph --oneline
*   6fada39 (HEAD -> main, tag: v1.0) resolt!
|\  
| * 8bf099d segona 1
* | eacb48e segona 2
|/  
* b475802 afegim pregunta
* 17466a8 (origin/main, origin/HEAD) primer commit
2$ git status
On branch main
Your branch is ahead of 'origin/main' by 4 commits.
  (use "git push" to publish your local commits)
nothing to commit, working tree clean

Veiem que el repositori remot està al primer commit, però el local encara té els quatre commits posteriors: ens diu “your branch is ahead”. Si l’usuari 2 fes ara un push normal, git el rebutjaria; si el forcés, tornaria a pujar tot el que l’usuari 1 volia treure.

Això es pot resoldre canviant al commit remot en local:

2$ git reset --hard origin/main
HEAD is now at 17466a8 primer commit
2$ git status
On branch main
Your branch is up to date with 'origin/main'.
nothing to commit, working tree clean
2$ git log --graph --oneline
* 17466a8 (HEAD -> main, origin/main, origin/HEAD) primer commit

Desfer un commit publicat

El reset i el push forçat treuen commits de la història, i per això obliguen tots els companys a reajustar-se. Quan un commit ja és a main i el tenen altres persones, la manera segura de desfer-lo és afegir un commit nou que en faci el contrari. És el que fa git revert:

git revert a1b2c3d     # crea un commit nou que desfà els canvis d'a1b2c3d
git push               # push normal, sense forçar

La història només avança, i el commit erroni hi continua, desfet pel commit següent:

gitGraph
    commit id: "A"
    commit id: "B (erroni)"
    commit id: "C"
    commit id: "Revert B"

Com que no es reescriu res, el push és normal i els companys només han de fer pull, com amb qualsevol altre canvi. git revert obre l’editor amb un missatge proposat (“Revert …”); si vols acceptar-lo sense editar, afegeix --no-edit. Si algun commit posterior ha tocat les mateixes línies, el revert pot generar un conflicte: es resol igual que el d’un merge, i després es continua amb git revert --continue.

Reescriure la història

El reset i el push forçat de viatjar en el temps són l’excepció a la idea que git afegeix, no esborra.

Operacions que reescriuen

Hi ha un grup reduït d’operacions que, en canvi, modifiquen commits que ja existien (o canvien on apunta una branca de manera que abandona commits):

  • git commit --amend: substitueix l’últim commit per un de nou (per corregir-ne el missatge o afegir-hi un arxiu oblidat).
  • git reset (sobretot --hard): mou el punter de la branca a un commit anterior, deixant enrere els commits posteriors.
  • git rebase: torna a aplicar una sèrie de commits a sobre d’un altre punt, i en genera de nous en lloc dels originals.
  • git push --force / --force-with-lease: substitueix la història de la branca remota per la teva versió reescrita.

Tècnicament, fins i tot aquestes operacions són additives per sota: els commits antics no s’esborren a l’instant, sinó que queden sense cap punter que hi apunti i, fins que git no els recull (garbage collection), encara els pots recuperar amb git reflog (veure les preguntes freqüents). Però a efectes pràctics la branca ja no els mostra, i per això diem que “reescriuen la història”.

Per què reescriure-la?

Gairebé sempre, per deixar-la neta abans de compartir-la:

  • corregir l’últim commit (un missatge mal escrit, un arxiu que faltava);
  • treure un arxiu que no hi hauria d’haver estat (per exemple, una contrasenya);
  • agrupar diversos commits de feina a mitges en un de sol i coherent;
  • mantenir un historial lineal (rebase en lloc de generar commits de merge).

La regla d’or: reescriu només història que encara no has compartit. Un cop has fet push i altres persones tenen aquells commits, reescriure’ls els obliga a reconciliar la divergència i pot fer perdre feina. Per això el push forçat és una operació delicada, i quan cal fer-lo es prefereix --force-with-lease.

A més de ser una convenció, aquesta regla es pot convertir en una restricció tècnica: molts serveis git permeten prohibir la reescriptura d’història a les branques importants, de manera que el servidor rebutja qualsevol push que la reescrigui.

  • github i gitlab: les branques protegides inclouen una opció per no permetre force push (i bloquejar l’esborrat de la branca). Per defecte, en una branca protegida no es pot reescriure.
  • gitolite: distingeix el permís RW (només pots avançar la branca) del RW+ (a més, pots reescriure-la o fer-la enrere). Si no dones RW+ sobre main, ningú no la pot reescriure.

Regles d’or del treball en equip

  1. git pull (o fetch + merge) primer, sempre: sincronitza abans de treballar per minimitzar conflictes.
  2. Commits atòmics amb missatges llegibles: un commit, una idea; el teu company ha d’entendre el canvi sense llegir el diff.
  3. No fer push de codi trencat: si saps que el codi no funciona, no el pugis. Trenca el flux de treball de tot l’equip.
  4. Comunicar-se amb l’equip: avisa abans de fer canvis grans o resets. Un missatge ràpid evita molts conflictes.

Més enllà d’aquest flux

Aquest document fa servir el flux més senzill possible: dues persones que fan commit directament a main. Funciona per a grups petits amb bona comunicació, però els equips reals s’organitzen d’una altra manera: cada tasca en una branca pròpia, revisió del codi abans que arribi a main i validació automàtica de cada canvi.

El pas següent és Git en equip, que explica les estratègies de branques, les pull requests i merge requests, els fluxos que fa servir la indústria (GitHub flow, trunk-based development, Git flow), la connexió amb CI/CD i les bones pràctiques de commit.

Referències

Last change: , commit: c2e9839