Git refs: An overview
Poznaj podstawy środowiska Git dzięki kosmicznemu samouczkowi.
Rozumiejąc wiele sposobów odnoszenia się do commitów, możesz znacznie rozszerzyć możliwości każdego z tych poleceń. W tym rozdziale rzucimy nieco światła na wewnętrzne mechanizmy działania popularnych poleceń, takich jak git checkout
, git branch
i git push
, poznając wiele metod odnoszenia się do commitów.
Dowiemy się również, jak odzyskać pozornie „utracone” commity, uzyskując do nich dostęp za pośrednictwem mechanizmu dziennika reflog w Git.
Hashe
Najbardziej bezpośrednim sposobem odnoszenia się do commita jest użycie jego hasha SHA-1. Pełni on funkcję unikatowego identyfikatora każdego commita. Hash każdego commita możesz znaleźć w danych wyjściowych polecenia git log
.
commit 0c708fdec272bc4446c6cabea4f0022c2b616eba Author: Mary Johnson Date: Wed Jul 9 16:37:42 2014 -0500 Some commit message
Podczas przekazywania commita do innych poleceń Git musisz jedynie podać liczbę znaków wystarczającą do jednoznacznego zidentyfikowania commita. Możesz na przykład sprawdzić powyższy commit za pomocą polecenia git show
, wykonując następujące polecenie:
git show 0c708f
Czasami konieczne jest rozwiązanie gałęzi, tagu lub innej referencji pośredniej do odpowiadającego im hasha commita. Można do tego użyć polecenia git rev-parse
. Następujące polecenie zwraca hash commita wskazywanego przez gałąź main
:
materiały pokrewne
Jak przenieść pełne repozytorium Git
POZNAJ ROZWIĄZANIE
Poznaj środowisko Git z rozwiązaniem Bitbucket Cloud
git rev-parse main
Jest to szczególnie przydatne podczas pisania niestandardowych skryptów, które dopuszczają referencje commitów. Zamiast ręcznego analizowania referencji commita, możesz użyć polecenia git rev-parse
, które pozwala znormalizować dane wejściowe za Ciebie.
Odwołania
Referencja jest pośrednim sposobem odnoszenia się do commita. Można potraktować referencję jako przyjazną dla użytkownika alternatywę hasha commita. Jest to wewnętrzny mechanizm Git służący do reprezentowania gałęzi i tagów.
Referencje są przechowywane jako zwykłe pliki tekstowe w katalogu .git/refs
, gdzie plik o nazwie .git
jest zazwyczaj nazywany .git
. Aby przyjrzeć się referencjom w jednym ze swoich repozytoriów, przejdź do katalogu .git/refs
. Powinien on zawierać następującą strukturę, jednak w zależności od gałęzi, tagów oraz zdalnych repozytoriów, będzie on zawierać inne pliki:
.git/refs/ heads/ main some-feature remotes/ origin/ main tags/ v0.9
Katalog heads
definiuje wszystkie lokalne gałęzie w repozytorium. Każda nazwa pliku jest zgodna z nazwą odpowiedniej gałęzi, a wewnątrz pliku znajduje się hash commita. Taki hash commita jest lokalizacją końcówki gałęzi. Aby to sprawdzić, spróbuj wykonać następujące dwa polecenia z poziomu katalogu głównego repozytorium Git:
# 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
Hash commita zwrócony przez polecenie cat
powinien być zgodny z identyfikatorem commita wyświetlonym po użyciu polecenia git log
.
Aby zmienić lokalizację gałęzi main
, Git musi jedynie zmienić zawartość pliku refs/heads/main
. W podobny sposób utworzenie nowej gałęzi jest po prostu kwestią napisania hasha commita do nowego pliku. Częściowo dlatego gałęzie Git są tak proste w porównaniu z gałęziami SVN.
Katalog tags
działa dokładnie tak samo, tylko zamiast gałęzi zawiera tagi. Katalog remotes
zawiera listę wszystkich zdalnych repozytoriów utworzonych przy użyciu polecenia git remote
w formie osobnych podkatalogów. W każdym z nich znajdują się wszystkie gałęzie zdalne, które zostały pobrane do repozytorium.
Specifying refs
Przekazując referencję do polecenia Git, możesz zdefiniować pełną nazwę referencji lub użyć skróconej nazwy i pozwolić, aby Git wyszukał pasującą referencję. Skrócone nazwy referencji powinny już być Ci znane. Z tej właśnie funkcji korzystasz za każdym razem, gdy odwołujesz się do gałęzi po nazwie.
git show some-feature
Argument some-feature
w powyższym poleceniu jest w rzeczywistości skróconą nazwą gałęzi. Przed jej użyciem Git rozwiązuje ją do refs/heads/some-feature
. Możesz również podać pełną referencję w wierszu polecenia, jak w następującym przykładzie:
git show refs/heads/some-feature
Pozwala to uniknąć niejednoznaczności co do lokalizacji referencji. Jest to konieczne, jeśli na przykład masz tag i gałąź o nazwie some-feature
. Jeśli jednak używasz właściwych konwencji nazewnictwa, nie powinny występować problemy z niejednoznacznością tagów i gałęzi.
Więcej pełnych nazw referencji zobaczymy w sekcji Specyfikacje refspec.
Packed refs
W przypadku dużych repozytoriów Git przeprowadza okresowo operację usuwania zbędnych elementów w celu usunięcia niepotrzebnych obiektów i skompresowania referencji do jednego pliku, aby zwiększyć wydajność. Taką kompresję można wymusić za pomocą polecenia usuwania zbędnych elementów:
git gc
Spowoduje to przeniesienie wszystkich pojedynczych plików gałęzi i tagów w folderze refs
do jednego pliku o nazwie packed-refs
znajdującego się w katalogu najwyższego poziomu .git
. Jeśli otworzysz ten plik, znajdziesz mapowanie hashy commitów na referencje:
00f54250cf4e549fdfcafe2cf9a2c90bc3800285 refs/heads/feature 0e25143693cfe9d5c2e83944bbaf6d3c4505eb17 refs/heads/main bb883e4c91c870b5fed88fd36696e752fb6cf8e6 refs/tags/v0.9
Na zewnątrz nie wpływa to w żaden sposób na funkcjonalność Git. Jeśli jednak zastanawiasz się, dlaczego Twój folder .git/refs
jest pusty, tam właśnie trafiły referencje.
Special refs
Oprócz katalogu refs
, istnieje kilka specjalnych referencji, które znajdują się w katalogu najwyższego poziomu .git
. Zostały one wymienione poniżej:
HEAD
— aktualnie wyewidencjonowany commit lub aktualnie wyewidencjonowana gałąź.FETCH_HEAD
— ostatnio pobrana gałąź ze zdalnego repozytorium.ORIG_HEAD
— kopia zapasowa referencji do wskaźnikaHEAD
przed wprowadzeniem w nim drastycznych zmian.MERGE_HEAD
— commity, które scalasz z bieżącą gałęzią przy użyciu poleceniagit merge
.CHERRY_PICK_HEAD
— commit, na którym wykonujesz operację „cherry pick”.
Git tworzy i aktualizuje te wszystkie referencje w razie potrzeby. Przykładowo polecenie git pull
najpierw uruchamia polecenie git fetch
, które aktualizuje referencję FETCH_HEAD
. Następnie uruchamia polecenie git merge FETCH_HEAD
, aby zakończyć ściąganie pobranych gałęzi do repozytorium. Naturalnie każdej z tych referencji można używać tak, jak wszystkich innych referencji, co na pewno zdarzyło Ci się niejednokrotnie z HEAD
.
Te pliki zawierają różne treści w zależności od ich typu i stanu repozytorium. Referencja HEAD
może zawierać referencję symboliczną, czyli po prostu referencję do innej referencji zamiast hasha commita, lub hash commita. Przyjrzyjmy się zawartości wskaźnika HEAD
, gdy znajdujesz się w gałęzi main
:
git checkout main cat .git/HEAD
To polecenie zwróci wynik ref: refs/heads/main
, co oznacza, że HEAD
wskazuje na referencję refs/heads/main
. Dzięki temu Git wie, że gałąź main
jest aktualnie wyewidencjonowywana. Gdybyśmy przełączyli się na inną gałąź, zawartość wskaźnika HEAD
zostałaby zaktualizowana tak, aby odzwierciedlić nową gałąź. Gdybyśmy jednak wyewidencjonowali commit zamiast gałęzi, wskaźnik HEAD
będzie zawierał hash commita, a nie referencję symboliczną. Dzięki temu Git wie, że znajduje się w stanie odłączonego wskaźnika HEAD.
Najczęściej HEAD
jest jedyną referencją używaną bezpośrednio. Inne przydają się zasadniczo tylko podczas pisania skryptów niższego poziomu, które muszą odnosić się do wewnętrznych mechanizmów Git.
Refspecs
Specyfikacja refspec mapuje gałąź znajdującą się w lokalnym repozytorium na gałąź w zdalnym repozytorium. To umożliwia zarządzanie zdalnymi gałęziami przy użyciu lokalnych poleceń Git i konfigurowanie zaawansowanych sposobów działania poleceń git push
i git fetch
.
Specyfikację refspec definiuje się jako [+]
<src>
:
<dst>
. Parametr <src>
oznacza gałąź źródłową w repozytorium lokalnym, a parametr <dst>
oznacza gałąź docelową w repozytorium zdalnym. Opcjonalny znak +
służy do wymuszania wykonania aktualizacji innej niż fast-forward w repozytorium zdalnym.
Specyfikacji refspec można użyć w połączeniu z poleceniem git push
, aby nadać inną nazwę gałęzi zdalnej. Przykładowo następujące polecenie spowoduje wypchnięcie gałęzi main
do zdalnego repozytorium origin
, jak w przypadku zwykłego polecenia git push
, z tym że qa-main
zostanie wykorzystane jako nazwa gałęzi w repozytorium origin
. Jest to przydatne rozwiązanie w przypadku zespołów QA, które muszą wypychać własne gałęzie do zdalnego repozytorium.
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
Jest to bardzo wygodne, ponieważ nie trzeba logować się do repozytorium zdalnego i ręcznie usuwać gałęzi zdalnej. Pamiętaj, że począwszy od Git w wersji 1.7.0, możesz używać flagi --delete
zamiast powyższej metody. Następujące polecenie zadziała tak samo, jak to, które przedstawiono powyżej:
git push origin --delete some-feature
Dodając kilka wierszy do pliku konfiguracyjnego Git, możesz użyć specyfikacji refspec, aby zmienić sposób działania polecenia git fetch
. Domyślnie polecenie git fetch
pobiera wszystkie gałęzie ze zdalnego repozytorium. Odpowiada za to następująca sekcja pliku .git/config
:
[remote "origin"] url = https://git@github.com:mary/example-repo.git fetch = +refs/heads/*:refs/remotes/origin/*
Wiersz fetch
jest dla polecenia git fetch
instrukcją pobrania wszystkich gałęzi z repozytorium origin
. Jednak w niektórych przepływach pracy nie wszystkie one są potrzebne. Przykładowo wiele przepływów pracy opartych na ciągłej integracji bierze pod uwagę tylko gałąź main
. Aby pobrać samą gałąź main
, wystarczy zmodyfikować wiersz fetch
w następujący sposób:
[remote "origin"] url = https://git@github.com:mary/example-repo.git fetch = +refs/heads/main:refs/remotes/origin/main
W podobny sposób można również skonfigurować polecenie git push
. Jeśli na przykład chcesz, aby gałąź main
była zawsze wypychana do qa-main
w repozytorium zdalnym origin
(jak to zrobiliśmy powyżej), musisz zmodyfikować plik konfiguracyjny w następujący sposób:
[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
Specyfikacje refspec zapewniają pełną kontrolę nad sposobem przenoszenia gałęzi między repozytoriami za pomocą różnych poleceń Git. Umożliwiają one usuwanie gałęzi z lokalnego repozytorium i zmienianie ich nazw, pobieranie danych z gałęzi o różnych nazwach i wypychanie ich do nich, a także konfigurowanie poleceń git push
i git fetch
tak, aby działały tylko w przypadku konkretnych gałęzi.
Relative refs
Można także odnosić się do commitów względem innego commita. Znak ~
umożliwia dotarcie do commitów nadrzędnych. Przykładowo następujące polecenie spowoduje wyświetlenie elementu nadrzędnego w drugim stopniu względem commita HEAD
:
git show HEAD~2
Podczas pracy ze commitami scalenia sytuacja nieco się komplikuje. Commity scalenia mają więcej niż jeden element nadrzędny, istnieje więcej niż jedna ścieżka, którą można podążyć. W przypadku scaleń trójstronnych pierwszy element nadrzędny pochodzi z gałęzi, w której się znajdowano podczas wykonywania scalania, a drugi element nadrzędny — z gałęzi przekazanej do polecenia git merge
.
Znak ~
zawsze będzie podążał za pierwszym elementem nadrzędnym commita scalenia. Aby podążać za innym elementem nadrzędnym, trzeba wskazać jego numer za pomocą znaku ^
. Jeśli na przykład HEAD
jest commitem scalenia, poniższe polecenie zwróci drugi element nadrzędny HEAD
.
git show HEAD^2
Możesz użyć więcej niż jednego znaku ^
, aby cofnąć się o więcej niż jedną generację. Poniższe polecenie spowoduje na przykład wyświetlenie elementu nadrzędnego w drugim stopniu commita HEAD
(zakładając, że jest on commitem scalenia) względem drugiego elementu nadrzędnego.
git show HEAD^2^1
Aby wyjaśnić, jak działają znaki ~
i ^
, na poniższym rysunku przedstawiono, jak dotrzeć do dowolnego commita z punktu A
, korzystając z referencji względnych. W niektórych przypadkach istnieje wiele sposobów dotarcia do commita.
Referencji względnych można używać z tymi samymi poleceniami, co zwykłych referencji. Przykładowo wszystkie następujące polecenia obsługują referencję względną:
# 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
Dziennik reflog stanowi „siatkę asekuracyjną” systemu Git. Rejestruje on niemal każdą zmianę wprowadzoną w repozytorium, niezależnie od tego, czy migawka została zatwierdzona, czy nie. Można traktować go jako chronologiczną historię wszystkich czynności wykonanych w repozytorium lokalnym. Aby wyświetlić dziennik reflog, uruchom polecenie git reflog
. Zwrócony wynik powinien wyglądać mniej więcej następująco:
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
Można go zinterpretować w następujący sposób:
- Właśnie wyewidencjonowano commit
HEAD~2
. - Przed tą operacją zmieniono komunikat dotyczący commita.
- Przed tą operacją scalono gałąź
feature
z gałęziąmain
. - Przed tą operacją zatwierdzono migawkę.
Składnia HEAD{
pozwala odnosić się do commitów zapisanych w dzienniku reflog. Działa podobnie, jak referencje HEAD~
z poprzedniej sekcji, tylko
odnosi się do wpisu w dzienniku reflog, a nie do historii commitów.
Dzięki temu można przywrócić stan, który w innym przypadku byłby utracony. Powiedzmy na przykład, że właśnie usunęliśmy nową funkcję za pomocą polecenia git reset
. Dziennik reflog wyglądałby mniej więcej następująco:
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
Trzy commity przed wykonaniem polecenia git reset
są teraz w zawieszeniu, co oznacza, że nie da się do nich odnieść inaczej niż za pośrednictwem dziennika reflog. Teraz powiedzmy, że zdajesz sobie sprawę, że usunięcie całej pracy było błędem. Wystarczy, że wyewidencjonujesz commit HEAD@{1}
tak, aby cofnąć się do stanu repozytorium sprzed uruchomienia polecenia git reset
.
git checkout HEAD@{1}
Repozytorium zostanie ustawione w stanie odłączonego wskaźnika HEAD
. Z tego miejsca będzie można utworzyć nową gałąź i kontynuować pracę nad funkcją.
Podsumowanie
Zapewne czujesz się już dość komfortowo w kwestii odnoszenia się do commitów w repozytorium Git. Dowiedzieliśmy się, jak gałęzie i znaczniki są zapisywane jako referencje w podkatalogu .git
, jak odczytać plik packed-refs
, jak jest reprezentowany wskaźnik HEAD
, jak korzystać ze specyfikacji refspec w celu zaawansowanego wypychania i pobierania oraz jak używać operatorów względnych ~
i ^
do poruszania się po hierarchii gałęzi.
Przyjrzeliśmy się również dziennikowi reflog, który stanowi sposób odnoszenia się do commitów niedostępnych innymi sposobami. Jest to doskonały sposób odzyskiwania po sytuacjach, w których nagle uświadamiamy sobie, że popełniliśmy błąd.
Celem tego wszystkiego było umożliwienie wybrania dokładnie tego commita, który jest potrzebny w danej chwili, w dowolnym scenariuszu prac programistycznych. Bardzo łatwo jest wykorzystać umiejętności zdobyte podczas lektury tego artykułu w połączeniu z istniejącą wiedzą na temat Git, ponieważ referencje są akceptowane jako argumenty przez najczęściej używane polecenia, takie jak git log
, git show
, git checkout
, git reset
, git revert
, git rebase
i wiele innych.
Udostępnij ten artykuł
Następny temat
Zalecane lektury
Dodaj te zasoby do zakładek, aby dowiedzieć się więcej na temat rodzajów zespołów DevOps lub otrzymywać aktualności na temat metodyki DevOps w Atlassian.