Git refs: An overview
Leer de basisprincipes van Git met deze tutorial met spaces.
Als je de vele manieren begrijpt waarop je naar een commit kunt verwijzen, zijn deze opdrachten allemaal veel nuttiger. In dit hoofdstuk werpen we wat licht op de interne werking van veelgebruikte opdrachten zoals git checkout
, git branch
en git push
door de vele methoden te verkennen om naar een commit te verwijzen.
We verkennen ook hoe je schijnbaar 'verloren' commits opnieuw kunt gebruiken door ze te openen via het reflog-mechanisme van Git.
Hashes
De meest directe manier om naar een commit te verwijzen is via de SHA-1-hash. Dit fungeert als de unieke ID voor elke commit. Je kunt de hash van al je commits vinden in de output van git log
.
commit 0c708fdec272bc4446c6cabea4f0022c2b616eba Author: Mary Johnson Date: Wed Jul 9 16:37:42 2014 -0500 Some commit message
Als je de commit doorgeeft aan andere Git-opdrachten, hoef je alleen maar genoeg tekens op te geven om de commit uniek te identificeren. Je kunt de bovenstaande commit bijvoorbeeld inspecteren met git show
door de volgende opdracht uit te voeren:
git show 0c708f
Soms is het nodig om een branch, tag of andere referentie op te lossen in de bijbehorende commit-hash. Hiervoor kun je de opdracht git rev-parse
gebruiken. Het volgende geeft de hash van de commit terug die wordt aangewezen door de main
-branch:
gerelateerd materiaal
Een volledige Git-repository verplaatsen
Oplossing bekijken
Git leren met Bitbucket Cloud
git rev-parse main
Dit is met name handig wanneer je aangepaste scripts schrijft die een commit-referentie accepteren. In plaats van de commit-referentie handmatig te parseren, kun je git rev-parse
de input voor je laten normaliseren.
Refs
Een ref is een indirecte manier om naar een commit te verwijzen. Je kunt het zien als een gebruiksvriendelijke alias voor een commit-hash. Dit is het interne mechanisme van Git om branches en tags weer te geven.
Refs worden opgeslagen als normale tekstbestanden in de map .git/refs
waar .git
gewoonlijk .git
wordt genoemd. Om de refs in een van je repository's te bekijken, navigeer je naar .git/refs
. Je zou de volgende structuur moeten zien, maar die bevat verschillende bestanden, afhankelijk van welke branches, tags en remotes je in je repo hebt:
.git/refs/ heads/ main some-feature remotes/ origin/ main tags/ v0.9
De heads
-map bevat alle lokale branches in je repository. Elke bestandsnaam komt overeen met de naam van de bijbehorende branch, en in het bestand vind je een commit-hash. Deze commit-hash is de locatie van de tip van de branch. Probeer de volgende twee opdrachten uit te voeren vanaf de root van de Git-repository om dit te verifiëren:
# Output the contents of `refs/heads/main` file: cat .git/refs/heads/main # Inspect the commit at the tip of the `main` branch: git log -1 main
De commit-hash die door de opdracht cat
wordt geretourneerd, moet overeenkomen met de commit-ID die wordt weergegeven in git log
.
Om de locatie van de main
-branch te wijzigen, hoeft Git alleen maar de inhoud van het refs/heads/main
-bestand te wijzigen. Zo is het aanmaken van een nieuwe branch gewoon een kwestie van een commit-hash naar een nieuw bestand schrijven. Dit is een deel van de reden waarom Git-branches zo licht zijn in vergelijking met SVN.
De map met tags
werkt op precies dezelfde manier, maar bevat tags in plaats van branches. De remotes
-map bevat alle externe repository's die je hebt gemaakt met git remote
als afzonderlijke submappen. In elke branch vind je alle externe branches die zijn opgehaald in je repository.
Specifying refs
Als je een ref doorgeeft aan een Git-opdracht, kun je ofwel de volledige naam van de ref opgeven, of een korte naam gebruiken en Git laten zoeken naar een bijbehorende ref. Je zou al bekend moeten zijn met korte namen voor refs, want dit gebruik je elke keer als je naar een branch verwijst met de naam ervan.
git show some-feature
Het argument some-feature
in de bovenstaande opdracht is eigenlijk een korte naam voor de branch. Git lost dit op naar refs/heads/some-feature
voordat het wordt gebruikt. Je kunt ook de volledige ref specificeren op de opdrachtregel, zoals het volgende:
git show refs/heads/some-feature
Dit voorkomt dubbelzinnigheid met betrekking tot de locatie van de ref. Dit is bijvoorbeeld nodig als je zowel een tag als een branch had met de naam some-feature
. Als je echter de juiste naamgevingsconventies gebruikt, zou dubbelzinnigheid tussen tags en branches over het algemeen geen probleem moeten zijn.
We zien meer volledige refs in de sectie Refspecs.
Packed refs
Voor grote repository's voert Git periodiek een afvalinzameling uit om onnodige objecten te verwijderen en refs in één bestand te comprimeren voor efficiëntere prestaties. Je kunt deze compressie forceren met de opdracht afvalinzameling:
git gc
Hiermee worden alle afzonderlijke branch- en tagbestanden in de map refs
verplaatst naar een één bestand met de naam packed-refs
, dat bovenaan staat in de .git
-map. Als je dit bestand opent, vind je een overzicht van commit-hashes naar refs:
00f54250cf4e549fdfcafe2cf9a2c90bc3800285 refs/heads/feature 0e25143693cfe9d5c2e83944bbaf6d3c4505eb17 refs/heads/main bb883e4c91c870b5fed88fd36696e752fb6cf8e6 refs/tags/v0.9
Van buitenaf wordt de normale Git-functionaliteit op geen enkele manier beïnvloed. Maar als je je afvraagt waarom je map .git/refs
leeg is, is dit de plek waar de refs zijn gebleven.
Special refs
Naast de map met refs
zijn er een paar speciale refs in de bovenste .git
-map. Ze worden hieronder vermeld:
HEAD
– De huidige uitgecheckte commit/branch.FETCH_HEAD
– De branch die het laatst is opgehaald uit een externe repo.ORIG_HEAD
– Een back-upreferentie naarHEAD
voordat deze drastisch wordt gewijzigd.MERGE_HEAD
– De commit(s) die je samenvoegt in de huidige branch metgit merge
.CHERRY_PICK_HEAD
– De commit voor 'cherry-picking'.
Deze refs worden allemaal aangemaakt en bijgewerkt door Git indien nodig. De opdracht git pull
voert bijvoorbeeld eerst git fetch
uit, die de referentie FETCH_HEAD
bijwerkt. Vervolgens wordt git merge FETCH_HEAD
uitgevoerd om het ophalen van de opgehaalde branches naar de repository te voltooien. Natuurlijk kun je deze allemaal gebruiken zoals elke andere ref, zoals je waarschijnlijk ook gedaan hebt met HEAD
.
Deze bestanden bevatten verschillende inhoud, afhankelijk van het type en de staat van je repository. De HEAD
ref kan ofwel een symbolic ref bevatten, wat gewoon een referentie is naar een andere ref in plaats van een commit-hash, of een commit-hash. Kijk bijvoorbeeld eens naar de inhoud van HEAD
als je op de main
-branch bent:
git checkout main cat .git/HEAD
Dit levert ref: refs/heads/main
op, wat betekent dat HEAD
verwijst naar de ref refs/heads/main
. Zo weet Git dat de main
-branch momenteel wordt bekeken. Als je naar een andere branch zou overstappen, zou de inhoud van HEAD
bijgewerkt worden zodat deze overeenkomt met de nieuwe branch. Maar als je een commit zou bekijken in plaats van een branch, dan zou HEAD
een commit-hash bevatten in plaats van een symbolic ref. Zo weet Git dat het zich in een afzonderlijke HEAD-toestand bevindt.
HEAD
is voor het grootste deel de enige referentie die je rechtstreeks zult gebruiken. De andere zijn over het algemeen alleen nuttig bij het schrijven van scripts op een lager niveau die je in de interne werkzaamheden van Git moet inpassen.
Refspecs
Een refspec koppelt een branch in de lokale repository aan een branch in een externe repository. Hierdoor is het mogelijk om externe branches te beheren met lokale Git-opdrachten en om wat geavanceerd gedrag van git push
en git fetch
te configureren.
Een refspec is gespecificeerd als [+]
<src>
:
<dst>
. De parameter <src>
is de bron-branch in de lokale repository, en de parameter <dst>
is de doel-branch in de externe repository. Het optionele +
-teken is bedoeld om de externe repository te dwingen een niet-'fast-forward' update uit te voeren.
Refspecs kan worden gebruikt in combinatie met de opdracht git push
om de externe branch een andere naam te geven. De volgende opdracht pusht bijvoorbeeld de main
-branch naar de origin
externe repo, zoals bij een gewone git-push
, maar deze gebruikt qa-main
als de naam voor de branch in de origin
repo. Dit is handig voor QA-teams die hun eigen branches naar een externe repo pushen.
git push origin main:refs/heads/qa-main
You can also use refspecs for deleting remote branches. This is a common situation for feature-branch workflows that push the feature branches to a remote repo (e.g., for backup purposes). The remote feature branches still reside in the remote repo after they are deleted from the local repo, so you get a build-up of dead feature branches as your project progresses. You can delete them by pushing a refspec that has an empty parameter, like so:
git push origin :some-feature
Dit is erg handig, want je hoeft niet in te loggen op je externe repository en de externe branch handmatig te verwijderen. Let op dat je vanaf Git v1.7.0 de markering --delete
kunt gebruiken in plaats van de bovenstaande methode. Het volgende heeft hetzelfde effect als de bovenstaande opdracht:
git push origin --delete some-feature
Door een paar regels aan het Git-configuratiebestand toe te voegen, kun je refspecs gebruiken om het gedrag van git fetch
te wijzigen. Git fetch
haalt standaard alle branches op in de externe repository. De reden hiervoor is de volgende sectie van het bestand .git/config
:
[remote "origin"] url = https://git@github.com:mary/example-repo.git fetch = +refs/heads/*:refs/remotes/origin/*
De regel fetch
vraagt git fetch
om alle branches van de origin
repo te downloaden. Maar voor sommige workflows zijn ze niet allemaal nodig. Veel workflows voor continue integratie hebben bijvoorbeeld alleen betrekking op de main
-branch. Om alleen de main
-branch op te halen, wijzig je de fetch
-regel zodat deze overeenkomt met het volgende:
[remote "origin"] url = https://git@github.com:mary/example-repo.git fetch = +refs/heads/main:refs/remotes/origin/main
Je kunt git push
ook op een vergelijkbare manier configureren. Als je bijvoorbeeld altijd de main
-branch wilt pushen naar qa-main
in de origin
remote (zoals we hierboven hebben gedaan), zou je het configuratiebestand wijzigen in:
[remote "origin"] url = https://git@github.com:mary/example-repo.git fetch = +refs/heads/main:refs/remotes/origin/main push = refs/heads/main:refs/heads/qa-main
Refspecs geven je volledige controle over hoe verschillende Git-opdrachten branches tussen repository's overdragen. Ze laten je branches hernoemen en verwijderen uit je lokale repository, naar/vanuit branches met verschillende namen fetchen/pushen, en git push
en git fetch
configureren zodat je alleen met de branches werkt die je wilt.
Relative refs
Je kunt ook verwijzen naar commits met betrekking tot een andere commit. Met het teken ~
kun je de bovenliggende commits bereiken. Het volgende toont bijvoorbeeld het hoogst gelegen van HEAD
:
git show HEAD~2
Maar als je met merge-commits werkt, wordt het wat ingewikkelder. Aangezien merge-commits meer dan één bovenliggende optie hebben, is er meer dan één pad dat je kunt volgen. Bij samenvoegingen in drie richtingen is de eerste bovenliggende afkomstig van de branch waar je actief was toen je de samenvoeging uitvoerde, en de tweede bovenliggende is van de branch die je hebt doorgegeven aan de opdracht git merge
.
Het teken ~
volgt altijd de eerste bovenliggende van een merge-commit. Als je een andere bovenliggende wilt volgen, moet je aangeven welke van de bovenliggende het teken ^
heeft. Als HEAD
bijvoorbeeld een merge-commit is, geeft het volgende de tweede bovenliggende van HEAD
als resultaat.
git show HEAD^2
Je kunt meer dan één ^
-teken gebruiken om meer dan één generatie te verplaatsen. Dit toont bijvoorbeeld het hoogst gelegen van HEAD
(ervan uitgaand dat het een merge-commit) die berust op de tweede bovenliggende.
git show HEAD^2^1
Om duidelijk te maken hoe ~
en ^
werken, laat de volgende afbeelding zien hoe je een willekeurige commit van A
kunt bereiken met behulp van relatieve referenties. In sommige gevallen zijn er meerdere manieren om een commit te bereiken.
Relatieve refs kunnen worden gebruikt met dezelfde opdrachten als waarmee een normale ref kan worden gebruikt. Alle volgende opdrachten gebruiken bijvoorbeeld een relatieve referentie:
# Only list commits that are parent of the second parent of a merge commit git log HEAD^2 # Remove the last 3 commits from the current branch git reset HEAD~3 # Interactively rebase the last 3 commits on the current branch git rebase -i HEAD~3
The reflog
De reflog is het vangnet van Git. Deze registreert bijna elke wijziging die je aanbrengt in je repository, ongeacht of je een momentopname hebt gemaakt of niet. Je kunt deze zien als een chronologische geschiedenis van alles wat je in je lokale repo hebt gedaan. Voer de opdracht git reflog
uit om de reflog te bekijken. Dit zou iets moeten opleveren dat er als volgt uitziet:
400e4b7 HEAD@{0}: checkout: moving from main to HEAD~2 0e25143 HEAD@{1}: commit (amend): Integrate some awesome feature into `main` 00f5425 HEAD@{2}: commit (merge): Merge branch ';feature'; ad8621a HEAD@{3}: commit: Finish the feature
Dat kan als volgt worden vertaald:
- Je hebt net
HEAD
uitgecheckt - Daarvoor heb je een commit-bericht gewijzigd
- Daarvoor heb je de
feature
-branch samengevoegd metmain
- Daarvoor heb je een momentopname gemaakt
Met de HEAD {
-syntaxis kun je verwijzen naar commits die zijn opgeslagen in de reflog. Het werkt ongeveer zoals de HEAD~
-referenties uit de vorige sectie, maar de
verwijst naar een item in de reflog in plaats van de commit-geschiedenis.
Je kunt dit gebruiken om terug te keren naar een toestand die anders verloren zou gaan. Laten we bijvoorbeeld zeggen dat je zojuist een nieuwe functie met git reset
hebt geschrapt. Dit ziet er ongeveer als volgt uit:
ad8621a HEAD@{0}: reset: moving to HEAD~3 298eb9f HEAD@{1}: commit: Some other commit message bbe9012 HEAD@{2}: commit: Continue the feature 9cb79fa HEAD@{3}: commit: Start a new feature
De drie commits voor de git reset
zijn nu losstaand, wat betekent dat er geen manier is om ernaar te verwijzen, behalve via een reflog. Stel dat je je realiseert dat je niet al je werk had moeten weggooien. Het enige wat je hoeft te doen is de HEAD@{1}
-commit bekijken om terug te gaan naar de status van je repository voordat je de git reset
uitvoerde.
git checkout HEAD@{1}
Dit plaatst de repo in een aparte HEAD
-status. Van hieruit kun je een nieuwe branch aanmaken en verder werken aan je functie.
Samenvatting
You should now be quite comfortable referring to commits in a Git repository. We learned how branches and tags were stored as refs in the .git
subdirectory, how to read a packed-refs
file, how HEAD
is represented, how to use refspecs for advanced pushing and fetching, and how to use the relative ~
and ^
operators to traverse a branch hierarchy.
We hebben ook naar de reflog gekeken, een manier om naar commits te verwijzen die op geen enkele andere manier beschikbaar zijn. Dit is een geweldige manier om van situaties te herstellen waar je denkt 'Oeps, had ik dat maar niet gedaan'.
Het doel van dit alles was om precies de commit te kunnen kiezen die je nodig hebt in een bepaald ontwikkelingsscenario. Het is heel eenvoudig om de vaardigheden die je in dit artikel hebt geleerd te gebruiken in combinatie met je bestaande Git-kennis, aangezien sommige van de meest voorkomende opdrachten refs accepteren als argumenten, waaronder git log
, git show
, git checkout
, git reset
, git revert
, git rebase
, en vele anderen.
Deel dit artikel
Volgend onderwerp
Aanbevolen artikelen
Bookmark deze resources voor meer informatie over soorten DevOps-teams of voor voortdurende updates over DevOps bij Atlassian.