Git #
Git jest rozproszonym systemem kontroli wersji. Dostarcza narzędzia do zarządzania repozytorium. Repozytorium to nic innego jak zbiór utrwalonych stanów katalogu roboczego. Taki utrwalony stan to commit.
Repozytorium zwykle ma postać folderu .git
przechowywanego wewnątrz katalogu roboczego.
W środku znajdują się pliki opisujące commity, w tym zapisana treść katalogu roboczego.
mkdir repo
cd repo
git init
ls -l .git
Takie polecenie utworzy w pustym katalogu roboczym puste repozytorium: niezawierające jeszcze żadnych zapisanych wersji.
-rw-rw-r-- 1 user user 92 mar 22 10:11 config # Lokalna konfiguracja repozytorium
-rw-rw-r-- 1 user user 73 mar 22 10:11 description # Czytelny opis repozytorium
-rw-rw-r-- 1 user user 23 mar 22 10:11 HEAD # Wskazanie na "bieżący" commit
drwxrwxr-x 4 user user 4096 mar 22 10:11 objects # Przechowywane obiekty: commity, stany plików, stany katalogów
drwxrwxr-x 4 user user 4096 mar 22 10:11 refs # Referencje, czyli nazwy dla commitów
Katalog roboczy razem z .git
można dowolnie przenosić i kopiować, .git
nie zawiera żadnych absolutnych ścieżek.
Usunięcie katalogu .git
usuwa repozytorium wraz ze wszystkimi informacjami kontroli wersji, ale oczywiście
nie dotyka samego katalogu roboczego.
Obiekty #
Repozytorium składa się z obiektów, przechowywanych w katalogu .git/objects
.
Git definiuje 4 rodzaje obiektów (blob, tree, commit, annotated tag), z czego najistotniejsze są pierwsze 3:
- blob: utrwalona zawartość jakiegoś pliku z katalogu roboczego
- tree: utrwalona zawartość katalogu (słownika)
- commit: utrwalony stan katalogu roboczego z dodatkowymi informacjami nt. wersji
Obiekty po utworzeniu są niemodyfikowalne, nie można zmieniać ich zawartości. Dzięki temu zawartość obiektów może posłużyć do ich identyfikacji! Git rozpoznaje obiekty na podstawie skrótu SHA-1 ich zawartości. Skrót zależy tylko i wyłącznie od zawartości. Zmiana zawartości = zmiana identyfikatora = inny obiekt.
Można ręcznie wyliczyć skrót pliku poleceniem hash-object
. Nie trzeba do tego posiadać nawet repozytorium:
echo Hi! > hi.txt
git hash-object hi.txt # 663adb09143767984f7be83a91effa47e128c735
Wyliczony skrót będzie taki sam na każdej maszynie, bo zawartość pliku jest taka sama. Zmieniając treść, zmienimy skrót.
Plik dodany do repozytorium, np. jako element commit’a zostanie umieszczony w katalogu objects jako obiekt typu blob i będzie identyfikowany jego skrótem.
cd repo
echo Hi! > hi.txt
git add hi.txt
git commit -m "Initial commit"
cat .git/objects/66/3adb09143767984f7be83a91effa47e128c735 | zlib-flate -uncompress | xxd
git cat-file blob 663ad
Katalog .git/objects
jest partycjonowany po dwóch pierwszych znakach skrótu.
Jak widać na powyższym przykładzie, do identyfikacji obiektów
wystarczy podać kilka pierwszych znaków sumy SHA1. Wystarczy tyle,
żeby nie było niejednoznaczności.
Utrwalenie stanu pliku nie jest wystarczające do wersjonowania projektu: trzeba zapisywać stany całych katalogów. Do tego służy obiekt typu tree.
Za pomocą polecenia git commit utworzyliśmy kilka obiektów:
cd repo
find .git/objects -type f
git cat-file -t 8da9
git ls-tree 8da9
Jeden z nich to właśnie tree. Jego zawartość to listing katalogu, którego stan opisuje.
100644 blob 663adb09143767984f7be83a91effa47e128c735 hi.txt
Nasz obiekt zawiera tylko jeden wpis, bo katalog roboczy miał tylko 1 plik w momencie wywołania polecenia git commit
.
Obiekt tree listuje elementy katalogu, każdy z nich jest innym obiektem git’a. Dla każdego obiektu zawiera:
- uprawnienia (100644 = rw-r–r–)
- typ (blob/tree)
- identyfikator obiektu (SHA1)
- nazwę obiektu w katalogu (hi.txt)
Tree może zawierać inne obiekty tree. Pozwala opisywać dowolnie zagnieżdżone struktury katalogów.
graph TD X[Commit<br>da14b73] --> B B[Tree<br>8dab03] B --> C[Blob<br>fe493f7] B --> D[Blob<br>28c7351] B --> E[Tree<br>9c75956] E --> F[Blob<br>ab2ea75] E --> G[Blob<br>db09143]
Jeżeli katalog zawiera kilka plików o tej samej zawartości, to tree będzie wielokrotnie listował ten sam obiekt.
cd repos
cp hi.txt hey.txt
git add hey.txt
git commit -m "Copied hi.txt"
git ls-tree a5250
100644 blob 663adb09143767984f7be83a91effa47e128c735 hey.txt
100644 blob 663adb09143767984f7be83a91effa47e128c735 hi.txt
Identyfikator obiektu tree to skrót SHA1 tej listy, zawierającej skróty zawieranych elementów. Zmiana zawartości hi.txt skutkuje zmianą jego skrótu, to z kolei spowoduje zmianę zawartości tree katalogu głównego i w konsekwencji zmianę jego skrótu.
Commity to główne obiekty repozytorium tworzone w momencie utrwalania wersji projektu.
git cat-file commit a7f3
tree a5250f7c6ad5260e28003ba5a0b1841b752918e3
parent 23d9585ca2a2fe493f79c75956ab4d815da14b73
author Paweł Sobótka <pawel.sobotka@pw.edu.pl> 1742665566 +0100
committer Paweł Sobótka <pawel.sobotka@pw.edu.pl> 1742665566 +0100
Copied hi.txt
Można z niego odczytać:
- identyfikator utrwalonego stan katalogu roboczego (
tree
) - identyfikatory poprzednich commitów (
parent
) - autor (
author
) - czas autorstwa (utrwalenia stanu) (
1742665566 +0100
) - committer (
commiter
) - czas commiterstwa (nałożenia commita) (
1742665566 +0100
) - wiadomość opisująca wersję (
Copied hi.txt
)
Commit zawiera stan całego projektu, a nie wprowadzone zmiany!
Podobnie jak wcześniej, SHA1 commit’a to skrót liczony za powyższe pola. Zmiana któregokolwiek wymusza powstanie nowego commita o innym SHA1.
Branche #
Posługiwanie się sumami SHA1 nie należy do najprzyjemniejszych dla człowieka.
Zamiast mówić: popatrz sobie na wersję 23d9585ca2a2fe493f79c75956ab4d815da14b73
łatwiej
byłoby mieć mnemoniczną nazwę dla rewizji. Takie referencje to znane powszechnie branche.
Branche można wylistować:
git branch -v
# * master a7f3d48 Copied hi.txt
Technicznie branche to pliki w folderze .git/refs/heads/
przechowujące SHA1 commita, który nazywają:
cat .git/refs/heads/master
# a7f3d48e135b4f4deb9a985ed7058b70491d7c71
Listowanie branchy to tak naprawdę listowanie tego katalogu. Tworzenie brancha wskazującego na jakiś commit po prostu tworzy podobny plik.
git branch feature a7f3d48
git branch -v
# feature a7f3d48 Copied hi.txt
# * master a7f3d48 Copied hi.txt
ls -l .git/refs/heads
# feature master
Stąd ważny fakt:
Branch to nic więcej jak nazwa dla commita!
Branche mogą być modyfikowalne. W momencie tworzenia nowego commita aktywny branch jest przepinany na nowy commit. Git wspiera również niemodyfikowalne nazwy dla commitów, czyli tzw. tagi.
HEAD #
A co to jest aktywny branch? Repozytorium zawiera specjalną referencję o nazwie HEAD
(w pliku .git/HEAD
), która pokazuje aktualny commit, na którym pracujemy.
HEAD
może pokazywać na commit pośrednio poprzez branch lub bezpośrednio.
Typową sytuacją jest wskazanie pośrednie:
cat .git/HEAD
# ref: refs/heads/master
To sytuacja, w której kolokwialnie mówimy, że jesteśmy na branchu. HEAD
wskazuje
na master
, który wskazuje na konkretny commit. Zarówno HEAD
jak i master
są nazwami tego samego
commita.
graph LR %% Definitions by type classDef commitStyle fill: #f9a825, stroke: #333, stroke-width: 2px, font-weight: bold; classDef treeStyle fill: #8bc34a, stroke: #333, stroke-width: 2px, font-weight: bold; classDef blobStyle fill: #03a9f4, stroke: #333, stroke-width: 2px, font-weight: normal; classDef ref fill: #add8e6, stroke: none, color: #000, font-family: monospace, font-size: 12px, padding: 2px, rx: 5px, ry: 5px; %% Commit Nodes (Left-Aligned) HEAD:::ref master:::ref C0[NULL]:::textOnly C1[Commit 23d9]:::commitStyle C2[Commit a7f3]:::commitStyle C2 -->|parent| C1 C1 -->|parent| C0 C1 -->|tree| T1 C2 -->|tree| T2 HEAD --> master master --> C2 Hi[Blob 663a]:::blobStyle T1[Tree 8da9]:::treeStyle T2[Tree a525]:::treeStyle T1 --> Hi T2 --> Hi T2 --> Hi
Do manipulacji HEAD
‘em służy operacja git checkout
, która oczekuje jako argumentu jakiegoś
commita. Zmiana HEAD’a zmienia stan katalogu roboczego na ten zapisany w docelowym commicie.
git checkout 23d9
cat .git/HEAD
# 23d9585ca2a2fe493f79c75956ab4d815da14b73
HEAD
wskazuje teraz bezpośrednio na commit. To tzw. detached HEAD state: normalny w przypadku przeglądania
starych rewizji.
graph LR %% Definitions by type classDef commitStyle fill: #f9a825, stroke: #333, stroke-width: 2px, font-weight: bold; classDef treeStyle fill: #8bc34a, stroke: #333, stroke-width: 2px, font-weight: bold; classDef blobStyle fill: #03a9f4, stroke: #333, stroke-width: 2px, font-weight: normal; classDef ref fill: #add8e6, stroke: none, color: #000, font-family: monospace, font-size: 12px, padding: 2px, rx: 5px, ry: 5px; %% Commit Nodes (Left-Aligned) HEAD:::ref master:::ref C0[NULL]:::textOnly C1[Commit 23d9]:::commitStyle C2[Commit a7f3]:::commitStyle C2 -->|parent| C1 C1 -->|parent| C0 HEAD --> C1 master --> C2
Commit wskazywany przez HEAD
staje się automatycznie rodzicem nowo tworzonych rewizji.
git checkout master # HEAD -> master -> a7f3
touch empty.txt
git add empty.txt
git commit -m "Add empty file"
# [master eef04fe] Add empty file
# 1 file changed, 0 insertions(+), 0 deletions(-)
# create mode 100644 empty.txt
git cat-file commit eef0
# tree 580e44691fcd53fb04aebbd71fe23b5c626afec8
# parent a7f3d48e135b4f4deb9a985ed7058b70491d7c71
# author Paweł Sobótka <pawel.sobotka@pw.edu.pl> 1742673019 +0100
# committer Paweł Sobótka <pawel.sobotka@pw.edu.pl> 1742673019 +0100
#
# Add empty file
Dodatkowo podczas takiej operacji aktywny branch jest przesuwany na nowy commit. W przypadku detached HEAD nie ma aktywnego brancha, więc nie jest to naturalny stan do tworzenia nowych commitów.
Index #
Przed wyprodukowaniem commita zwykle wydajemy polecenie git add
- po co?
Istnieją 3 niezależne stany katalogu roboczego, które biorą udział w procesie tworzenia commita:
- Sam katalog roboczy z bieżącym stanem plików
HEAD
- względem niego wypracowywujemy zmianę (będzie rodzicem)- Indeks - stan pośredni, do którego selektywnie dodajemy zmiany przed utworzeniem commita
Pozwala to na selektywne wypracowywanie treści następnego commita. Możemy, np. mieć jakieś prywatne, brudne zmiany w części plików, których nie chcemy uwzględniać w nowej rewizji. Nie chcemy ich też wycofywać, bo są przydatne.
Indeks technicznie znajduje się wpliku .git/index
. Można go wydrukować poleceniem ls-files
:
git ls-files --stage
# 100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 empty.txt
# 100644 663adb09143767984f7be83a91effa47e128c735 0 hey.txt
# 100644 663adb09143767984f7be83a91effa47e128c735 0 hi.txt
Wygląda zaskakująco podobnie do obiektu tree
bo właśnie nim (prawie) jest!
W momencie operacji git commit
to właśnie stan indeksu utrwalany jest w postaci
nowego obiektu tree
, wokół którego powstaje nowy obiekt commit
.
Operacja git checkout
przywraca nie tylko stan katalogu roboczego, na taki jak jest zapisany
w checkoutowanym commicie, ale też zrównuje z nim stan indeksu.
W czystym stanie repozytorium mamy HEAD == index == workdir
.
git status
# Na gałęzi master
# nic do złożenia, drzewo robocze czyste
Wprowadzając zmiany w katalogu roboczym otrzymujemy: HEAD == index != workdir
.
echo "not really" > empty.txt
git status
# Na gałęzi master
# Zmiany nie przygotowane do złożenia:
# (użyj „git add <plik>...”, żeby zmienić, co zostanie złożone)
# (użyj „git restore <plik>...”, aby odrzucić zmiany w katalogu roboczym)
# zmieniono: empty.txt
#
# brak zmian dodanych do zapisu (użyj „git add” i/lub „git commit -a”)
git diff
# git diff
# diff --git a/empty.txt b/empty.txt
# index e69de29..4d0c7d7 100644
# --- a/empty.txt
# +++ b/empty.txt
# @@ -0,0 +1 @@
# +not really
Polecenie git diff
porównuje stan katalogu roboczego ze stanem indeksu.
Dodając zmianę do indeksu otrzymujemy HEAD != index == workdir
:
git add empty.txt
git status
# Na gałęzi master
# Zmiany do złożenia:
# (użyj „git restore --staged <plik>...”, aby wycofać)
# zmieniono: empty.txt
git diff
#
git diff --cached
# diff --git a/empty.txt b/empty.txt
# index e69de29..4d0c7d7 100644
# --- a/empty.txt
# +++ b/empty.txt
# @@ -0,0 +1 @@
# +not really
git diff --cached
pokazuje indeks ze stanem HEAD
.
W momencie dodania do indeksu powstają nowe obiekty blob
i tree
, które będą później
utrwalane w nowym commicie.
Można teraz ponownie zmienić stan katalogu roboczego uzyskując HEAD != index != workdir
:
echo "hey!" > empty.txt
# git status
# Na gałęzi master
# Zmiany do złożenia:
# (użyj „git restore --staged <plik>...”, aby wycofać)
# zmieniono: empty.txt
#
# Zmiany nie przygotowane do złożenia:
# (użyj „git add <plik>...”, żeby zmienić, co zostanie złożone)
# (użyj „git restore <plik>...”, aby odrzucić zmiany w katalogu roboczym)
# zmieniono: empty.txt
#
git diff
# diff --git a/empty.txt b/empty.txt
# index 4d0c7d7..d4c3701 100644
# --- a/empty.txt
# +++ b/empty.txt
# @@ -1 +1 @@
# -not really
# +hey!
Wykonując teraz polecenie git commit
:
- indeks zostanie utrwalony w postaci nowego obiektu
tree
- powstanie nowy obiekt
commit
:- wskazujący na nowo powstałe
tree
- obecny
HEAD
wstawiony zostanie do atrybutuparent
- wskazujący na nowo powstałe
- gałąź wskazywana przez
HEAD
zostanie przestawiona na nowy commit
Otrzymamy w ten sposób HEAD = index != workdir
. W katalogu roboczym pozostanie
ostatnio wprowadzona zmiana. git commit
nie dotyka katalogu roboczego.
git commit -m "Filled empty file"
# [master 401c27f] Filled empty file
# 1 file changed, 1 insertion(+)
git status
# Na gałęzi master
# Zmiany nie przygotowane do złożenia:
# (użyj „git add <plik>...”, żeby zmienić, co zostanie złożone)
# (użyj „git restore <plik>...”, aby odrzucić zmiany w katalogu roboczym)
# zmieniono: empty.txt
#
# brak zmian dodanych do zapisu (użyj „git add” i/lub „git commit -a”)
Reset #
Jedna z podstawowych operacji: git reset
, często powoduje trudności ze względu
na swoją wielofunkcyjność. Wbrew destrukcyjnie brzmiącej nazwie reset
w istocie
przestawia branch na wskazany commit. Dodatkowo potrafi w tym samym momencie zmieniać stan indeksu
i katalogu roboczego na stan z danej rewizji.
git reset
ma 3 tryby:
git reset --soft
przestawia referencję zapisaną w HEAD
na wskazany commit.
Nie zmienia przy tym stanu katalogu roboczego ani indeksu.
git reset --soft HEAD^
# git status
# Na gałęzi master
# Zmiany do złożenia:
# (użyj „git restore --staged <plik>...”, aby wycofać)
# zmieniono: empty.txt
#
# Zmiany nie przygotowane do złożenia:
# (użyj „git add <plik>...”, żeby zmienić, co zostanie złożone)
# (użyj „git restore <plik>...”, aby odrzucić zmiany w katalogu roboczym)
# zmieniono: empty.txt
Wróciliśmy tym samym do stanu HEAD != index != workdir
, niejako odwracając operację git commit
.
Commit, mimo że nienazwany ciągle istnieje, możemy do niego wrócić:
git reset --soft 401c
git reset --mixed
robi to co --soft
i dodatkowo przywraca stan indeksu na wskazany commit:
git reset --mixed HEAD^
git diff
# diff --git a/empty.txt b/empty.txt
# index e69de29..d4c3701 100644
# --- a/empty.txt
# +++ b/empty.txt
# @@ -0,0 +1 @@
# +hey!
Mamy teraz HEAD = index != workdir
. Katalog roboczy został niedotknięty.
git reset --hard
zmienia wszystko: HEAD branch, stan indeksu i stan katalogu roboczego
na stan ze wskazanej rewizji. Nieodwracalnie porzuca zmiany w katalogu roboczym!
git reset --hard 401c
git status
# Na gałęzi master
# nic do złożenia, drzewo robocze czyste
Merge #
Repozytorium to zbiór commitów. Każdy commit zawiera listę odniesień do swoich przodków (0-n). Każde repozytorium jest zatem acyklicznym grafem skierowanym, w którym węzłami są commity, a krawędziami wskazania na przodków.
Graf można zwizualizować poleceniem git log
:
git log --oneline --graph --all
# * 401c27f (HEAD -> master) Filled empty file
# * eef04fe Add empty file
# * a7f3d48 (feature) Copied hi.txt
# * 23d9585 Initial commit
Na tą chwilę nasz graf jest listą. Projekt rozwijał się liniowo.
Jeden commit może być przodkiem kilku różnych rewizji. Dzieje się tak zwykle, gdy kilku autorów w podobnym czasie zaczyna rozwijać projekt, wychodząc z tej samej rewizji.
git checkout feature
echo "Feature Hi!" > hi.txt
git add hi.txt
git commit -m "Featurized hi.txt"
git log --oneline --graph --all
# * 5e11eea (HEAD -> feature) Featurized hi.txt
# | * 401c27f (master) Filled empty file
# | * eef04fe Add empty file
# |/
# * a7f3d48 Copied hi.txt
# * 23d9585 Initial commit
Na commicie a7f3d48
nastąpiło rozwidlenie historii projektu. Zwykle, w pewnym momencie
niezależnie rozwijane gałęzie muszą ulec połączeniu celem wypracowania jednej, spójnej rewizji
integrującej całą równoległą pracę. Służy do tego operacja git merge
tworząca commity
o wielu przodkach.
git merge --no-edit master
git log --oneline --graph --all
# * 1296142 (HEAD -> feature) Merge branch 'master' into feature
# |\
# | * 401c27f (master) Filled empty file
# | * eef04fe Add empty file
# * | 5e11eea Featurized hi.txt
# |/
# * a7f3d48 Copied hi.txt
# * 23d9585 Initial commit
git cat-file -p HEAD
# tree 8c1f710ff54a3848913a130fbd58b1247827b1ed
# parent 5e11eea4a4f3199d1a097e0716c4afa1c0724317
# parent 401c27fd94595bc9565d76e389fa5923febf2302
# author Paweł Sobótka <p.sobotka@oxla.com> 1742684714 +0100
# committer Paweł Sobótka <p.sobotka@oxla.com> 1742684714 +0100
#
# Merge branch 'master' into feature
Nowo utworzony merge-commit scala stan katalogu roboczego zapisany w jego dwóch przodkach.
Jednym przodkiem zawsze jest HEAD
, drugim to co wskazaliśmy w argumencie.
Integracja przebiegła automatycznie, bo zmiany nie były konfliktujące.
Do scalania zmian git stosuje algorym znany jako three way merge. Bierze on pod uwagę
trzy stany katalogu roboczego: dwa ze scalanych commitów (HEAD -> development
-> 5e11eea
i master
-> 401c27f
)
oraz ich najbliższego wspólnego przodka: punkt rozwidlenia a7f3d48
.
graph LR classDef commit fill: #f9a825, stroke: #333, stroke-width: 2px, font-weight: bold, rx: 2px, ry: 2px classDef new fill: #90EE90, stroke: #333, stroke-width: 2px, font-weight: bold, rx: 2px, ry: 2px classDef ref fill: #add8e6, stroke: none, color: #000, font-family: monospace, font-size: 12px, padding: 2px, rx: 5px, ry: 5px; development:::ref --> A %% development:::ref -.-> M A["5e11eea<br>'Feature Hi!'"]:::commit B["401c27f<br>'Hi!'"]:::commit C["a7f3d48<br>'Hi!'"]:::commit M["1296142<br>'Feature Hi!'"]:::new A --> C B --> C M --> A M --> B master:::ref --> B
W powyższym przypadku zawartość blob’a jest taka sama w jednej ze scalanych wersji jak w ich wspólnym przodku. To czyni operację merge trywialną: wybierana jest wersja, która się odróżnia. Algorytm zakłada, że jeżeli stan w jednej z gałęzi się nie zmienił a w drugiej tak, to ta druga jest pożądaną zmianą po scaleniu.
Jeżeli wszystkie 3 wersje są takie same, sprawa jest jasna. Co jednak w przypadku, kiedy wszystkie są różne? W takim przypadku mamy do czynienia z konfliktem zmian.
git reset --hard HEAD^
git checkout master
echo "Master Hi!" > hi.txt
git add hi.txt && git commit -m "Masterized hi.txt"
git merge feature
# Auto-scalanie hi.txt
# KONFLIKT (zawartość): Konflikt scalania w hi.txt
# Automatyczne scalanie nie powiodło się; napraw konflikty i złóż wynik.
graph LR classDef commit fill: #f9a825, stroke: #333, stroke-width: 2px, font-weight: bold, rx: 2px, ry: 2px classDef new fill: #90EE90, stroke: #333, stroke-width: 2px, font-weight: bold, rx: 2px, ry: 2px classDef ref fill: #add8e6, stroke: none, color: #000, font-family: monospace, font-size: 12px, padding: 2px, rx: 5px, ry: 5px; development:::ref --> A %% development:::ref -.-> M A["5e11eea<br>'Feature Hi!'"]:::commit B["401c27f<br>'Master Hi!'"]:::commit C["a7f3d48<br>'Hi!'"]:::commit M["1296142<br>???"]:::new A --> C B --> C M --> A M --> B master:::ref --> B
Operacja git merge
jest wykonana częściowo i została zatrzymana przed
wypracowaniem scalonego commita. Katalog roboczy i indeks zawierają teraz częściowo scalony stan.
Pliki, których algorym nie mógł przetworzyć automatycznie,
zawierają znaczniki w miejscach, w których wykryto konflikty.
cat hi.txt
# <<<<<<< HEAD
# Master Hi!
# =======
# Feature Hi!
# >>>>>>> feature
Odpowiedzialnością użytkownika jest teraz ręczne rozwiązanie konfliktów, pozostawiając zamiast tych oznaczonych miejsc poprawną treść.
echo "Feature master Hi!" > hi.txt
git add hi.txt
git commit
Merge to tylko jedno z narzędzi do łączenia zmian, które posiada git.
Zachęcamy do zapoznania się z innymi: rebase
, rebase -i
, cherry-pick
.
Remote #
Git jest rozproszonym systemem kontroli wersji służącym do pracy nad tym samym projektem na wielu stacjach roboczych. Typowo, każdy autor posiada na swojej maszynie własne repozytorium i synchronizuje jego zawartość z repozytoriami zdalnymi, korzystając z danego protokołu sieciowego do wymiany obiektów (np. http lub ssh). Identyfikatory obiektów (SHA1) wyliczane z ich zawartości są identyfikatorami globalnymi, będą zgodne we wszystkich repozytoriach.
Adresy zdalnych repozytoriów muszą być skonfigurowane w lokalnym repozytorium w pliku .git/config
za pomocą polecenia git remote
. Na potrzeby nauki można wskazać jako remote inny katalog
na tej samej maszynie.
mkdir origin && cd origin
git init -b null
cd repo
git remote add origin ../origin
Klonując repozytorium git automatycznie dodaje remote
o nazwie origin
wskazujący na miejscie z którego klonujemy.
Istnieją jedynie 4 polecenia komunikujące się ze zdalnym repozytorium:
git clone
git fetch
git pull
git push
Wszystko inne nie dotyka w żaden sposób zdalnej kopii.
git push
aktualizuje zdalne referencje.
Powoduje, że branch w zdalnym repozytorium odpowiadający lokalnemu branchowi
pokazuje na ten sam commit co lokalnie. W konsekwencji przesyła obiekty: commit
, tree
, blob
które mogą nie być obecne w repozytorium zdalnym.
git push --set-upstream origin master
# Wymienianie obiektów: 11, gotowe.
# Zliczanie obiektów: 100% (11/11), gotowe.
# Kompresja delt z użyciem do 8 wątków
# Kompresowanie obiektów: 100% (7/7), gotowe.
# Zapisywanie obiektów: 100% (11/11), 976 bajtów | 976.00 KiB/s, gotowe.
# Razem 11 (delty 0), użyte ponownie 0 (delty 0), paczki użyte ponownie 0
# To ../origin
# * [new branch] master -> master
# branch 'master' set up to track 'origin/master'.
Domyślnie git nie wie jaka zdalna gałąź odpowiada lokalnej gałęzi master
.
Konfigurujemy to jednorazowo za pomocą --set-upstream
.
Zdalne repozytorium było puste - nie zawierało żadnych obiektów.
git push
utworzył zdalną gałąź master, a następnie ustawił ją na commit 401c
.
Musiał do tego przesłać ten commit i wszystkie jego zależności, przechodząc
przez wskazania parent
aż do początku repozytorium.
git branch -va
# feature a7f3d48 Copied hi.txt
# * master 401c27f Filled empty file
# remotes/origin/master 401c27f Filled empty file
Operacje na zdalnych repozytoriach nie tylko manipulują referencjami w zdalnym repozytorium
ale i lokalnymi ich odpowiednikami. git push
utworzył w lokalnym repozytorium referencję origin/master
która pokazuje na to samo co master
w repozytorium origin
. Takie referencje to nie branche,
nie można ich przestawiać inaczej, niż komunikując się ze zdalnym repozytorium.
W repozytorium mogą asynchronicznie pojawić się zmiany: nowe commity, nowe branche:
cd origin
git checkout -b development master
echo "int main() { return 0; }" > main.cpp
git add main.cpp && git commit -m "Added main.cpp"
git fetch
aktualizuje stan wszystkich lokalnych odpowiedników zdalnych branchy,
pobierając przy tym niezbędne obiekty.
git fetch
# remote: Wymienianie obiektów: 4, gotowe.
# remote: Zliczanie obiektów: 100% (4/4), gotowe.
# remote: Kompresowanie obiektów: 100% (2/2), gotowe.
# remote: Razem 3 (delty 1), użyte ponownie 0 (delty 0), paczki użyte ponownie 0
# Rozpakowywanie obiektów: 100% (3/3), 287 bajtów | 287.00 KiB/s, gotowe.
# Z ../origin
# * [nowa gałąź] development -> origin/development
git branch -va
# feature a7f3d48 Copied hi.txt
# * master 401c27f Filled empty file
# remotes/origin/development e0e64f5 Added main.cpp
# remotes/origin/master 401c27f Filled empty file
W lokalnym repo pojawiła się nowa referencja origin/development
wskazująca na ten sam, nowy commit co w repo zdalnym.
Możemy teraz utworzyć tam gałąź i rozwijać dalej z teog miejsca:
git checkout development
git pull
to złożenie dwóch operacji: git fetch
+ git merge [remote ref]
.
Czyli robi dokładnie to co fetch i następnie łączy lokalną gałąź z jej zdalnym odpowiednikiem
potencjalnie tworząc merge commit.