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 .gitTakie 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ówKatalog 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 # 663adb09143767984f7be83a91effa47e128c735Wyliczony 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 663adKatalog .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 8da9Jeden z nich to właśnie tree. Jego zawartość to listing katalogu, którego stan opisuje.
100644 blob 663adb09143767984f7be83a91effa47e128c735 hi.txtNasz 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 a5250100644 blob 663adb09143767984f7be83a91effa47e128c735 hey.txt
100644 blob 663adb09143767984f7be83a91effa47e128c735 hi.txtIdentyfikator 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 a7f3tree 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.txtMoż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.txtTechnicznie branche to pliki w folderze .git/refs/heads/ przechowujące SHA1 commita, który nazywają:
cat .git/refs/heads/master
# a7f3d48e135b4f4deb9a985ed7058b70491d7c71Listowanie 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 masterStą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/masterTo 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 --> HiDo 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
# 23d9585ca2a2fe493f79c75956ab4d815da14b73HEAD 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 --> C2Commit 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 fileDodatkowo 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.txtWyglą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 czysteWprowadzają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 reallyPolecenie 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 reallygit 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
HEADwstawiony zostanie do atrybutuparent
- wskazujący na nowo powstałe
- gałąź wskazywana przez
HEADzostanie 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.txtWró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 401cgit 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 czysteMerge #
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 commitNa 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 commitNa 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 featureNowo 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 --> BW 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 --> BOperacja 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!
# >>>>>>> featureOdpowiedzialnoś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 commitMerge 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 nullcd repo
git remote add origin ../originKlonują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 clonegit fetchgit pullgit 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 fileOperacje 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 fileW 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 developmentgit 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.