La lliçó 06-06 va tancar el bloc tècnic del curs amb una constatació incòmoda: l'Ana, en Bruno i la Carla dominen l'eina, però no han acordat com treballar plegats. I la primera d'aquestes preguntes sense resposta és també la més concreta: quan algú acaba un canvi, com el proposa? L'envia directament a main i ho diu pel xat? I què passa si aquella persona no té permís d'escriptura al repositori?

Aquest cas ha deixat de ser hipotètic. L'empresa ha decidit publicar gestor-tasques com a projecte obert a git.exemple.cat/equip/gestor-tasques, i ja ha aparegut el primer col·laborador de fora: Diego Rueda, desenvolupador d'una consultora que fa servir l'aplicació en un client. En Diego ha trobat una errada —la llista de tasques no es reordena quan en marques una com a completada— i la vol arreglar. Pot clonar el repositori, perquè és públic. Pot crear una branca al seu portàtil. El que no pot és fer git push: el servidor li respondrà amb un error de permisos.

Aquesta lliçó resol exactament aquest problema. I, de passada, tanca la promesa pendent del mòdul 4: què vol dir realment el remot anomenat upstream.

Contingut

  1. El problema: proposar un canvi sense permís d'escriptura
  2. Què és un fork i per què no és un concepte de Git
  3. El triangle: origin, upstream i el teu repositori local
  4. Muntar el triangle pas a pas
  5. Mantenir el fork al dia
  6. Què és una pull request (o merge request)
  7. Anatomia d'una bona pull request
  8. Pull requests en esborrany
  9. El recorregut complet d'en Diego, de dalt a baix
  10. Baixar la branca d'una PR per provar-la en local
  11. Forks o branques al mateix repositori: criteris per triar

  1. El problema: proposar un canvi sense permís d'escriptura

Recordem com funciona l'enviament de canvis (lliçó 04-05). Quan l'Ana executa git push origin funcionalitat/ordre-alfabetic, passen dues coses:

  1. Git negocia amb el servidor quins objectes falten i els transfereix.
  2. El servidor actualitza una referència: crea o mou refs/heads/funcionalitat/ordre-alfabetic.

El pas 2 és una escriptura al repositori del servidor, i qualsevol plataforma d'allotjament la protegeix amb permisos. L'Ana, en Bruno i la Carla en tenen perquè són l'equip. En Diego, no.

Si en Diego ho intenta, veu això:

git push origin correccio/reordenar-al-completar
remote: Permission to equip/gestor-tasques.git denied to drueda.
fatal: unable to access 'https://git.exemple.cat/equip/gestor-tasques.git/':
The requested URL returned error: 403

Aquí és on molta gent s'encalla, i val la pena dir-ho clar: Git no té cap resposta per a aquest problema. Git és un sistema distribuït; en el seu disseny original, la manera de proposar un canvi era enviar pedaços per correu electrònic (ho veurem a la lliçó 07-02). No existeix cap ordre git propose ni git pull-request.

La resposta se la van inventar les plataformes d'allotjament, i consta de dues peces complementàries:

Peça Què resol On viu
Fork Donar a en Diego un lloc on sí que pugui escriure Plataforma (GitHub, GitLab, Gitea…)
Pull request Donar-li un canal formal per proposar el seu canvi i discutir-lo Plataforma

Cap de les dues forma part de Git. Totes dues es construeixen damunt d'operacions de Git que ja coneixes: clonar, enviar i fusionar.

  1. Què és un fork i per què no és un concepte de Git

La definició, sense floritures:

Un fork és una còpia del repositori feta al servidor mateix, allotjada sota el teu compte, sobre la qual tens permís total d'escriptura.

És a dir: un git clone que passa de servidor a servidor, no de servidor al teu portàtil. En Diego prem el botó de crear el fork i la plataforma crea git.exemple.cat/drueda/gestor-tasques amb tot l'historial, totes les branques i totes les etiquetes de l'original.

flowchart LR
    A["git.exemple.cat/equip/gestor-tasques<br/>(original — en Diego només hi pot llegir)"]
    B["git.exemple.cat/drueda/gestor-tasques<br/>(fork — en Diego hi escriu)"]
    C["Portàtil d'en Diego<br/>(clon local)"]
    A -->|"fork (al servidor)"| B
    B -->|"git clone"| C
    C -->|"git push"| B
    B -.->|"pull request"| A

Fixa't en la fletxa de punts: el fork no retorna res automàticament. En Diego pot omplir el seu fork de commits durant mesos i el repositori original no se n'assabentarà. La pull request és el mecanisme explícit que diu «mira què tinc, ho vols?».

