Workflow Git
- Conceptes
- Configuració inicial
- Flux de treball
- Escenari base
- Escenari amb conflicte
- Operacions habituals
- Estratègies de branques
- Bones pràctiques
- Git i CI/CD
- Regles d’or del treball en equip
- FAQ
- Crear un repositori des d’una carpeta existent
git pullvsgit fetch+git mergegit pull: merge o rebase?- Vincular una branca local amb la remota (upstream)
- El push m’ha estat rebutjat (“fetch first”)
- Veure els canvis abans del commit
- Treure un arxiu afegit per error
- El .gitignore no ignora un arxiu
- Descartar tots els canvis locals
- Desfer o corregir l’últim commit
- Tornar a l’estat del remot
- Recuperar commits perduts (reflog)
- M’ha sortit un editor que no sé com tancar
- Referències
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 les operacions del dia a dia, i acaba amb les estratègies de treball en equip, les bones pràctiques i una FAQ amb els dubtes més freqüents. El focus és treballar directament sobre la branca principal, l’enfocament més senzill, però també es presenten alternatives com les feature branches i els fluxos d’aprovació per a equips més grans o projectes més complexos.
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 l’acte d’emmagatzemar un conjunt de canvis al repositori.
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. Un commit estarà format per tots els arxius ficats en aquesta àrea, i s’identifica amb un hash o resum. Quan es fa el commit, es buida.
- El repositori és el lloc on s’emmagatzemen els commits d’arxius provenents del stagging 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ò més endavant es presenten les estratègies de branques disponibles. 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.
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 la FAQ). 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) delRW+(a més, pots reescriure-la o fer-la enrere). Si no donesRW+sobremain, ningú no la pot reescriure.
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
ghde 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/HEAD -> origin/main
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_moduleso/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_StoreoThumbs.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.tmpque hi ha dins detemp/.!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.
- Obtenir canvis remots, en dos passos:
- Obtenir-los amb git fetch
- Barrejar-los amb git merge
- Si el merge genera conflicte:
- Editar arxius conflictius
- Fer git add de les solucions
- Fer git commit
- Fer canvis en local:
- Modificar els arxius del working directory
- Afegir-los al staging area (git add)
- Fer commit (git commit)
- 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
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 restore --staged <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
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; més endavant veurem com escriure bons missatges de commit.
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 “mantainer” 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 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 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
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/main, 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 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) 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.
Simulem la situació: els dos usuaris afegeixen una segona línia diferent a 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
17466a8..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
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 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.
$ 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!
<<<<<<< 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!
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
|/
* 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 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
* 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
|/
* 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:
git restore --ours arxiuogit restore --theirs arxiuquan saps que una versió substitueix completament l’altra (després cal fergit addper marcar-lo com a resolt). - Eines de merge visuals: l’editor o IDE (per exemple VS Code) mostren les dues versions de costat i ajuden a combinar-les;
git mergetooln’obre una. - Fer-se enrere:
git merge --aborttorna 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.
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.
Operacions habituals
Més enllà del cicle bàsic de sincronitzar i pujar canvis, hi ha un conjunt d’operacions que faràs sovint: etiquetar versions, recuperar estats anteriors, comparar canvis i esborrar arxius del control de versions.
Etiquetes
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
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 versioning | v1.0.0, v1.1.0, v1.1.1 | Projectes amb API pública o versionat formal |
| Sprint-based | sprint-1, sprint-2 | Projectes acadèmics o amb sprints definits |
| Data-based | 2026-03-15 | Menys 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
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
També podem fer referència a una etiqueta:
1$ 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. 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è 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
+ b475802...17466a8 main -> main (forced update)
Compte: reescriure l’historial d’una branca compartida (
reset --hardmés push forçat sobremain) 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
+ b475802...17466a8 main -> origin/main (forced update)
2$ git log --graph --oneline
* b475802 (HEAD -> main, tag: v1.0) afegim pregunta
* 17466a8 (origin/main) primer commit
2$ git status
On branch main
Your branch is ahead of 'origin/main' 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 restore <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/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
* 17466a8 (HEAD -> main, origin/main, origin/HEAD) primer commit
Comparar canvis amb diff
git diff permet comparar dos commits locals o remots. Per exemple, per comparar el HEAD amb el tag v1.0:
$ 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 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.txt
git rm --cached arxiu1.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ó.
Estratègies de branques
El flux descrit en aquest document és el més simple possible: tothom fa commit directament a main, sense crear branques. És adequat per a grups de 2 persones amb bona comunicació. Però existeixen altres estratègies que convé conèixer.
Abans d’entrar-hi, una intuïció que treu por a les branques: una branca és només un punter mòbil a un commit, no una còpia dels teus arxius. Crear-la és instantani i pràcticament gratis (git només desa un nom que apunta a un commit), i canviar de branca és moure el HEAD d’un punter a un altre. Quan fas un commit, la branca on ets simplement avança per apuntar al commit nou. Per això treballar amb branques és barat: no dupliques res, només poses etiquetes a punts de la història.
Commit directe a la branca principal
Cada membre fa commits i pushes directament a main. La integració és contínua i el feedback immediat.
Aquesta és la forma més extrema i simple del que la indústria anomena trunk-based development. Convé no confondre-les: en equips reals, el trunk-based no sol consistir a fer push directe sobre main, sinó a treballar en branques de molt curta durada (hores o un dia) que s’integren sovint a main darrere de validacions automàtiques (CI) i, habitualment, una revisió. El push directe que mostrem aquí és una simplificació pedagògica vàlida per a grups molt petits.
# Al principi de la sessió: sincronitzar
git pull origin main
# Treballar, fer commits
git add arxius_modificats
git commit -m "descripció del canvi"
git push origin main
Feature branches (branques per tasca)
Cada unitat de treball viu en una branca de curta durada (p. ex., feature/nova-funcio). La branca es fusiona a main quan la tasca és completa i provada.
# Crear una branca per la tasca
git switch -c feature/nova-funcio
# Treballar i fer commits
git add arxius_modificats
git commit -m "descripció del canvi"
# Quan la tasca és llesta, fusionar a main
git switch main
git pull origin main
git merge feature/nova-funcio
git push origin main
# Neteja
git branch -d feature/nova-funcio
Comparació
| Commit directe a main | Feature branches | |
|---|---|---|
| Complexitat git | Baixa | Mitjana |
| Risc de conflictes | Baix (si sincronitzen sovint) | Mig (si branques viuen > 3 dies) |
| Aïllament del treball | Cap: un error afecta tothom | Alt: errors queden a la branca |
| Recomanat per a 2 persones | Si | Opcional |
| Recomanat per a 3+ persones | Possible | Si |
Les branques de curta durada (2-3 dies) funcionen bé. Les de llarga durada (> 1 setmana) divergeixen molt de main i els merges es compliquen. La regla pràctica: una tasca, una branca, fusiona ràpid.
Flux d’aprovació (merge controlat)
Quan es treballa amb feature branches, el merge a la branca principal es pot fer de dues maneres: directament pel desenvolupador, o bé a través d’un flux d’aprovació on algú revisa els canvis abans d’integrar-los. Aquesta segona opció és la que materialitzen les pull requests (github) o merge requests (gitlab), i avui és la pràctica estàndard del treball en equip. La mecànica de fons, però, és independent de la plataforma: a continuació la veurem amb git pur per entendre què passa realment per sota.
La idea
El principi és senzill: separar qui proposa canvis de qui els accepta. Un desenvolupador treballa a la seva branca i, quan la tasca és llesta, demana a un responsable que revisi i integri els canvis a la branca principal. El responsable pot:
- Revisar el diff dels canvis proposats.
- Demanar correccions si cal.
- Acceptar i fer el merge quan tot és correcte.
Això afegeix una capa de revisió que millora la qualitat del codi i evita que canvis no revisats arribin a la branca principal.
El flux amb git pur
Aquest flux no requereix cap eina especial, només git i una convenció d’equip:
1. El desenvolupador treballa a la seva branca i la puja al repositori remot:
git switch -c feature/nova-funcio
# ... treballa, fa commits ...
git push -u origin feature/nova-funcio
El -u associa la branca local amb la remota (estableix l’upstream), de manera que les properes vegades n’hi ha prou amb git push.
2. Avisa al responsable (per correu, xat, o qualsevol canal) que la branca està llesta per revisar.
3. El responsable revisa els canvis des del seu repositori local:
git fetch origin
git log --oneline main..origin/feature/nova-funcio # quins commits hi ha
git diff main..origin/feature/nova-funcio # què canvien
4. Si cal demanar correccions, ho comunica al desenvolupador. El desenvolupador fa els canvis a la mateixa branca, commit i git push (ja no cal especificar el remot ni la branca gràcies al -u inicial). El responsable torna a revisar.
5. Quan els canvis són acceptats, el responsable fa el merge:
git switch main
git pull origin main
git merge origin/feature/nova-funcio
git push origin main
# Neteja de la branca remota
git push origin --delete feature/nova-funcio
6. El desenvolupador sincronitza i neteja en local:
git switch main
git pull origin main
git branch -d feature/nova-funcio
Permisos sobre branques
Per reforçar aquest flux, es poden configurar permisos d’escriptura sobre les branques del repositori remot. Per exemple, restringir l’escriptura a main a un sol usuari o a un grup reduït. D’aquesta manera, els desenvolupadors poden pujar les seves branques de feature però no poden fer push directament a main: només el responsable pot fer-ho després de revisar.
Això converteix una convenció organitzativa en una restricció tècnica. La majoria de servidors git (inclosos els auto-allotjats) permeten configurar aquests permisos. A github i gitlab això es fa amb les branch protection rules (regles de protecció de branca), que típicament es configuren per: exigir que els canvis arribin via pull/merge request, requerir un nombre mínim d’aprovacions, i no permetre el merge fins que els checks de CI passin. És la manera habitual d’aplicar aquest flux a la pràctica.
Quan val la pena
Per a equips de 2 persones amb molta comunicació, un procés formal pot semblar excessiu. Però com que github i gitlab ofereixen les pull/merge requests gairebé sense fricció, avui és habitual fer-les servir fins i tot en equips petits: combinades amb la validació automàtica (CI), són una de les pràctiques que més millora la qualitat del codi sense complicar gaire el flux de treball. A mesura que l’equip creix, passen de recomanables a imprescindibles.
Rebase per mantenir la branca al dia
Tant el merge com el rebase combinen feina, però d’una manera molt diferent: el merge conserva les dues històries i les uneix amb un commit nou amb dos pares; el rebase reescriu els teus commits perquè quedin com si els haguessis escrit a sobre d’una altra base.
La intuïció: imagina els teus commits enganxats amb post-it. El merge els deixa on són i hi pinta una línia que els connecta amb l’altra branca. El rebase els despenja, mou la base de la branca fins a un altre punt, i els torna a enganxar a sobre. Com que el resultat són commits “nous” (mateix contingut, hashes diferents), val la regla d’or de reescriure història: només sobre feina que encara no has compartit.
L’ús més habitual a la indústria és posar al dia la teva branca de feina amb main sense merges intermedis, abans de demanar-ne la revisió. Treballes a feature/nova-funcio i, mentrestant, main avança amb la feina d’altres companys. Si vas fent merge de main a la teva branca per estar al dia, deixes una bombolla de merge cada cop (“Merge branch ‘main’ into feature/nova-funcio”) i acabes amb una història enrevessada que costa de revisar.
El rebase ho evita:
git switch feature/nova-funcio
git fetch
git rebase origin/main
És com si despengessis els teus commits, mouguessis la base de la branca fins a l’últim main, i els tornessis a aplicar un a un per damunt. El resultat és una història lineal i neta, com si haguessis començat la feature ara mateix.
Per què val la pena: hi ha tres beneficis que es combinen.
- Revisió neta: el revisor veurà la teva feina com una llista de commits per damunt del
mainactual, sense soroll de manteniment. - Integració primerenca: desenvolupes contra el codi real d’ara, així que si
mainha canviat el nom d’una funció, ha modificat una signatura o ha introduït un bug, ho descobreixes immediatament en local i no el dia del merge. Compte: el rebase només resol conflictes textuals; els conflictes semàntics no els detecta, així que cal tornar a passar els tests després del rebase per confirmar que tot encaixa. - Conflictes més suaus: la quantitat total no canvia (vénen de tocar les mateixes línies), però l’experiència sí: els resols a poc a poc, amb el context d’un sol commit, i no en un únic xoc gran el dia del merge final. Quan el PR finalment s’integra, el merge a
mainés un fast-forward net.
Compte: després d’un rebase, els commits són nous. Si ja havies pujat la branca, el següent push caldrà fer-lo amb
--force-with-lease. Per això la regla d’or: si algun company ja treballa sobre la branca, parla-hi abans de reescriure-la.
Bones pràctiques
Missatges de commit
Un bon missatge de commit permet al teu company entendre el canvi sense llegir el diff. Dues convencions habituals:
Imperatiu simple (mínim de fricció):
git commit -m "add user validation"
git commit -m "fix null check in preprocessing"
git commit -m "remove unused imports"
Regla: comença amb un verb en imperatiu. Descriu què fa el commit, no el que has fet tu. Menys de 72 caràcters.
Conventional Commits (més estructurat):
git commit -m "feat: add predict endpoint"
git commit -m "fix: handle null age field"
git commit -m "test: add unit tests for validator"
git commit -m "docs: update README with examples"
git commit -m "chore: update dependencies"
Prefixes: feat (nova funcionalitat), fix (correcció), test (tests), docs (documentació), chore (manteniment), refactor (refactorització sense canvi de comportament).
| Imperatiu simple | Conventional Commits | |
|---|---|---|
| Fricció d’escriptura | Baixa | Mitjana (cal recordar el prefix) |
| Llegibilitat del log | Bona | Molt bona |
| Compatible amb eines de changelog | No | Si |
Commits atòmics
Un commit atòmic conté un sol canvi lògic. El codi ha de funcionar després de cada commit. Això contrasta amb fer un sol commit gran al final del dia amb tots els canvis barrejats.
Per què importa? Quan alguna cosa falla, els commits atòmics fan evident quin canvi ha causat el problema:
# Commit gran: on és el problema?
$ git log --oneline -1
a3f9b12 "add model, preprocessing, API and tests"
# Commits atòmics: el problema és al segon commit
$ git log --oneline -4
a3f9b12 "add predict endpoint"
def5678 "add logistic regression model" ← falla aquí
c72a3f8 "add preprocessing pipeline"
5e8d1a0 "add FastAPI skeleton"
Regla pràctica: fes commit de cada unitat lògica de treball per separat. Si el codi no està llest, utilitza git stash per guardar-lo temporalment sense fer commit.
Git i CI/CD
En molts projectes, el repositori git no és només un lloc on guardar codi: és el punt de connexió amb sistemes d’integració contínua (CI) i desplegament continu (CD). Entendre aquesta relació ajuda a treballar amb més cura.
Com funciona la connexió
Les plataformes com github i gitlab permeten configurar pipelines que reaccionen automàticament a events git:
- Push a una branca (p. ex.,
main): pot disparar l’execució de tests, anàlisi de codi, o compilació. Si els tests fallen, l’equip rep una notificació. - Push d’un tag: pot disparar un desplegament a producció o a un entorn de proves.
- Creació d’una pull/merge request: pot executar els tests sobre la branca abans que el merge sigui acceptat.
Això significa que cada git push té conseqüències més enllà del repositori: pot posar en marxa processos automàtics que afecten tot l’equip o fins i tot els usuaris finals.
Implicacions pràctiques
- Un push descuidat pot bloquejar el CI per a tot l’equip. Si fas push de codi que no compila o que trenca els tests, el pipeline fallarà i ningú podrà validar els seus canvis fins que es corregeixi.
- Els tags són decisions de desplegament. Un tag no és només una etiqueta: en molts projectes, crear un tag actualitza el producte en producció. Cal crear-los de forma deliberada.
- Testar en local abans de fer push. Executar els tests localment abans de pujar canvis evita cicles innecessaris de CI i estalvia temps a tot l’equip.
- L’ordre importa. Si el sistema CI reacciona a tags, cal assegurar-se que el commit ja és a la branca principal abans de crear el tag. L’ordre habitual és: primer
git pushdel commit, desprésgit pushdel tag.
git push origin main ← CI valida el codi
git tag -a v1.0 -m "versió 1"
git push origin v1.0 ← CI desplega a producció
Configuració del CI/CD
La majoria de plataformes git (github, gitlab, gitea, etc.) inclouen eines de CI/CD integrades. La configuració es fa típicament mitjançant arxius YAML al propi repositori, on es defineixen quins passos s’executen (tests, compilació, desplegament) i en resposta a quins events git (push, tag, merge request).
Regles d’or del treball en equip
git pull(ofetch+merge) primer, sempre – Sincronitza abans de treballar per minimitzar conflictes.- Commits atòmics amb missatges llegibles – Un commit, una idea; el teu company ha d’entendre el canvi sense llegir el diff.
- 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.
- Comunicar-se amb l’equip – Avisa abans de fer canvis grans o resets. Un missatge ràpid evita molts conflictes.
FAQ
Crear un repositori des d’una carpeta existent
Si ja tens una carpeta amb codi i vols pujar-la a un repositori remot buit (creat prèviament a github o gitlab):
cd la-meva-carpeta
git init -b main
git remote add origin https://gitlab.com/usuari/repositori.git
# afegir .gitignore abans del primer commit
git add .
git commit -m "commit inicial"
git push -u origin main
Alternativament, pots clonar primer el repositori buit i copiar-hi el contingut:
git clone https://gitlab.com/usuari/repositori.git
cd repositori
git switch -c main
# copiar arxius i afegir .gitignore
git add .
git commit -m "commit inicial"
git push -u origin main
git pull vs git fetch + git merge
git pull és equivalent a fer git fetch seguit de git merge. La diferència pràctica és que amb fetch + merge pots inspeccionar els canvis remots abans d’integrar-los (amb git log o git diff), mentre que pull ho fa tot de cop.
# Amb fetch + merge (més control)
git fetch
git log --oneline main..origin/main # veure què ha canviat
git merge
# Amb pull (més ràpid)
git pull origin main
git pull: merge o rebase?
Quan la teva branca local i la remota han divergit, git pull ha de combinar les dues històries, i pot fer-ho de dues maneres:
- Merge (comportament per defecte): crea un commit de merge que uneix les dues línies. Conserva l’historial exacte, però hi deixa “bombolles” de merge com les que hem vist a l’escenari amb conflicte.
- Rebase: reaplica els teus commits locals a sobre dels remots, deixant un historial lineal i més net. És la mateixa idea que Rebase per mantenir la branca al dia, aplicada al
pull.
Des de git 2.27, si no has triat estratègia, git pull mostra un avís demanant-te que en configuris una. Les opcions habituals:
git config --global pull.rebase true # pull sempre amb rebase (historial lineal)
git config --global pull.ff only # només permet pull si és fast-forward; si no, atura's
Amb pull.ff only (una opció prudent), si hi ha divergència git no fa res automàticament i decideixes tu si fer git merge o git rebase. Puntualment també pots forçar l’estratègia amb git pull --rebase o git pull --no-rebase.
Compte: no facis rebase de commits que ja hagis pujat i compartit, perquè en reescriu l’historial (com --amend o el push forçat).
Vincular una branca local amb la remota (upstream)
Quan fas git push o git pull sense especificar el remot ni la branca, git necessita saber a quina branca remota correspon la teva branca local. Aquesta associació s’anomena upstream o tracking branch.
Hi ha diverses maneres d’establir-la:
# Opció 1: al fer push per primera vegada, amb -u (o --set-upstream-to)
git push -u origin main
# Opció 2: explícitament, sense fer push
git branch --set-upstream-to=origin/main main
Un cop establerta l’associació, pots fer servir les comandes curtes:
git push # equivalent a git push origin main
git pull # equivalent a git pull origin main
Quan clones un repositori, git configura automàticament el tracking de la branca principal. Per això git pull funciona directament en un repositori clonat sense haver de fer -u primer.
Per veure quines branques locals tenen upstream configurat:
git branch -vv
La sortida mostra, entre claudàtors, la branca remota associada:
* main a1b2c3d [origin/main] últim missatge de commit
feature d4e5f6a [origin/feature] un altre missatge
El push m’ha estat rebutjat (“fetch first”)
Si en fer git push reps un error com aquest:
! [rejected] main -> main (fetch first)
error: failed to push some refs to '...'
vol dir que algú ha pujat canvis al remot abans que tu, i git no et deixa sobreescriure’ls. No és cap error greu: només cal que integris primer els canvis remots i tornis a pujar.
git pull # baixa i integra els canvis remots (fetch + merge)
# si hi ha conflictes, resol-los, fes git add i git commit
git push # ara sí
És exactament la situació de l’escenari amb conflicte. Recorda la regla d’or: sincronitza (pull) sovint per reduir les vegades que et trobes en aquesta situació.
Veure els canvis abans del commit
git diff # canvis al working directory (no afegits al staging)
git diff --staged # canvis ja afegits al staging area
git diff HEAD # tots els canvis respecte l'últim commit
Treure un arxiu afegit per error
Si encara no has fet push, pots treure l’arxiu de l’últim commit sense perdre els canvis locals:
git reset HEAD~1 --soft # desfà el commit, manté els canvis al staging
git restore --staged arxiu-erroni.txt # treu l'arxiu del staging
git commit -m "el commit correcte"
Si ja has fet push però vols treure l’arxiu del repositori sense esborrar-lo del disc:
git rm --cached arxiu-erroni.txt
# afegir-lo al .gitignore si cal
git commit -m "remove arxiu-erroni del repositori"
git push
El .gitignore no ignora un arxiu
El .gitignore només afecta els arxius que git encara no segueix (untracked). Si un arxiu ja s’havia afegit al repositori abans d’incloure’l al .gitignore, git continuarà seguint-lo i els seus canvis. Per deixar de seguir-lo (sense esborrar-lo del disc), treu-lo de l’índex amb --cached:
git rm --cached arxiu.txt
git commit -m "deixa de seguir arxiu.txt"
A partir d’aquí, amb l’arxiu ja al .gitignore, git l’ignorarà. Per a una carpeta sencera, fes servir git rm -r --cached carpeta.
Descartar tots els canvis locals
git reset --hard HEAD
Compte: això esborra tots els canvis no comesos. Si vols guardar-los temporalment per recuperar-los més tard:
git stash # guarda els canvis temporalment
# ... fas altres coses ...
git stash pop # recupera els canvis guardats
Desfer o corregir l’últim commit
Mentre no hagis fet push, l’últim commit es pot desfer o modificar sense afectar ningú.
Desfer el commit completament, mantenint els canvis al working directory:
git reset --soft HEAD~1 # els canvis queden al staging, llestos per tornar a fer commit
git reset HEAD~1 # els canvis queden al working directory, fora del staging
Desfer el commit i descartar els canvis (irreversible):
git reset --hard HEAD~1
Corregir l’últim commit (afegir arxius oblidats o canviar el missatge):
# Canviar només el missatge
git commit --amend -m "nou missatge corregit"
# Afegir arxius que faltaven al commit
git add arxiu-oblidat.txt
git commit --amend --no-edit # manté el missatge original
--amend reescriu l’últim commit. Si ja has fet push, no és recomanable fer-ho perquè reescriu l’historial i pot causar problemes als companys.
Tornar a l’estat del remot
Si vols descartar tots els canvis locals (commits, staging i working directory) i sincronitzar-te exactament amb el repositori remot:
git fetch origin
git reset --hard origin/main
Això mou el HEAD local al mateix commit que origin/main, descartant qualsevol commit local que no s’hagi pujat i qualsevol canvi pendent. Si tens arxius nous que no estan al repositori (untracked), no s’esborren. Per eliminar-los també:
git clean -fd # esborra arxius i carpetes untracked
Compte: ambdues operacions són irreversibles. Si no estàs segur de voler perdre els canvis, fes primer una còpia o un git stash.
Recuperar commits perduts (reflog)
Sovint la pots recuperar. git guarda un registre de tots els llocs on ha estat el HEAD (cada commit, reset, merge, switch…) durant un temps. Es consulta amb git reflog:
git reflog
8f151d7 (HEAD -> main) HEAD@{0}: reset: moving to HEAD~1
2be3461 HEAD@{1}: commit: feina que creia perduda
8f151d7 HEAD@{2}: commit (initial): primer commit
Cada línia és un estat anterior del HEAD. Quan localitzis el commit que vols (per exemple 2be3461, el que crèiem perdut), el pots recuperar tornant la branca a aquell punt:
git reset --hard 2be3461 # torna la branca a aquell commit
O, si només vols mirar-lo sense moure la branca, fes git switch --detach 2be3461.
Compte: el reflog és local i no és etern (git el va netejant; per defecte, les entrades caduquen al cap d’uns 90 dies). És una xarxa de seguretat, no un substitut de fer commits i push sovint.
M’ha sortit un editor que no sé com tancar
Si fas un git commit sense -m, o git necessita que escriguis un missatge (per exemple, en completar un merge), obre un editor de text. Per defecte sol ser vim o nano, i és fàcil quedar-s’hi encallat:
- vim: prem
Esc, escriu:wqi premEnterper desar i sortir (o:q!per sortir sense desar). - nano: prem
Ctrl+OiEnterper desar, i desprésCtrl+Xper sortir.
Per evitar-ho, posa el missatge directament a la comanda amb -m:
git commit -m "el missatge"
O configura un editor que et resulti més còmode:
git config --global core.editor "nano" # o "vim"
git config --global core.editor "code --wait" # VS Code