Git van binnenuit
Dit essay legt uit hoe Git werkt. Het gaat er van uit dat u Git goed genoeg begrijpt om het als versiecontrole-systeem voor uw projecten te gebruiken.
Het essay legt de nadruk op de graaf-structuur waarop Git is gebaseerd, en hoe de eigenschappen van deze graaf het gedrag van Git bepalen. Door naar de bouwstenen te kijken baseert u uw mentale model meer op de waarheid dan op hypotheses die voortkomen uit bewijs dat al experimenterend met de API is vergaard. Dit realistischer model verschaft u een beter begrip van wat Git heeft gedaan, wat het doet en wat het zal gaan doen.
De tekst is opgebouwd rond een serie Git-commando's, uitgevoerd op een enkel project. Deze worden afgewisseld met observaties over de graaf-datastructuur waarop Git is gebouwd. Deze observaties illustreren een eigenschap van de graaf, en het gedrag dat uit deze eigenschap voortkomt.
Als u naderhand misschien nog dieper in Git wilt duiken, kunt u een blik werpen op de uitgebreid becommentarieerde broncode van mijn implementatie van Git in JavaScript.
(Vaak zal ik termen die in verband met Git worden gebruikt vanaf nu onvertaald laten. Het wordt dus vaak "Tree Graph" ipv. "Boom Graaf" Hans)Creëer het project
~ $ mkdir alpha
~ $ cd alpha
De gebruiker maakt alpha
, een directory voor zijn/haar project.
~/alpha $ mkdir data
~/alpha $ printf 'a' > data/letter.txt
Hij/zij gaat naar de alpha
directory en maakt een directory data
aan.
(Gebruik onder Windows "echo a" ipv. "printf 'a'". Hans)
Daarin wordt een bestand letter.txt
aangemaakt met de letter a
als inhoud. De alpha directory ziet er nu aldus uit:
alpha
└── data
└── letter.txt
Initialiseer de repository
~/alpha $ git init
Initialized empty Git repository
git init
maakt van de huidige directory een Git repository. Daarbij creëert het een .git
directory en maakt daar enkele bestanden aan. Deze bestanden definiëren alles met betrekking tot de Git configuratie en de geschiedenis van het project. Het zijn doodgewone bestanden. De gebruiker kan ze lezen of editen met een teksteditor of shell. Oftewel: de gebruiker kan de geschiedenis van het project even gemakkelijk lezen en wijzigen als de bestanden van het project.
De alpha
directory ziet er nu zo uit:
alpha
├── data
| └── letter.txt
└── .git
├── objects
etc...
De .git
directory en de inhoud daarvan zijn van Git. Al de andere bestanden worden in het algemeen de werkkopie genoemd. Zij behoren aan de gebruiker toe.
Voeg een paar bestanden toe
~/alpha $ git add data/letter.txt
De gebruiker voert git add
uit op data/letter.txt
. Dit heeft twee gevolgen:
Ten eerste wordt er een nieuw blob bestand aangemaakt in de
.git/objects/
directory.Dit blob bestand bevat de gecomprimeerde inhoud van
data/letter.txt
. De naam ervan is afgeleid van de gehashte inhoud. Hashing van een stuk tekst houdt in dat het wordt omgezet naar een kleiner1 stuk tekst dat het origineel uniek 2 identificeert.
(Dus niet bevat zoals bij encryptie of compressie!!! Hans)
Git hashta
bijvoorbeeld naar2e65efe2a145dda7ee51d1741299f848e5bf752e
. De eerste twee karakters worden gebruikt als naam van een directory in de objects-database:.git/objects/2e/
. De rest van de hash wordt gebruikt als naam van het blob bestand dat de inhoud van de toegevoegde file bevat:.git/objects/2e/65efe2a145dda7ee51d1741299f848e5bf752e
.Merk op dat alleen al door het toevoegen van een bestand aan Git de inhoud ervan wordt opgeslagen in de
objects
directory. Die inhoud bevindt zich nog steeds veilig in Git als de gebruikerdata/letter.txt
wist in de werkkopie.Ten tweede voegt
git add
het bestand toe aan de index. De index is een lijst met alle bestanden die Git geacht wordt in de gaten te houden ("being tracked"). Die lijst is opgeslagen als een bestand in.git/index
. Elke regel van het bestand verbindt een beheerd bestand met de hash ("maps a tracked file to the hash") van de inhoud ervan op het moment dat het werd toegevoegd. Dit is de index nadat hetgit add
-commando is uitgevoerd:data/letter.txt 2e65efe2a145dda7ee51d1741299f848e5bf752e
Nu maakt de gebruiker een bestand data/number.txt
aan met 1234
als inhoud:
~/alpha $ printf '1234' > data/number.txt
De werkkopie ziet er nu zo uit:
alpha
└── data
└── letter.txt
└── number.txt
De gebruiker voegt het bestand aan Git toe:
~/alpha $ git add data
Het git add
commando creëert een blob object dat de inhoud van data/number.txt
bevat. En het voegt een entry in de index toe voor data/number.txt
die naar de blob verwijst. Dit is de index na het voor de tweede keer uitvoeren van het git add
commando:
data/letter.txt 2e65efe2a145dda7ee51d1741299f848e5bf752e
data/number.txt 274c0052dd5408f8ae2bc8440029ff67d79bc5c3
Merk op dat alleen de bestanden in de data
directory zijn opgenomen in de index, hoewel de gebruiker git add data
uitvoerde. De data
directory is niet afzonderlijk opgenomen.
~/alpha $ printf '1' > data/number.txt
~/alpha $ git add data
Stel dat toen de gebruiker data/number.txt
aanmaakte, het eigenlijk de bedoeling was 1
te typen, niet 1234
.
De correctie wordt aangebracht en het bestand opnieuw aan de index toegevoegd. Dit
commando creëert een nieuwe blob met de nieuwe inhoud. En de entry in de index wordt geupdate zodat deze voor data/number.txt
naar de nieuwe blob verwijst.
Het maken van een commit
~/alpha $ git commit -m 'a1'
[master (root-commit) 774b54a] a1
De gebruiker maakt de commit a1
. Git drukt wat gegevens over de commit af. Deze gegevens zullen snel duidelijk worden.
Het commit-commando heeft drie gevolgen.
- Er wordt een "tree graph" aangemaakt die de inhoud van de versie van het project dat wordt gecommit vertegenwoordigt.
- Het maakt een commit-object aan.
- Het laat de huidige branch wijzen naar het nieuwe commit-object.
Het aanmaken van een tree graph
Git legt de huidige staat van het project vast door een tree graph te maken op basis van de index. Deze tree graph slaat de locatie en inhoud van elk bestand van het project op.
De graph bevat twee soorten objecten: blobs en trees.
- Blobs worden opgeslagen door
git add
. Zij vertegenwoordigen de inhoud van bestanden. - Trees worden opgeslagen bij een commit. Een tree vertegenwoordigt een directory in de werkkopie.
Hier volgt het tree-object dat de inhoud van de data
directory vastlegt voor de nieuwe commit:
100664 blob 2e65efe2a145dda7ee51d1741299f848e5bf752e letter.txt
100664 blob 56a6051ca2b02b04ef92d5150c9ef600403cb1de number.txt
De eerste regel legt alles vast wat nodig is om data/letter.txt
te reproduceren.
- Het eerste deel geeft de file permissies weer.
- Het tweede deel geeft aan dat de inhoud van deze entry door een blob wordt vertegenwoordigt, en niet door een tree.
- Het derde deel geeft de hash van de blob.
- Het vierde deel is de filenaam.
De tweede regel geeft dezelfde informatie voor data/number.txt
.
Hier is het tree-object voor alpha
, de root-directory van het project:
040000 tree 0eed1217a2947f4930583229987d90fe5e8e0b74 data
De enige regel in deze tree wijst naar de data
tree.
In de graph hierboven wijst de root
tree naar de data
tree. De data
tree wijst naar de blobs voor data/letter.txt
en data/number.txt
.
Het aanmaken van een commit-object
Na de tree graph te hebben gemaakt creëert git commit
een commit-object. Het commit-object is gewoon een ander tekstbestand in .git/objects/
:
tree ffe298c3ce8bb07326f888907996eaa48d266db4
author Mary Rose Cook <mary@maryrosecook.com> 1424798436 -0500
committer Mary Rose Cook <mary@maryrosecook.com> 1424798436 -0500
a1
De eerste regel wijst naar de tree graph. De hash is van het tree-object dat de root van de werkkopie vertegenwoordigt. Dat is de alpha
directory. De laatste regel is de commit-message.
De huidige branch naar het nieuwe commit-object laten wijzen
Tenslotte laat het commit-commando de huidige branch naar het nieuwe commit-object wijzen.
Wat is de huidige branch? Git kijkt in het HEAD
bestand in .git/HEAD
en vindt:
ref: refs/heads/master
Dit zegt dat HEAD
naar master
wijst. master
is de huidige branch.
HEAD
en master
zijn allebei "refs". Een ref is een label dat door Git of de gebruiker wordt gebruikt om een specifieke commit te identificeren.
Het bestand dat de master
ref representeert bestaat niet, omdat dit de eerste commit naar de repository is. Git creëert het bestand in .git/refs/heads/master
met als inhoud de hash van het commit-object:
74ac3ad9cde0b265d2b4f1c778b283a6e2ffbafd
(Wanneer u deze Git commando's al lezend intypt, zal de hash van uw a1
commit verschillen van de mijne. Content-objecten als
blobs en trees zullen altijd dezelfde hash opleveren. Commits niet, omdat
ze ook datums en de namen van hun makers bevatten.)
Laten we HEAD
en master
toevoegen aan de Git graph:
HEAD
wijst naar master
, zoals het dat voor de commit al deed. Maar master
bestaat nu en wijst naar het nieuwe commit-object.
Het maken van een commit die niet de eerste commit is
Hier ziet u de Git graph na de a1
commit. De werkkopie en index zijn ook opgenomen.
Merk op dat de werkkopie, index, en a1
commit allemaal dezelfde inhoud hebben voor data/letter.txt
en data/number.txt
. De index en HEAD
commit gebruiken beide hashes om naar blob objecten te verwijzen, maar de inhoud van de werkkopie is als tekst opgeslagen op een andere lokatie.
~/alpha $ printf '2' > data/number.txt
De gebruiker verandert nu de inhoud van data/number.txt
in 2
. Dit verandert de werkkopie, maar laat de index en de HEAD
commit zoals ze waren.
~/alpha $ git add data/number.txt
De gebruiker voegt het bestand toe aan Git. Git voegt een blob met 2
als inhoud toe aan de objects
directory. En het laat de index entry voor data/number.txt
naar de nieuwe blob wijzen.
~/alpha $ git commit -m 'a2'
[master f0af7e6] a2
De gebruiker commit. De stappen voor de commit zijn hetzelfde als eerst:
Als eerste wordt er een nieuwe tree graph aangemaakt die de inhoud van de index vertegenwoordigt.
De index entry voor
data/number.txt
is veranderd. De oudedata
tree geeft niet langer de geïndexeerde toestand van dedata
directory weer. Er moet een nieuwdata
tree object worden gecreëerd:100664 blob 2e65efe2a145dda7ee51d1741299f848e5bf752e letter.txt 100664 blob d8263ee9860594d2806b0dfd1bfd17528b0ba2a4 number.txt
De nieuwe
data
tree geeft een andere hash-waarde dan de oudedata
tree. Er moet een nieuweroot
tree worden gecreëerd om deze hash op te slaan:040000 tree 40b0318811470aaacc577485777d7a6780e51f0b data
Als tweede wordt er een nieuw commit-object aangemaakt.
tree ce72afb5ff229a39f6cce47b00d1b0ed60fe3556 parent 774b54a193d6cfdd081e581a007d2e11f784b9fe author Mary Rose Cook <mary@maryrosecook.com> 1424813101 -0500 committer Mary Rose Cook <mary@maryrosecook.com> 1424813101 -0500 a2
De eerste regel van het commit-object wijst naar het nieuwe
root
tree object.
De tweede verwijst naara1
: de parent van de commit. Om de parent van de commit te vinden is Git naarHEAD
gegaan, volgde die naarmaster
en vond daar de commit hash vana1
.Als derde wordt de inhoud van de
master
branch file gewijzigd in de hash van de nieuwe commit.
Graph eigenschap I.: Inhoud wordt als een object-tree opgeslagen.
Dit betekent dat alleen diffs in de objects-database worden opgeslagen.
Bekijk bijvoorbeeld de bovenstaande graph. De a2
commit hergebruikt
de a
blob die eerder dan de a1
commit was aangemaakt. Evenzo kunnen, als een hele directory van commit tot
commit niet verandert, de tree ervan en alle blobs en trees eronder worden hergebruikt.
In het algemeen zijn er van commit tot commit weinig inhoudelijke veranderingen.
Zodoende kan Git grote commit-histories opslaan in een kleine ruimte.
Graph eigenschap II.: Elke commit heeft een parent. Zo kan een repository de geschiedenis van een project opslaan.
Graph eigenschap III.: Refs zijn toegangspunten tot het een of andere
gedeelte van de commit historie. Dus kunnen er aan commits betekenisvolle namen worden gegeven.
De gebruiker organiseert zijn/haar werk in afstammingslijnen (lineage) die in het project
betekenisvol zijn, zoals met concrete refs als fix-for-bug-376
.
Git gebruikt symbolische refs als HEAD
, MERGE_HEAD
en FETCH_HEAD
ter ondersteuning van commando's die de commit historie manipuleren.
Graph eigenschap IV.: De nodes in de objects/
directory zijn immutable. Dit betekent dat inhoud wordt aangepast, niet
gewist. Elk stuk informatie dat ooit is toegevoegd en elke commit die ooit is gedaan kan
ergens in de objects
directory3 worden teruggevonden.
Graph eigenschap V.: Refs zijn mutable. Daarom kan de betekenis van een ref veranderen. De commit waar master
naar wijst kan de beste versie zijn van een project op dit moment, maar die positie kan binnen de kortste keren
worden ingenomen door een nieuwere en betere commit.
Graph eigenschap VI.: De werkkopie en de commits waar door refs naar wordt gewezen zijn direkt beschikbaar, maar andere commits zijn dat niet. Dit betekent dat recente geschiedenis gemakkelijk kan worden opgeroepen, maar ook dat die vaker verandert. Oftewel: Git heeft een vervagend geheugen dat met steeds moeizamer middelen moet worden opgeroepen.
De werkkopie is het punt in de historie dat het gemakkelijkst is terug te vinden omdat het zich in de root van de repository bevindt. Er is niet eens een Git commando voor nodig. Maar het is ook het veranderlijkste punt in de geschiedenis. De gebruiker kan tientallen versies van een bestand maken zonder dat Git er een van opslaat, tenzij ze aan Git worden toegevoegd.
De commit waar HEAD
naar wijst is erg gemakkelijk op te roepen.
Deze is aan de basis van de branch die is uitgecheckt. Om de inhoud ervan te zien hoeft de gebruiker alleen maar
te "stashen"4 en dan de werkkopie te bekijken. Tegelijkertijd is HEAD
de meest veranderlijke ref.
Ook de commit waar een concrete ref naar verwijst is gemakkelijk op te roepen. de gebruiker
kan die branch eenvoudig uitchecken. The top van een branch verandert minder vaak dan
HEAD
, maak vaak genoeg om de betekenis van een branchnaam veranderlijk te laten zijn.
Het is moeilijk om een commit waar door geen enkele ref naar wordt verwezen terug te roepen. Hoe verder de gebruiker verwijdert raakt van een ref, des te moeilijker het voor hem/haar wordt de betekenis van een commit te reconstrueren. Maar ook, hoe verder hij/zij teruggaat, des te minder waarschijnlijk het is dat de een of ander de geschiedenis zal hebben veranderd sinds de vorige keer dat hij/zij keek5.
Het uitchecken van een commit
~/alpha $ git checkout 37888c2
You are in 'detached HEAD' state...
De gebruiker checkt de a2
commit uit door de hash ervan te gebruiken.
(Als u deze Git commando's uitvoert, zal deze niet werken. Gebruik git log
om de hash van uw a2
commit te vinden.)
Uitchecken gaat in vier stappen:
-
Ten eerste krijgt Git de
a2
commit en ook de tree graph waar die naar wijst. Ten tweede schrijft het de file entries in de tree graph naar de werkkopie. Dit levert geen veranderingen op. De werkkopie heeft de inhoud van de tree graph al die er naartoe is geschreven omdat
HEAD
viamaster
al naar dea2
commit wees.Ten derde schrijft Git de file entries in de tree graph naar de index. Ook dit resulteert niet in veranderingen. De index heeft de inhoud van de
a2
commit al.Ten vierde wordt de inhoud van
HEAD
gewijzigd in de hash van dea2
commit:f0af7e62679e144bb28c627ee3e8f7bdb235eee9
Het wijzigen van de inhoud van
HEAD
in een hash brengt de repository in the "detachedHEAD
" toestand. Merk bij de hier volgende graph op datHEAD
direct naar dea2
commit wijst, in plaats van naarmaster
te wijzen.~/alpha $ printf '3' > data/number.txt ~/alpha $ git add data/number.txt ~/alpha $ git commit -m 'a3' [detached HEAD 3645a0e] a3
Na het uitchecken verandert de gebruiker de inhoud van data/number.txt
in 3
en commit de verandering. Git gaat naar HEAD
om de parent van de a3
commit te krijgen. Inplaats van een branch-ref te vinden en te volgen, vindt en retourneert het de hash van de a2
commit.
Git past HEAD
zodanig aan dat deze direct naar de hash van de nieuwe a3
commit wijst. De repository bevindt zich nog steeds in de detached HEAD
toestand. De nieuwe commit bevindt zich niet op een branch omdat er geen enkele commit wijst naar of a3
of naar een van diens afstammelingen. Daarom kan deze gemakkelijk kwijtraken.
Van nu af aan zullen trees en blobs over het algemeen worden weggelaten bij de graph diagrammen.
Het maken van een branch
~/alpha $ git branch deputy
De gebruiker creëert een nieuwe branch met de naam deputy
. Dit maakt gewoon een nieuw bestand aan in .git/refs/heads/deputy
dat de hash bevat waar HEAD
naar wijst: de hash van de a3
commit.
Graph property: branches zijn gewoon refs en refs zijn gewoon bestanden. Git branches zijn dus lichtgewicht.
Het aanmaken van de deputy
branch plaatst de nieuwe a3
commit veilig op een branch. HEAD
is nog steeds "detached" omdat deze nog steeds direct naar een commit verwijst.
Het uitchecken van een branch
~/alpha $ git checkout master
Switched to branch 'master'
De gebruiker checkt de master
branch uit:
Eerst krijgt Git de
a2
commit waarmaster
naar wijst, en krijgt de tree graph waar de commit points naar verwijst.Dan, als tweede, schrijft Git de file entries in de tree graph naar de bestanden van de werkkopie. De inhoud van
data/number.txt
wordt hierdoor2
.Ten derde schrijft Git de file entries in de tree graph naar de index. Dit updatet de entry voor
data/number.txt
naar de hash van de2
blob.Als vierde laat Git
HEAD
naarmaster
wijzen door de inhoud ervan te veranderen van een hash naar:ref: refs/heads/master
Het uiteindelijke resultaat:
Het uitchecken van een branch die incompatibel is met de werkkopie
~/alpha $ printf '789' > data/number.txt
~/alpha $ git checkout deputy
Your changes to these files would be overwritten
by checkout:
data/number.txt
Commit your changes or stash them before you
switch branches.
De gebruiker verandert de inhoud van data/number.txt
per ongeluk in 789
. Daarna wordt geprobeerd deputy
uit te checken. Git voorkomt het uitchecken.
HEAD
wijst naar master
die naar a2
verwijst waar data/number.txt
als inhoud 2
heeft. deputy
wijst naar a3
waar data/number.txt
als inhoud 3
heeft. De werkkopie versie van data/number.txt
heeft als inhoud 789
. Al deze versies zijn verschillend, en de verschillen moeten worden opgelost.
Git zou de werkkopie versie van data/number.txt
kunnen vervangen door de versie in de commit die wordt uitgecheckt. Maar het vermijdt dataverlies tegen elke prijs.
Git zou de werkkopie versie kunnen samenvoegen ("mergen") met de versie die wordt uitgecheckt. Maar dat is ingewikkeld.
Dus Git breekt de check out af.
~/alpha $ printf '2' > data/number.txt
~/alpha $ git checkout deputy
Switched to branch 'deputy'
De gebruiker heeft nu door dat hij/zij per ongeluk data/number.txt
heeft gewijzigd, en verandert de inhoud terug naar 2
. deputy
wordt nu succesvol uitgecheckt.
Merge met een voorganger (ancestor)
~/alpha $ git merge master
Already up-to-date.
De gebruiker voegt master
samen met deputy
. Mergen van twee branches betekent het mergen van twee commits. De eerste commit is die waar deputy
naar wijst: de ontvanger (receiver). De tweede commit is die waar master
naar wijst: de donor (giver). In dit geval doet Git niets bij de merge. Het meldt dat het Already up-to-date.
is.
Graph property: de serie commits in de graph wordt opgevat als een serie veranderingen uitgevoerd op de inhoud van de repository. Dit houdt in dat bij een merge, wanneer de giver commit een ancestor van de receiver commit is, Git niets zal doen. De veranderingen zijn al verdisconteerd.
Merge met een afstammeling (descendent)
~/alpha $ git checkout master
Switched to branch 'master'
De gebruiker checkt master
uit.
~/alpha $ git merge deputy
Fast-forward
deputy
wordt samengevoegd met master
. Git ontdekt dat de receiver commit, a2
, een ancestor is van de giver commit, a3
. Het kan een fast-forward merge uitvoeren.
Het gaat naar de giver commit en krijgt de tree graph waar die naar wijst.
Het schrijft de file entries in de tree graph naar de werkkopie en de
index. Het “fast-forwards” master
door die naar a3
te laten wijzen.
Graph property: de serie commits in the graph
wordt beschouwd als een serie veranderingen uitgevoerd op de inhoud van de
repository. Dit betekent dat bij een merge, wanneer de giver een descendent van
de receiver is, de geschiedenis niet veranderd is. Er is al een serie commits
die de te maken verandering beschrijft: de serie commits
tussen de receiver en de giver. Maar hoewel de Git historie niet
verandert, doet de Git graph dat wel. De concrete ref waar HEAD
naar wijst wordt geupdate zodat die nu naar de giver commit wijst.
Merge van twee commits van verschillende afstamming (lineages)
~/alpha $ printf '4' > data/number.txt
~/alpha $ git add data/number.txt
~/alpha $ git commit -m 'a4'
[master 7b7bd9a] a4
De gebruiker verandert nu de inhoud van number.txt
in 4
en commit de verandering naar master
.
~/alpha $ git checkout deputy
Switched to branch 'deputy'
~/alpha $ printf 'b' > data/letter.txt
~/alpha $ git add data/letter.txt
~/alpha $ git commit -m 'b3'
[deputy 982dffb] b3
De gebruiker checkt deputy
uit. De inhoud van data/letter.txt
wordt veranderd in b
, en de verandering wordt naar deputy
gecommit.
Graph property: commits kunnen voorgangers (parents) delen. Zodoende kunnen nieuwe afstammingslijnen (lineages) in de commit historie worden gecreëerd.
Graph property: commits kunnen meerdere ouders (parents) hebben. Zo kunnen gescheiden afstammingslijnen (lineages) door een commit met twee ouders worden verenigd (joined): een merge commit.
~/alpha $ git merge master -m 'b4'
Merge made by the 'recursive' strategy.
De gebruiker voegt master
toe in deputy
.
Git stelt vast dat de ontvanger (receiver), b3
, en de donor (giver), a4
, zich in verschillende afstammingslijnen (lineages) bevinden. Het maakt een merge commit. Dit proces heeft acht stappen:
Stap een A, Git schrijft de hash van de giver commit naar een bestand
alpha/.git/MERGE_HEAD
. De aanwezigheid van dit bestand vertelt Git dat het midden in het merge proces zit.Stap twee A, Git vindt de basis (base) commit: de meest recente voorouder die de receiver en giver commits gemeenschappelijk hebben.
Graph property: commits hebben parents. Dit betekent dat er een punt gevonden kan worden waar de twee afstammingslijnen zich splitsten. Git zoek terug in de tijd van
b3
om alle voorouders daarvan te vinden, en terug in de tijd vana4
om alle voorouders daarvan te vinden. De meest recente voorouder die beide afstammingslijnen delen,a3
, wordt de base commit.Stap drie A, Git genereert the indices voor de base, receiver en giver commits vanuit hun tree graphs.
Stap vier A, Git genereert een diff die de veranderingen combineert die bij de base commit zijn aangebracht door de receiver commit en de giver commit. Deze diff is een lijst bestandspaden die naar een verandering wijzen: add, remove, modify of conflict.
Git verzamelt de lijst van al de bestanden die voorkomen in de base, receiver of giver indices. Voor elk daarvan vergelijkt het de index entries om te beslissen wat er aan het bestand moet worden veranderd. Het schrijft een corresponderende entry naar de diff. In dit geval heeft de diff twee entries.
De eerste entry is die voor
data/letter.txt
. De inhoud van dit bestanda
in de base,b
in de receiver ena
in de giver. De inhoud verschilt tussen de base en de receiver. Maar identiek tussen de base en de giver. Git ziet dat de inhoud door de receiver is veranderd, maar niet door de giver. De diff entry voordata/letter.txt
is een modificatie, niet een conflict.De tweede entry in the diff is die voor
data/number.txt
. In dit geval is de inhoud tussen base en receiver gelijk, maar verschillend in de giver. De diff entry voordata/letter.txt
is ook een modificatie.Graph property: het is mogelijk de base commit van een merge te vinden. Dit betekent dat wanneer een bestand ten opzichte van de base alleen veranderd is in of de receiver of de giver, Git de merge van dat bestand automatisch kan oplossen. Dit bespaart de gebruiker werk dat hij/zij zou moeten doen.
Stap vijf A, de veranderingen die door de entries in de diff worden aangegeven worden doorgevoerd op de werkkopie. De inhoud van
data/letter.txt
wordtb
, de inhoud vandata/number.txt
wordt veranderd in4
.Stap zes A, de veranderingen die door de entries in the diff worden aangegeven worden toegepast op de index. De entry voor
data/letter.txt
verwijst nu naar deb
blob en de entry voordata/number.txt
wijst naar de4
blob.Stap zeven A, de geupdate index wordt gecommit:
tree 20294508aea3fb6f05fcc49adaecc2e6d60f7e7d parent 982dffb20f8d6a25a8554cc8d765fb9f3ff1333b parent 7b7bd9a5253f47360d5787095afc5ba56591bfe7 author Mary Rose Cook <mary@maryrosecook.com> 1425596551 -0500 committer Mary Rose Cook <mary@maryrosecook.com> 1425596551 -0500 b4
Merk op dat de commit twee parents heeft.
Stap acht A, Git laat de huidige branch,
deputy
, nu naar de nieuwe commit wijzen.
Het mergen van twee commits van verschillende afstammingslijnen (lineages) die beide het zelfde bestand modificeren
~/alpha $ git checkout master
Switched to branch 'master'
~/alpha $ git merge deputy
Fast-forward
De gebruiker checkt master
uit. deputy
wordt in master
gemergd. Dit "fast-forwards" master
naar de b4
commit. master
en deputy
wijzen nu naar dezelfde commit.
~/alpha $ git checkout deputy
Switched to branch 'deputy'
~/alpha $ printf '5' > data/number.txt
~/alpha $ git add data/number.txt
~/alpha $ git commit -m 'b5'
[deputy bd797c2] b5
De gebruiker checkt deputy
uit. De inhoud van data/number.txt
wordt veranderd in 5
, en de verandering naar deputy
gecommit.
~/alpha $ git checkout master
Switched to branch 'master'
~/alpha $ printf '6' > data/number.txt
~/alpha $ git add data/number.txt
~/alpha $ git commit -m 'b6'
[master 4c3ce18] b6
De gebruiker checkt master
uit. De inhoud van data/number.txt
wordt veranderd in 6
, en de verandering naar master
gecommit.
~/alpha $ git merge deputy
CONFLICT in data/number.txt
Automatic merge failed; fix conflicts and
commit the result.
De gebruiker merget deputy
in master
. Er is een
conflict en de merge wordt afgebroken.
Het proces bij een conflicterende merge volgt dezelfde eerste zes stappen als het proces voor een
conflictloze merge: maak .git/MERGE_HEAD
aan, vind de base
commit, genereer the indices voor de base, receiver en giver commits,
creëer een diff, update de werkkopie en update de index.
Vanwege het conflict worden de zevende commitstap en achtste ref-update-stap nooit genomen.
Laten we de acht stappen nog eens doornemen en zien wat er gebeurt:
Stap een B, Git schrijft de hash van de donor (giver) commit naar een bestand
.git/MERGE_HEAD
.Stap twee B, Git vindt the base commit,
b4
.Stap drie B, Git genereert de indices voor de base, receiver en giver commits.
Stap vier B, Git genereert een diff die de veranderingen gemaakt in de base door de receiver commit en de giver commit combineert. Deze diff is een lijst van bestandspaden die naar een verandering wijzen: add, remove, modify of conflict.
In dit geval bevat de diff slechts een entry:
data/number.txt
. Deze entry is gemarkeerd als een conflict, omdat de inhoud vandata/number.txt
verschillend is in de receiver, giver en base.Stap vijf B, de veranderingen aangegeven door de entries in de diff worden toegepast op de werkkopie. In het geval van een conflicterend onderdeel, worden beide versies door Git naar het bestand in de werkkopie geschreven. De inhoud van
data/number.txt
wordt dan:<<<<<<< HEAD 6 ======= 5 >>>>>>> deputy
Stap zes B, de veranderingen die door de entries in de diff worden aangegeven worden toegepast op de index. Entries in de index worden uniek geïdenticeerd door een combinatie van hun bestandspad en wachtcode (stage). De entry voor een conflictvrij bestand heeft een wachtcode
(Git kent een "staging area", waar u bestanden klaar zet om te worden gecommit. Ik vertaal dat met "in de wacht zetten". Hans)0
. Voor deze merge zag de index er aldus uit (de0
-len zijn wachtcodes (stage values):
0 data/letter.txt 63d8dbd40c23542e740659a7168a0ce3138ea748 0 data/number.txt 62f9457511f879886bb7728c986fe10b0ece6bcb
Nadat de merge diff naar de index is geschreven ziet deze de index er zo uit:
0 data/letter.txt 63d8dbd40c23542e740659a7168a0ce3138ea748 1 data/number.txt bf0d87ab1b2b0ec1a11a3973d2845b42413d9767 2 data/number.txt 62f9457511f879886bb7728c986fe10b0ece6bcb 3 data/number.txt 7813681f5b41c028345ca62a2be376bae70b7f61
De entry voor
data/letter.txt
met wachtcode0
is hetzelfde als deze voor de merge was. De entry voordata/number.txt
met wachtcode0
is verdwenen. Er zijn drie nieuwe entries voor in de plaats gekomen.
De entry met wachtcode1
bevat de hash van de basedata/number.txt
inhoud.
De entry met wachtcode2
bevat de hash van de receiverdata/number.txt
inhoud.
De entry met wachtcode3
bevat de hash van de giverdata/number.txt
inhoud.
De aanwezigheid van deze drie entries vertelt Git dat er voordata/number.txt
een conflict bestaat.De merge wordt onderbroken.
~/alpha $ printf '11' > data/number.txt ~/alpha $ git add data/number.txt
De gebruiker integreert de inhoud van de twee conflicterende versies door de inhoud van
data/number.txt
te veranderen in11
. Het bestand wordt aan de index toegevoegd. Git voegt een blob toe met als inhoud11
. Het toevoegen van een conflicterend bestand is voor Git het teken dat het conflict is opgelost. Git verwijdert dedata/number.txt
entries met wachtcodes1
,2
en3
uit de index. Het voegt een entry toe voordata/number.txt
met wachtcode0
, met de hash van de nieuwe blob als inhoud. De index ziet er nu uit als:0 data/letter.txt 63d8dbd40c23542e740659a7168a0ce3138ea748 0 data/number.txt 9d607966b721abde8931ddd052181fae905db503
~/alpha $ git commit -m 'b11' [master 251a513] b11
Stap zeven B, de gebruiker commit. Git ziet
.git/MERGE_HEAD
in de repository, waardoor het weet dat er een merge aan de gang is. Het checkt de index en vindt dat er geen conflicten meer zijn. Het creëert een nieuwe commit,b11
, om de inhoud van de opgeloste merge vast te leggen. Het wist het bestand.git/MERGE_HEAD
. Dit completeert de merge.Stap acht B, Git laat de huidige branch,
master
, naar de nieuwe commit wijzen.
Het verwijderen van een bestand
Dit diagram van de Git graph bevat zowel commit historie, de trees en blobs voor de laatste commit, als de werkkopie en index:
~/alpha $ git rm data/letter.txt
rm 'data/letter.txt'
De gebruiker vraagt Git data/letter.txt
te verwijderen. Het bestand wordt verwijderd uit de werkkopie. De entry wordt gewist uit de index.
~/alpha $ git commit -m '11'
[master d14c7d2] 11
De gebruiker commit. Als onderdeel van de commit bouwt Git als altijd een tree graph op die de inhoud van de index representeert. data/letter.txt
is niet opgenomen in de tree graph omdat het niet meer in de index staat.
Het kopiëren van een repository
~/alpha $ cd ..
~ $ cp -R alpha bravo
De gebruiker kopiëert de inhoud van de alpha/
repository naar de bravo/
directory. Dit produceert de volgende directory structuur:
~
├── alpha
| └── data
| └── number.txt
└── bravo
└── data
└── number.txt
Er is nu een tweede Git graph in de bravo
directory:
Het koppelen van een repository aan een andere repository
~ $ cd alpha
~/alpha $ git remote add bravo ../bravo
De gebruiker gaat terug naar de alpha
repository. bravo
wordt als een remote repository van alpha
aangewezen. Dit voegt een paar regels toe aan het bestand alpha/.git/config
:
[remote "bravo"]
url = ../bravo/
Deze regels houden in dat er nu een remote repository bravo
is in de directory ../bravo
.
Het binnenhalen (fetch) van een branch van een remote
~/alpha $ cd ../bravo
~/bravo $ printf '12' > data/number.txt
~/bravo $ git add data/number.txt
~/bravo $ git commit -m '12'
[master 94cd04d] 12
De gebruiker gaat naar de bravo
repository. Hij/zij verandert de inhoud data/number.txt
in 12
en commit de verandering naar master
op bravo
.
~/bravo $ cd ../alpha
~/alpha $ git fetch bravo master
Unpacking objects: 100%
From ../bravo
* branch master -> FETCH_HEAD
De gebruiker gaat nu naar de alpha
repository. Hij/zij haalt master
van bravo
binnen in alpha
. Dit proces heeft vier stappen:
Een. Git verkrijgt de hash van de commit waar master op
bravo
naar wijst. Dit is de hash van de12
commit.Twee. Git maakt een lijst van alle objecten waar de
12
commit vanaf hangt: het commit object zelf, de objecten in de tree graph daarvan, de ancestor commits van de12
commit en de objecten in hun tree graphs. Het verwijdert elk object van deze lijst dat al in dealpha
object database aanwezig was. Het kopiëert de rest naaralpha/.git/objects/
.Drie. De inhoud van de concrete ref file
alpha/.git/refs/remotes/bravo/master
wordt veranderd in de hash van de12
commit.Vier. De inhoud
alpha/.git/FETCH_HEAD
wordt nu:94cd04d93ae88a1f53a4646532b1e8cdfbc0977f branch 'master' of ../bravo
Dit betekent dat het meest recente fetch commando de
12
commit vanmaster
uitbravo
binnenhaalde.
Graph property: objecten kunnen worden gekopiëerd. Dat betekent dat historie tussen repositories kan worden gedeeld.
Graph property: een repository kan remote branch refs zoals alpha/.git/refs/remotes/bravo/master
opslaan.
Dit betekent dat een repository lokaal de toestand van een branch
in een remote repository kan bijhouden. Deze is correct op het moment van de fetch, maar zal
uit de pas gaan lopen als de remote branch verandert.
Het mergen met FETCH_HEAD
~/alpha $ git merge FETCH_HEAD
Updating d14c7d2..94cd04d
Fast-forward
De gebruiker mergt met FETCH_HEAD
. FETCH_HEAD
is gewoon een andere ref. Hij verwijst naar de 12
commit, de giver. HEAD
wijst naar de 11
commit, de receiver. Git doet een fast-forward merge en laat master
naar de 12
commit wijzen.
Het pullen van een branch van een remote
~/alpha $ git pull bravo master
Already up-to-date.
De gebruiker pullt master
van bravo
in alpha
. "Pull" is een verkorte schrijfwijze voor “fetch and merge FETCH_HEAD
”. Git voert deze twee commando's uit en rapporteert dat master
Already up-to-date
is.
Het clonen van een repository
~/alpha $ cd ..
~ $ git clone alpha charlie
Cloning into 'charlie'
De gebruiker gaat naar de bovengelegen directory. Dan wordt alpha
naar charlie
gecloned. Het clonen naar charlie
heeft een vergelijkbaar resultaat als de cp
die de gebruiker eerder uitvoerde om de bravo
repository te produceren. Git maakt een nieuwe directory aan met de naam charlie
. Het initialiseert charlie
als een Git repo, voegt alpha
toe als een remote met de naam origin
, fetcht origin
en mergt FETCH_HEAD
.
Het pushen van een branch naar een uitgecheckte branch op een remote
~ $ cd alpha
~/alpha $ printf '13' > data/number.txt
~/alpha $ git add data/number.txt
~/alpha $ git commit -m '13'
[master 3238468] 13
De gebruiker gaat terug naar de alpha
repository. De inhoud van data/number.txt
wordt veranderd in 13
, en de verandering wordt gecommit naar master
op alpha
.
~/alpha $ git remote add charlie ../charlie
charlie
wordt een remote repository van alpha
gemaakt.
~/alpha $ git push charlie master
Writing objects: 100%
remote error: refusing to update checked out
branch: refs/heads/master because it will make
the index and work tree inconsistent
master
wordt naar charlie
gepusht.
Alle objecten die nodig zijn voor de 13
commit worden naar charlie
gekopiëerd.
Op dit punt wordt het push proces afgebroken. Zoals altijd vertelt Git de gebruiker
wat er mis ging. Het weigert te pushen naar een uitgecheckte branch op
de remote. Daar is wat voor te zeggen. Een push zou de remote index en HEAD
updaten. Dat zou verwarring veroorzaken als er iemand bezig was aan de werkkopie op de remote.
De gebruiker zou nu een nieuwe branch kunnen maken, de 13
commit erin mergen, en die branch naar charlie
kunnen pushen.
Maar eigenlijk wil hij/zij een repository waar op elk willekeurig moment naartoe gepusht kan worden. Een centrale repository waarheen gepusht en waarvan gepulled kan worden, maar waar verder niemand direkt naar commit. Eigenlijk iets als een GitHub remote. Hij/zij wil een "bare repository".
Het klonen van een bare repository
~/alpha $ cd ..
~ $ git clone alpha delta --bare
Cloning into bare repository 'delta'
De gebruiker gaat naar de bovengelegen directory. Hij/zij kloont delta
als een bare repository. Dit is een gewone clone, maar met twee verschillen. Het config
bestand geeft aan dat de repository "bare" is. En de bestanden die normaliter in de .git
directory worden opgeslagen worden nu opgeslagen in de root van de repository:
delta
├── HEAD
├── config
├── objects
└── refs
Het pushen van een branch naar een bare repository
~ $ cd alpha
~/alpha $ git remote add delta ../delta
De gebruiker gaat terug naar de alpha
repository. Van delta
wordt een remote repository op alpha
gemaakt.
~/alpha $ printf '14' > data/number.txt
~/alpha $ git add data/number.txt
~/alpha $ git commit -m '14'
[master cb51da8] 14
De inhoud van data/number.txt
wordt veranderd in 14
, en de verandering naar master
gecommit op alpha
.
~/alpha $ git push delta master
Writing objects: 100%
To ../delta
3238468..cb51da8 master -> master
Dan wordt master
naar delta
gepusht. Pushen heeft drie stappen:
Als eerste worden al de objecten die nodig zijn voor de
14
commit op demaster
branch gekopiëerd vanalpha/.git/objects/
naardelta/objects/
.Als tweede wordt
delta/refs/heads/master
aangepast zodat het naar de14
commit verwijst.Als derde gaat
alpha/.git/refs/remotes/delta/master
wijzen naar de14
commit (op alpha).alpha
heeft nu een up-to-date weergave van de toestand bijdelta
.
Samenvatting
Git is gebaseerd op een graph. Bijna elk Git commando manipuleert deze graph. Richt u, om Git diepgaand te begrijpen, op de eigenschappen van deze graph, niet op workflows of commando's.
Onderzoek om meer over Git te weten te komen de .git
directory.
Dat is niet eng. Kijk erin. Verander de inhoud van bestanden en kijk wat er gebeurt.
Creëer een commit met de hand. Probeer het uit en zie hoe grondig u een puinhoop kunt maken van een
repo. Repareer hem dan.
-
In dit geval is de hash langer dan de oorspronkelijke inhoud. Maar alle stukken inhoud die langer zijn dan het aantal karakters in een hash zullen beknopter worden weergegeven dan het origineel. ↩
-
Er is een kans dat twee verschillende stukken inhoud zullen hashen tot de zelfde waarde. Maar deze kans is klein. ↩
-
git prune
verwijdert alle objecten die niet kunnen worden bereikt vanaf een ref. Als de gebruiker dit commando uitvoert kan er inhoud verloren gaan. ↩ -
git stash
slaat alle verschillen tussen de werkkopie en deHEAD
commit op op een veilige plaats. Ze kunnen later worden teruggehaald. ↩ -
Het
rebase
commando kan gebruikt worden om commits in de historie toe te voegen, te wijzigen of te verwijderen. ↩