Tres conseqüències que convé tenir clares des del principi:

  • El fork és un repositori Git normal i corrent. Té la seva URL, es clona igual, accepta push igual. L'única cosa especial és que la plataforma recorda d'on va sortir, per poder-te oferir de crear pull requests contra l'original.
  • El fork no s'actualitza sol. És una foto del moment en què el vas crear. Si l'equip continua treballant, el teu fork es queda enrere. Mantenir-lo al dia és cosa teva (apartat 5).
  • La paraula té un altre significat històric. En el món del programari lliure, «fer un fork» volia dir tradicionalment escindir un projecte per portar-lo en una altra direcció, amb un altre equip (LibreOffice a partir d'OpenOffice, per exemple). Aquest sentit encara existeix. El fork del qual parlem aquí és purament tècnic i temporal: una còpia per poder contribuir.

Noms a cada plataforma. GitHub, GitLab i Gitea en diuen fork. Bitbucket, també. GitLab, a més, manté un enllaç visible entre el fork i l'original que permet sincronitzar des de la interfície.

  1. El triangle: origin, upstream i el teu repositori local

Ara en Diego té tres repositoris en joc, no pas dos. I aquí és on entra la convenció que el mòdul 4 va deixar anunciada.

A la lliçó 04-01 vam veure que un remot no és res més que un nom curt per a una URL, i que origin i upstream no són paraules reservades de Git: són costums. També vam avisar que upstream vol dir dues coses diferents. Aquí fem servir la primera:

upstream (com a nom de remot): el remot que apunta al repositori original del qual vas fer el fork.

El repartiment convencional queda així:

Remot Apunta a Permisos d'en Diego Serveix per a
origin El seu fork (drueda/gestor-tasques) Lectura i escriptura push de les seves branques de treball
upstream L'original (equip/gestor-tasques) Només lectura fetch per estar al dia
flowchart TD
    U["upstream<br/>equip/gestor-tasques"]
    O["origin<br/>drueda/gestor-tasques"]
    L["Local<br/>~/projectes/gestor-tasques"]
    U -->|"git fetch upstream"| L
    L -->|"git push origin"| O
    O -->|"pull request"| U
    U -->|"fork inicial"| O

Llegeix-ho com un cicle: la feina baixa de l'original, passa pel teu local, puja al teu fork i torna a l'original en forma de proposta. Aquest recorregut triangular és la raó que aquesta configuració es digui triangular workflow, i Git fins i tot té opcions pensades per a ella (remote.pushDefault, push.default = current), que veurem a l'apartat 4.

L'errada més habitual de qui comença és fer git pull esperant portar-se els canvis del projecte original, quan pull parla amb origin, que és el teu fork, que no s'ha mogut. Un es queda mirant la pantalla convençut que «Git no baixa res». I té raó: no hi ha res per baixar d'allà.

  1. Muntar el triangle pas a pas

En Diego ja ha creat el fork a la plataforma. Ara, al seu portàtil:

# 1. Clonar EL SEU FORK (no l'original). origin queda configurat tot sol.
git clone [email protected]:drueda/gestor-tasques.git
cd gestor-tasques
# 2. Afegir l'original com a segon remot anomenat upstream
git remote add upstream [email protected]:equip/gestor-tasques.git

Un detall pràctic: com que en Diego mai no podrà escriure a upstream, convé fer servir la URL de només lectura (https://) per a aquest remot, o bé desactivar-ne directament l'enviament:

# Que un push accidental a upstream falli de seguida, sense arribar al servidor
git remote set-url --push upstream NO_ENVIAR

Comprovem el resultat:

git remote -v
origin    [email protected]:drueda/gestor-tasques.git (fetch)
origin    [email protected]:drueda/gestor-tasques.git (push)
upstream  [email protected]:equip/gestor-tasques.git (fetch)
upstream  NO_ENVIAR (push)

Ara git push upstream mor en local amb un error clar en comptes d'intentar autenticar-se i fallar amb un 403 confús.

Ajustos que estalvien errades

Aquestes tres opcions converteixen el triangle en una cosa còmoda:

# Que 'git push' sense arguments vagi sempre al fork
git config remote.pushDefault origin

# Que enviï la branca actual amb el mateix nom
git config push.default current

# Que 'git pull' faci rebase en comptes de fusionar (lliçó 05-01)
git config pull.rebase true

I una més, molt útil: fer que la branca main local segueixi l'original, no el fork. Així git status li diu a en Diego com de lluny ha quedat del projecte de debò, que és el que li importa:

git branch --set-upstream-to=upstream/main main

Compte amb la coincidència de paraules: aquí estem fent servir els dos sentits d'upstream a la mateixa línia. --set-upstream-to configura la branca de seguiment (lliçó 04-06) i upstream/main és la branca del remot anomenat upstream. Que el nom coincideixi és casualitat de la convenció, no pas màgia.

  1. Mantenir el fork al dia

El fork envelleix. Mentre en Diego prepara la seva correcció, l'Ana i la Carla continuen integrant coses a equip/gestor-tasques. Si en Diego construeix la seva branca sobre una base de fa tres setmanes, la seva pull request arribarà amb conflictes i amb codi que ja no encaixa.

La rutina de posada al dia són tres ordres:

# 1. Portar l'estat real del projecte original
git fetch upstream

# 2. Posar el meu main local exactament on és el seu
git switch main
git merge --ff-only upstream/main

# 3. Empènyer aquest main actualitzat al meu fork
git push origin main

Val la pena aturar-se en cadascuna.

git fetch upstream només actualitza les referències remotes upstream/*. No toca cap branca local ni el directori de treball. És sempre segur.

git merge --ff-only upstream/main és deliberadament estricte. Com vam veure a la lliçó 03-03, --ff-only avorta si la fusió no es pot resoldre avançant el punter. I això és exactament el que volem: si falla, vol dir que hi ha commits al teu main local que no són a l'original, cosa que en un fork gairebé sempre és un error (has treballat directament sobre main en comptes de crear una branca). Val més assabentar-se'n amb un error que amb una fusió sorpresa.

Si de debò vols llençar el teu main i adoptar el de l'original sense preguntes:

git switch main
git reset --hard upstream/main
git push --force-with-lease origin main

Això és legítim al teu propi fork, perquè el teu main no és un historial compartit: ningú més no el consumeix. Continua valent la regla d'or de la lliçó 05-06 (no reescriguis el que altres ja han rebut), però aquí aquests «altres» ets tu mateix. I --force-with-lease en comptes de --force, sempre (lliçó 04-05).

I la branca de treball?

Cas diferent. Si en Diego ja té commits a correccio/reordenar-al-completar i l'original ha avançat, té dues opcions que ja coneix:

git switch correccio/reordenar-al-completar

# Opció A: rebase sobre el nou main (historial lineal, cal reenviar amb força)
git rebase upstream/main
git push --force-with-lease origin correccio/reordenar-al-completar

# Opció B: fusionar main dins de la branca (no reescriu, afegeix un commit de fusió)
git merge upstream/main
git push origin correccio/reordenar-al-completar

Quina triar depèn de l'acord del projecte, i aquest acord és matèria de la lliçó 08-02. Com a orientació: mentre la pull request no tingui revisions publicades, el rebase és net i no molesta ningú. Un cop hi ha comentaris sobre commits concrets, un rebase els descol·loca i sol preferir-se la fusió (o esperar a integrar i deixar que qui fusiona decideixi).

Sobre el botó «Sync fork». Moltes plataformes ofereixen sincronitzar el fork des del web amb un clic. Fa exactament el del bloc de dalt, però només actualitza el fork al servidor: el teu clon local no se n'assabenta fins que facis git fetch origin o git pull. És una font clàssica de confusió.

  1. Què és una pull request (o merge request)

Definició operativa:

Una pull request és una petició formal d'integrar els commits d'una branca en una altra, acompanyada d'un fil de conversa, un diff calculat per la plataforma i un conjunt de comprovacions automàtiques.

Fixa't en el que no diu. No diu «els canvis d'un fork»: una PR pot anar perfectament d'una branca a una altra dins del mateix repositori, i de fet així és com treballa la majoria d'equips interns (apartat 11). El fork és un cas particular, no la definició.

Una pull request té sempre aquests elements:

Element Què és
Branca d'origen (source, compare, head) D'on vénen els commits: drueda:correccio/reordenar-al-completar
Branca de destinació (target, base) On es volen integrar: equip:main
Títol i descripció L'explicació per a humans
Diff Calculat des de l'ancestre comú (lliçó 07-02)
Conversa Comentaris generals i comentaris ancorats a línies concretes
Comprovacions Resultats del CI (lliçó 07-06)
Estat Oberta, esborrany, tancada o integrada

El nom canvia segons la plataforma, però la cosa és la mateixa:

Plataforma Nom
GitHub, Gitea, Bitbucket Pull request (PR)
GitLab Merge request (MR)
Gerrit Change (amb un model diferent, basat en un commit per canvi)

El nom de GitHub és l'històricament correcte: «et demano que facis pull de la meva branca». El de GitLab descriu millor el que passa realment al final: una fusió.

I una precisió important sobre el cicle de vida: la PR és un objecte viu. No és un enviament d'un sol cop. Si en Diego fa git push de commits nous a la mateixa branca del seu fork, la PR s'actualitza sola: hi apareixen els commits nous, es recalcula el diff i es tornen a llançar les comprovacions. No cal crear una PR nova per cada correcció.

  1. Anatomia d'una bona pull request

Una PR és, sobretot, una petició de temps aliè. Algú deixarà el que estava fent per llegir la teva feina. Tot el que ve a continuació va de respectar aquest temps.

La branca

Nom significatiu, seguint la convenció del projecte (lliçó 03-06). A gestor-tasques: funcionalitat/, correccio/, documentacio/, hotfix/. correccio/reordenar-al-completar diu què hi ha a dins abans d'obrir res; pedac2 o apany-diego no diuen res.

La mida

Aquest és el factor que més impacte té en la qualitat de la revisió, i el quantificarem amb dades a la lliçó 07-02. De moment, la regla: una PR ha de fer una cosa. Si en escriure la descripció necessites la paraula «i» tres vegades, són tres PRs.

Un cas concret: en Diego troba l'errada de reordenació, però de passada veu que estils.css té indentació inconsistent i que README.md té un enllaç trencat. La temptació d'arreglar-ho tot en el mateix canvi és enorme. No ho facis. El revisor haurà de separar mentalment l'arranjament de debò de seixanta línies de reformatatge, i l'arranjament de debò és l'única cosa que importa.

Els commits

Que es puguin llegir un a un. Aquí és on es paga el rebase interactiu de la lliçó 05-02: abans d'obrir la PR, mira el teu propi historial i neteja'l.

git log --oneline upstream/main..HEAD
9f2a1c8 apanyat
7e3d0b4 ara si
c5a8f21 proves
1d4e9a7 wip

Això no s'ensenya a ningú. Amb un git rebase -i upstream/main i uns quants fixup, es converteix en:

a7c2e91 Reordena la llista en marcar una tasca com a completada

Un commit, un canvi, un missatge que explica el perquè. Les regles concretes de redacció d'aquests missatges són el contingut de la lliçó 08-01; aquí n'hi ha prou amb la idea que l'historial que proposes forma part de la proposta.

La descripció

Una bona descripció respon a quatre preguntes:

  1. Quin problema resol (i enllaç al tiquet: GT-214, la convenció que ja validen els hooks del mòdul 6).
  2. Què has fet i, si hi ha diverses opcions, per què aquesta.
  3. Com es comprova: passos concrets per reproduir l'errada i veure que ja no passa.
  4. Què queda fora, si has decidit conscientment no abordar alguna cosa.

Exemple real de la PR d'en Diego:

## Problema (GT-214)

En marcar una tasca com a completada, la tasca es queda a la seva posició
original de la llista en comptes de moure's al final. En recarregar la pàgina
sí que apareix ben col·locada, perquè l'ordre es recalcula en llegir de
localStorage.

## Solució

`marcaCompletada()` a `app.js` actualitzava l'estat i repintava només
l'element afectat. Ara crida `renderitzaLlista()`, que aplica el
mateix criteri d'ordenació que la càrrega inicial.

## Com comprovar-ho

1. Obrir `index.html` amb tres tasques pendents.
2. Marcar la primera com a completada.
3. Ha de desplaçar-se al final de la llista, sense recarregar.

## Fora d'abast

`estils.css` té indentació inconsistent a la zona que he tocat.
No ho he arreglat aquí per no barrejar; ho proposo en una altra PR.

Molts projectes automatitzen això amb una plantilla de pull request: un fitxer versionat (per exemple .github/pull_request_template.md o .gitlab/merge_request_templates/) el contingut del qual apareix precarregat a la caixa de descripció. És un fitxer de text al repositori, res més.

  1. Pull requests en esborrany

Hi ha un moment incòmode: vols ensenyar la feina abans d'acabar-la. Per demanar opinió sobre l'enfocament, perquè el CI la provi, perquè ningú no dupliqui el teu esforç. Però si obres una PR normal, algú la revisarà a fons i perdrà el temps, o pitjor, la integrarà.

Per a això hi ha les pull requests en esborrany (draft a GitHub, merge request marcada com a Draft a GitLab, que històricament s'indicava amb el prefix WIP: al títol).

Una PR en esborrany:

  • És visible i té el seu fil de conversa i el seu diff.
  • Llança les comprovacions automàtiques igual que una de normal (segons la configuració).
  • No es pot integrar fins que es marca com a llesta.
  • Normalment no demana revisors de manera automàtica.

És l'eina correcta per a tres situacions: demanar validació primerenca de l'enfocament abans d'invertir-hi tres dies, deixar constància pública que estàs treballant en alguna cosa, i fer servir el CI del projecte per provar en entorns que no tens en local (la Carla, amb Windows 11, la fa servir per comprovar que el seu canvi també funciona a Linux).

  1. El recorregut complet d'en Diego, de dalt a baix

Tot junt, en ordre. En Diego parteix de zero.

Pas 1: fork. A la plataforma, sobre equip/gestor-tasques. Resultat: drueda/gestor-tasques.

Pas 2: clonar el fork i muntar el triangle.

git clone [email protected]:drueda/gestor-tasques.git
cd gestor-tasques
git remote add upstream https://git.exemple.cat/equip/gestor-tasques.git
git remote set-url --push upstream NO_ENVIAR
git config remote.pushDefault origin

Pas 3: partir de l'estat més recent de l'original. No del que hi havia quan va fer el fork.

git fetch upstream
git switch -c correccio/reordenar-al-completar upstream/main

Aquest switch -c ... upstream/main és el gest clau: crea la branca sobre la punta del projecte real, no sobre el seu fork desactualitzat.

Pas 4: treballar. Editar app.js, confirmar, editar, confirmar. Sense preocupar-se encara per la neteja.

git add app.js
git commit -m "Repinta la llista completa en marcar-la com a completada"
# ... més iteracions

Pas 5: netejar abans d'ensenyar.

git fetch upstream
git rebase -i upstream/main

Aixafa els tanteigs, deixa els commits que expliquen una història i, de passada, comprova que continua funcionant sobre la base actual.

Pas 6: enviar al fork.

git push -u origin correccio/reordenar-al-completar
remote: Crea una pull request per a 'correccio/reordenar-al-completar' a:
remote:   https://git.exemple.cat/equip/gestor-tasques/compare/main...drueda:correccio/reordenar-al-completar
To [email protected]:drueda/gestor-tasques.git
 * [new branch]      correccio/reordenar-al-completar -> correccio/reordenar-al-completar

Aquest missatge remote: no se l'inventa Git: l'emet el servidor mitjançant un hook post-receive (els que 06-01 va deixar pendents i que reprendrem a 07-06).

Pas 7: obrir la pull request. A la plataforma, amb branca d'origen drueda:correccio/reordenar-al-completar i destinació equip:main, amb la descripció de l'apartat 7.

Pas 8: la revisió. L'Ana revisa i demana un canvi: que la funció no repinti la llista sencera si l'ordre no ha canviat, per rendiment amb moltes tasques.

Pas 9: respondre a la revisió. En Diego no obre cap PR nova. Treballa a la mateixa branca:

git switch correccio/reordenar-al-completar
# editar app.js
git commit -am "Repinta nomes si l'ordre ha canviat"
git push origin correccio/reordenar-al-completar

La PR s'actualitza sola. L'Ana veu el commit nou i pot revisar només això, sense rellegir-ho tot.

Pas 10: integració. L'Ana aprova i prem el botó. La plataforma executa la fusió al servidor —amb el mètode que el projecte hagi decidit: fusió normal, squash o rebase, tema de la lliçó 07-04— i tanca la PR.

Pas 11: neteja.

git switch main
git fetch upstream
git merge --ff-only upstream/main            # ja conté el seu canvi
git push origin main                          # actualitzar el fork
git branch -d correccio/reordenar-al-completar
git push origin --delete correccio/reordenar-al-completar

I amb git fetch --prune (lliçó 04-04), les referències remotes obsoletes desapareixen.

  1. Baixar la branca d'una PR per provar-la en local

L'Ana no vol revisar el canvi d'en Diego només llegint un diff al navegador. El vol executar. Però la branca és al fork d'en Diego, un repositori que ella no té configurat.

Hi ha tres maneres de portar-se-la, de menys a més elegant.

Manera 1: afegir el fork com a remot

Funciona sempre, però acumula remots si revises molta gent:

git remote add drueda https://git.exemple.cat/drueda/gestor-tasques.git
git fetch drueda
git switch -c revisio-diego drueda/correccio/reordenar-al-completar

Manera 2: la referència especial de la PR

Aquí hi ha el truc bo. Les plataformes publiquen cada pull request com una referència dins del repositori original, encara que la branca visqui en un fork. A GitHub:

# La PR número 42, portada a una branca local anomenada revisio/pr-42
git fetch origin pull/42/head:revisio/pr-42
git switch revisio/pr-42

Llegeix-ho amb el que saps dels refspecs (lliçó 04-02): pull/42/head és la referència al servidor, revisio/pr-42 és el nom local. Els dos punts separen origen i destinació, exactament igual que en qualsevol altre refspec.

A GitLab, la ruta canvia de nom però el mecanisme és idèntic:

# GitLab: merge request número 42
git fetch origin merge-requests/42/head:revisio/mr-42

I a Gitea/Forgejo:

git fetch origin pull/42/head:revisio/pr-42

GitHub publica, a més, una segona referència molt útil:

Referència Què conté
pull/42/head La punta de la branca tal com la va enviar el seu autor
pull/42/merge El resultat de fusionar-la amb la branca de destinació, precalculat pel servidor

pull/42/merge és el que en realitat prova el CI quan comprova una pull request, i és l'origen d'una sorpresa clàssica que desenvoluparem a la lliçó 07-06: les proves no s'executen sobre la teva branca, sinó sobre la teva branca fusionada amb main.

Manera 3: configurar el refspec d'un cop

Si revises PRs cada dia, afegeix això a la configuració del remot perquè un simple git fetch les porti totes:

git config --add remote.origin.fetch '+refs/pull/*/head:refs/remotes/origin/pr/*'
git fetch origin
git switch -c revisio-42 origin/pr/42

A .git/config queda així:

[remote "origin"]
    url = [email protected]:equip/gestor-tasques.git
    fetch = +refs/heads/*:refs/remotes/origin/*
    fetch = +refs/pull/*/head:refs/remotes/origin/pr/*

El + inicial permet actualitzacions no fast-forward, necessari perquè l'autor d'una PR pot haver reescrit la seva branca amb un rebase.

Compte en repositoris molt actius. En un projecte amb deu mil pull requests històriques, aquesta segona línia fa que cada fetch porti milers de referències. Fes-la servir en repositoris de mida raonable, o restringeix el patró.

I un apunt pràctic: les eines de línia d'ordres de les plataformes (gh pr checkout 42 a GitHub, glab mr checkout 42 a GitLab) fan exactament això per tu, incloent-hi configurar el seguiment perquè puguis enviar correccions si en tens permís. Són còmodes, però saber què hi ha a sota és el que et salva quan no funcionen.

Combinat amb el del mòdul 6, la revisió surt de franc en temps de context:

# Revisar sense abandonar el que estaves fent (lliçó 06-06)
git fetch origin pull/42/head:revisio/pr-42
git worktree add ../revisio-42 revisio/pr-42

  1. Forks o branques al mateix repositori: criteris per triar

En Diego fa servir un fork perquè no té cap altra opció. L'Ana, en Bruno i la Carla no són en aquesta situació: tenen permís d'escriptura, i per a ells el fork seria una volta innecessària. Treballen amb branques al mateix repositori i obren pull requests de branca a branca.

La comparació completa:

Aspecte Forks Branques al mateix repositori
Permisos necessaris Només lectura a l'original Escriptura a l'original
Configuració Dos remots, cal mantenir el fork al dia Un remot, res per sincronitzar
Visibilitat de la feina Baixa: les branques són en repositoris aliens Alta: git branch -r les mostra totes
Baixar la branca d'un altre Requereix refspec de PR o afegir un remot git fetch && git switch <branca>
Col·laborar a la branca d'un altre Només si l'autor ho permet explícitament Directe, si hi ha permís
Aïllament Total: ningú no embruta el repositori principal Menor: les branques abandonades s'acumulen
CI amb secrets Restringit: les PRs des de forks no reben credencials Accés complet
Adequat per a Projectes oberts, col·laboradors externs, contractes Equips amb confiança mútua

El punt dels secrets al CI mereix explicació, perquè és una restricció de seguretat important i sorprèn molta gent. Si el CI del projecte tingués accés a credencials de desplegament en executar el codi de qualsevol PR, qualsevol persona d'internet podria robar-les simplement obrint una PR el codi de la qual imprimís les variables d'entorn. Per això les plataformes executen les PRs procedents de forks en un mode restringit, sense secrets, i sovint exigint que un membre de l'equip aprovi l'execució. Ho reprendrem a la lliçó 07-06.

Criteri pràctic

La regla es redueix a una pregunta: confies en aquesta persona per escriure al repositori?

  • → branques al mateix repositori. Menys fricció, més visibilitat.
  • No, o encara no → fork.

Molts projectes oberts apliquen la regla esglaonada: els col·laboradors de fora fan servir forks i, quan algú demostra constància, se li dóna permís d'escriptura i passa a treballar amb branques. En Diego, si continua contribuint a gestor-tasques, acabarà al segon grup.

Un matís sobre els permisos. «Permís d'escriptura» no vol dir «pot trencar main». Amb les branques protegides de la lliçó 07-06, algú pot crear branques i obrir PRs però continuar tenint prohibit enviar directament a main. Els dos mecanismes són independents i es combinen.

Errors Habituals i Consells

Error 1: clonar l'original en comptes del fork. En Diego clona equip/gestor-tasques, treballa i, en enviar, rep un 403. S'arregla sense perdre res: git remote rename origin upstream i git remote add origin <url-del-fork>.

Error 2: creure que el fork s'actualitza sol. És una foto del moment de creació. Si no executes git fetch upstream periòdicament, treballes sobre una base antiga i la teva PR arribarà amb conflictes.

Error 3: git pull esperant els canvis del projecte original. pull parla amb origin, que és el teu fork, que no es mou sol. El que vols és git fetch upstream.

Error 4: sincronitzar el fork al web i creure que el local està al dia. El botó «Sync fork» actualitza el servidor. El teu portàtil continua igual fins que facis git fetch origin.

Error 5: treballar directament sobre main al fork. Converteix cada posada al dia en un conflicte i t'impedeix tenir dues propostes simultànies. Branca nova sempre, i creada sobre upstream/main.

Error 6: obrir una PR nova per cada correcció de la revisió. La PR s'actualitza sola en enviar a la mateixa branca. Tancar-la i obrir-ne una altra llença a les escombraries tota la conversa prèvia.

Error 7: ficar reformatatges o arranjaments aliens a la mateixa PR. Converteix un diff de deu línies revisables en un de dues-centes il·legibles. Una PR a part, sempre.

Error 8: enviar sense netejar l'historial. Quatre commits anomenats wip, apanyat, ara si i proves són una falta de respecte al revisor. git rebase -i upstream/main abans d'enviar.

Error 9: esborrar la branca del fork abans que la PR s'integri. La PR es queda sense contingut i la plataforma la tanca sola. Esborrar-la després.

Consell 1: desactiva el push a upstream. git remote set-url --push upstream NO_ENVIAR. Una errada tipogràfica menys.

Consell 2: crea les branques amb git switch -c <branca> upstream/main. Explicitar la base evita el 90% dels conflictes d'una PR.

Consell 3: llegeix el CONTRIBUTING.md abans d'obrir res. Molts projectes hi documenten el format dels missatges, la branca de destinació correcta i si accepten o no certs canvis. Ignorar-lo és la via ràpida a que et tanquin la PR.

Consell 4: obre en esborrany si encara no està llest. És més honest que un títol amb NO FUSIONAR en majúscules.

Consell 5: aprèn git fetch origin pull/<n>/head:<branca> de memòria. És l'ordre que converteix la revisió en una cosa real i no en lectura de diffs en un navegador.

Consell 6: combina la referència de PR amb git worktree. Revisar sense perdre el context de la teva pròpia feina (lliçó 06-06).

Exercicis

Exercici 1: simular el triangle en local

Sense plataforma ni internet, reprodueix la topologia completa amb repositoris bare (lliçó 04-01):

  1. Crea /tmp/servidor/original.git com a repositori bare i publica-hi un main amb index.html, app.js i README.md.
  2. Simula el fork: crea /tmp/servidor/fork.git com a clon bare de l'original.
  3. Clona el fork a /tmp/diego, afegeix-hi upstream apuntant a l'original i desactiva'n l'enviament.
  4. Comprova amb git remote -v que el triangle està ben muntat.
  5. Des d'un altre clon /tmp/ana, afegeix un commit a l'original perquè el fork quedi desactualitzat.
  6. Des de /tmp/diego, posa el main local al dia i actualitza el fork.

Exercici 2: el cicle complet d'una contribució

Continuant amb l'escenari anterior:

  1. A /tmp/diego, crea correccio/enllac-trencat sobre upstream/main i fes tres commits desordenats sobre README.md (un de bo i dos de tanteig).
  2. Neteja'ls amb rebase interactiu fins a deixar un sol commit amb un missatge decent.
  3. Envia'l al fork.
  4. Des de /tmp/ana, afegeix el fork com a remot, porta la branca d'en Diego, revisa-la amb git log i git diff, i fusiona-la a main de l'original.
  5. Torna a /tmp/diego, actualitza main, actualitza el fork i esborra la branca en local i al fork.

Exercici 3: refspecs de pull request

  1. A /tmp/servidor/original.git, crea a mà una referència que imiti la d'una PR: agafa el commit de la branca d'en Diego i desa'l com a refs/pull/1/head (pista: git update-ref, executat dins del repositori bare).
  2. Des de /tmp/ana, porta aquesta referència a una branca local anomenada revisio/pr-1 amb un sol git fetch.
  3. Configura el refspec permanent al .git/config de /tmp/ana perquè totes les refs/pull/*/head arribin com a origin/pr/*.
  4. Comprova amb git branch -r que hi apareix origin/pr/1.
  5. Crea un worktree a /tmp/revisio-1 apuntant a aquesta referència, sense abandonar la teva branca actual.

Solucions

Solució 1:

mkdir -p /tmp/servidor && cd /tmp/servidor
git init --bare original.git

# Publicar contingut inicial des d'un clon temporal
git clone /tmp/servidor/original.git /tmp/inicial
cd /tmp/inicial
git switch -c main 2>/dev/null || git switch main
echo "<h1>Gestor de Tasques</h1>" > index.html
echo "// app.js" > app.js
printf '# gestor-tasques\n\nMira els docs a http://enllac-trencat.exemple.cat\n' > README.md
git add . && git commit -q -m "Estructura inicial de l'aplicacio"
git push -u origin main
# El "fork": un clon bare de servidor a servidor
cd /tmp/servidor
git clone --bare /tmp/servidor/original.git fork.git
# El clon d'en Diego, amb el triangle muntat
git clone /tmp/servidor/fork.git /tmp/diego
cd /tmp/diego
git remote add upstream /tmp/servidor/original.git
git remote set-url --push upstream NO_ENVIAR
git remote -v
origin    /tmp/servidor/fork.git (fetch)
origin    /tmp/servidor/fork.git (push)
upstream  /tmp/servidor/original.git (fetch)
upstream  NO_ENVIAR (push)
# L'Ana avança l'original
git clone /tmp/servidor/original.git /tmp/ana
cd /tmp/ana
echo "body { margin: 0; }" > estils.css
git add . && git commit -q -m "Afegeix el full d'estils base"
git push origin main
# En Diego es posa al dia
cd /tmp/diego
git fetch upstream
git switch main
git merge --ff-only upstream/main
git push origin main
git log --oneline -2

Solució 2:

cd /tmp/diego
git fetch upstream
git switch -c correccio/enllac-trencat upstream/main

sed -i 's|http://enllac-trencat.exemple.cat|https://docs.exemple.cat/gestor-tasques|' README.md
git commit -qam "apanyo enllac"
echo "" >> README.md && git commit -qam "wip"
echo "## Documentacio" >> README.md && git commit -qam "ara si"
git log --oneline upstream/main..HEAD
# Neteja: aixafar els tres en un
GIT_SEQUENCE_EDITOR="sed -i '2,3s/^pick/fixup/'" git rebase -i upstream/main
git commit --amend -q -m "Corregeix l'enllac trencat de la documentacio al README

L'enllac apuntava a un domini que es va retirar. Ara apunta al portal
de documentacio actual. Refs GT-231."
git log --oneline upstream/main..HEAD
b3f1a7d Corregeix l'enllac trencat de la documentacio al README
git push -u origin correccio/enllac-trencat
# L'Ana revisa i integra
cd /tmp/ana
git remote add drueda /tmp/servidor/fork.git
git fetch drueda
git log --oneline main..drueda/correccio/enllac-trencat
git diff main...drueda/correccio/enllac-trencat
git merge --no-ff drueda/correccio/enllac-trencat -m "Fusiona correccio/enllac-trencat de drueda"
git push origin main
# En Diego neteja
cd /tmp/diego
git switch main
git fetch upstream
git merge --ff-only upstream/main
git push origin main
git branch -d correccio/enllac-trencat
git push origin --delete correccio/enllac-trencat

Solució 3:

# 1. Crear la referència de PR a mà al bare
COMMIT=$(git --git-dir=/tmp/servidor/fork.git rev-parse correccio/enllac-trencat 2>/dev/null \
         || git -C /tmp/diego rev-parse origin/main)
git --git-dir=/tmp/servidor/original.git update-ref refs/pull/1/head "$COMMIT"
git --git-dir=/tmp/servidor/original.git show-ref | grep pull
b3f1a7d1c4e8f9a2b7d0c3e5f8a1b4d7e0c3f6a9 refs/pull/1/head

Nota: si el commit no existeix a original.git perquè no es va arribar a fusionar, porta'l abans amb git --git-dir=/tmp/servidor/original.git fetch /tmp/servidor/fork.git 'refs/heads/*:refs/remotes/fork/*'.

# 2. Portar-la a una branca local
cd /tmp/ana
git fetch origin pull/1/head:revisio/pr-1
git switch revisio/pr-1
git log --oneline -1
# 3. Refspec permanent
git config --add remote.origin.fetch '+refs/pull/*/head:refs/remotes/origin/pr/*'
git fetch origin
# 4. Comprovar
git branch -r
  origin/HEAD -> origin/main
  origin/main
  origin/pr/1
# 5. Worktree per revisar sense canviar de context
git switch main
git worktree add /tmp/revisio-1 origin/pr/1
git worktree list
/tmp/ana          a1b2c3d [main]
/tmp/revisio-1    b3f1a7d (detached HEAD)

Conclusió

Aquesta lliçó ha respost la primera pregunta que va deixar oberta el mòdul 6: com proposa un canvi qui no pot escriure al repositori. L'essencial:

  • Ni el fork ni la pull request són conceptes de Git. Són invents de les plataformes construïts sobre operacions que ja coneixies: clonar, enviar i fusionar.
  • Un fork és un clon del repositori fet al servidor, sota el teu compte, on sí que tens permís d'escriptura. No s'actualitza sol.
  • El triangle origin (el teu fork) / upstream (l'original) / local és la configuració estàndard del col·laborador extern, i tanca la convenció upstream que vam anunciar al mòdul 4. Recorda que la paraula té un altre sentit —la branca de seguiment de la lliçó 04-06— i que no són el mateix.
  • Mantenir el fork al dia és git fetch upstream + git merge --ff-only upstream/main + git push origin main. Crear les branques amb git switch -c <branca> upstream/main evita la majoria dels conflictes.
  • Una pull request és una petició d'integrar una branca en una altra, més el fil de conversa. És un objecte viu: enviar-hi més commits l'actualitza, no cal obrir-ne una altra.
  • Una bona PR: branca amb nom significatiu, una sola cosa, mida acotada, commits llegibles després d'un rebase interactiu, i una descripció que digui quin problema resol i com comprovar-ho.
  • Les PRs en esborrany serveixen per ensenyar feina sense acabar sense que ningú perdi el temps revisant-la ni la integri per error.
  • Les plataformes publiquen cada PR com una referència del repositori original: git fetch origin pull/<n>/head:<branca> a GitHub i Gitea, merge-requests/<n>/head a GitLab. És la manera de provar de debò el que revisaràs. GitHub publica, a més, pull/<n>/merge, el resultat precalculat de la fusió, que serà important a la lliçó 07-06.
  • Forks per a qui no té permís (projectes oberts, col·laboradors externs, restricció de secrets al CI); branques al mateix repositori per a equips amb confiança mútua. L'Ana, en Bruno i la Carla fan servir branques; en Diego fa servir un fork.

Ja tenim el canal pel qual arriba una proposta. Falta el que passa dins d'aquest canal: què mira exactament l'Ana quan revisa la feina d'en Diego, amb quines ordres l'examina sense dependre del navegador, com comenta sense desmoralitzar ningú i quan aprova. Aquest és el contingut de la lliçó 07-02: Revisions de Codi amb Git, on descobrirem, entre altres coses, per què el diff correcte per revisar porta tres punts i no pas dos.

Dominant Git: De Principiant a Avançat

Mòdul 1: Introducció a Git

Mòdul 2: Operacions Bàsiques de Git

Mòdul 3: Branques i Fusió

Mòdul 4: Treballant amb Repositoris Remots

Mòdul 5: Operacions Avançades de Git

Mòdul 6: Eines i Tècniques de Git

Mòdul 7: Estratègies de Col·laboració i Flux de Treball

Mòdul 8: Bones Pràctiques i Consells de Git

Mòdul 9: Resolució de Problemes i Depuració

Mòdul 10: Git al Món Real

© Copyright 2026. Tots els drets reservats