Git i GitHub – zdalne repozytorium, gałęzie i merge

by Patryk

W poprzedniej lekcji zapisaliśmy trzy wersje pliku README.md i porównaliśmy je w historii Gita. Cała praca została na maszynie wirtualnej z Debianem. Teraz połączysz repozytorium devops-lab z GitHubem, pobierzesz jego drugą kopię i prześlesz zmiany między nimi. Następnie rozbudujesz projekt o skrypty, nad którymi popracujesz na osobnych gałęziach. Git i Github to dwie różne rzeczy dlatego warto je zrozumieć.

Punkt startowy: repozytorium z poprzedniej lekcji


Pracuj na tym samym koncie użytkownika Debiana, na którym masz repozytorium. W tym przykładzie katalog projektu znajduje się w ~/devops-lab. Jeśli zapisałeś go gdzie indziej, dostosuj ścieżkę.

cd ~/devops-lab

git status
git branch --show-current
git log --oneline -3

Przed dalszymi ćwiczeniami doprowadź repozytorium do stanu z końca poprzedniej lekcji: README powinien zawierać trzy commity, a git status nie powinien zgłaszać oczekujących zmian.

Git, GitHub i zdalne repozytorium


Jak już wiesz Git to narzędzie do zarządzania wersjami projektu. Kiedy używasz Gita, inicjujesz go w dowolnym folderze, w którym chcesz zapisać pracę, co zamienia ten folder w repozytorium Gita, umożliwiając śledzenie zmian i zapewniając doskonałą historię plików podczas pracy. Oprócz tego istnieje również Github, czyli miejsce do przechowywania repozytoriów git online, dzięki czemu inni ludzie mogą go zobaczyć. Github ułatwia również współpracę nad projektami.

Do istniejącego projektu dodamy teraz zdalne repozytorium.

Konfiguracja SSH na Debianie


W tym labie użyjemy SSH, czyli Secure Shell. Protokół zapewnia szyfrowane połączenie między Debianem a GitHubem. Za pomocą pary kluczy potwierdzisz dostęp do swojego konta bez podawania przy każdej operacji danych logowania do GitHuba. SSH nie jest jedyną dostępną metodą: GitHub obsługuje również połączenia przez HTTPS.

Potrzebujesz konta na GitHubie oraz pary kluczy na swojej maszynie wirtualnej.

Sprawdź zawartość katalogu:

ls -la ~/.ssh

Jeżeli katalog nie istnieje, przejdź do generowania klucza. Jeśli masz już klucz, którego używasz do GitHuba, możesz go wykorzystać. Nie nadpisuj istniejących plików.

Utworzenie klucza

Wygeneruj klucz Ed25519. Ed25519 jest obsługiwany przez GitHuba i pojawia się w jego instrukcji generowania nowych kluczy.

ssh-keygen -t ed25519

Przy pytaniu o lokalizację zaakceptuj domyślną ścieżkę, o ile nie znajduje się tam inny klucz. Ustaw też hasło chroniące klucz prywatny.

Po zakończeniu otrzymasz dwa pliki, które razem tworzą parę:

PlikPrzeznaczenie
~/.ssh/id_ed25519Klucz prywatny, używany lokalnie do potwierdzania dostępu
~/.ssh/id_ed25519.pubKlucz publiczny, który dodasz do konta na GitHubie

Klucz chroniony hasłem trzeba odblokować przed użyciem. Bez dodatkowej konfiguracji możesz więc zobaczyć pytanie o passphrase podczas kolejnych operacji wymagających połączenia. Aby tego uniknąć skorzystamy z ssh-agent, który pozwala korzystać z odblokowanego klucza bez wpisywania hasła przy każdym wywołaniu. Działa lokalnie i przechowuje klucze w pamięci. Nie przesyła ich na GitHuba.

eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519

Przy dodawaniu klucza agent poprosi o wpisanie ustawionego wcześniej hasła. Robimy to tylko raz i później podczas każdorazowego połączenia nie będziemy pytani.

Dodanie klucza do GitHuba

Na Debianie masz już parę kluczy. Teraz dodasz klucz publiczny na GitHubie.

cat ~/.ssh/id_ed25519.pub

Skopiuj całą linię, zaczynającą się od ssh-ed25519. Na GitHubie otwórz ustawienia swojego konta:

Settings → SSH and GPG keys → New SSH key

Nadaj mu nazwę, na przykład debian-devops-lab, wybierz typ Authentication Key i wklej klucz publiczny w pole Key. Zapisz go przyciskiem Add SSH key.

Sprawdzenie połączenia

Wróć do terminala Debiana i uruchom:

ssh -T git@github.com

W adresie pozostaw użytkownika git. Nie zastępuj go nazwą konta labadmin ani swoim loginem z GitHuba. GitHub rozpozna Twoje konto na podstawie klucza użytego do uwierzytelnienia.

Przy pierwszym połączeniu SSH może zapytać, czy ufasz serwerowi. Po poprawnym uwierzytelnieniu zobaczysz komunikat podobny do tego:

Hi TWOJ_LOGIN! You've successfully authenticated, but GitHub does not provide shell access.

Informacja o braku dostępu do powłoki jest oczekiwana. Sprawdzamy dostęp do operacji Git, a nie logowanie do terminala na serwerze GitHuba.

Utworzenie repozytorium na GitHubie


Utwórz na swoim koncie repozytorium o nazwie devops-lab.

Pozostaw je puste. Nie dodawaj podczas tworzenia pliku README, .gitignore ani licencji. Masz już lokalną historię i za chwilę ją wyślesz.

Wróć do terminala i dodaj adres GitHuba, zastępując TWOJ_LOGIN nazwą swojego konta:

git remote add origin git@github.com:TWOJ_LOGIN/devops-lab.git
git remote -v

Wynik powinien zawierać dwa wpisy:

origin  git@github.com:TWOJ_LOGIN/devops-lab.git (fetch)
origin  git@github.com:TWOJ_LOGIN/devops-lab.git (push)

To adres do pobierania i wysyłania zmian. W naszym labie obie operacje korzystają z tego samego repozytorium.

Pierwszy git push

Wyślij lokalną gałąź master:

git push -u origin master

Polecenie publikuje historię gałęzi na GitHubie. Opcja -u ustawia również jej upstream, czyli powiązanie używane przy dalszej synchronizacji. Dzięki temu git dodatkowo zapamiętał powiązanie między lokalnym master a origin/master. Przy standardowej konfiguracji kolejne wysłanie zmian z tej gałęzi będzie wymagało już tylko git push.

Odśwież stronę repozytorium. Powinieneś zobaczyć README oraz commity z poprzedniej lekcji. Pamiętaj, że push dotyczy zapisanej historii. Zmodyfikowane pliki, których nie uwzględniłeś w commitach, nie trafią na GitHuba razem z gałęzią.

master i origin/master: dwie różne referencje

Po pierwszym push sprawdź historię:

git log --oneline --decorate --graph --all

Przy najnowszym commicie zobaczysz oznaczenia podobne do:

(HEAD -> master, origin/master)

master to lokalna gałąź, na której pracujesz.

origin/master jest lokalną informacją o stanie gałęzi master na zdalnym repozytorium origin. Git aktualizuje ją podczas komunikacji z remote. Nie jest to podgląd GitHuba działający w czasie rzeczywistym.

Przećwiczmy różnicę. Dopisz informację do README i zapisz ją w nowym commicie:

echo 'Repository: GitHub connected' >> README.md

git diff
git add README.md
git diff --staged
git commit -m "Document remote repository"

Sprawdź:

git log --oneline --decorate -2

Lokalna gałąź wskazuje już nowy commit. origin/master pozostaje przy poprzednim, bo zmiana nie została jeszcze wysłana.

Po wykonaniu kolejnego git pusha obie referencje powinny ponownie wskazywać ten sam commit.

git push
git log --oneline --decorate -2

Druga kopia projektu: git clone


Do sprawdzenia pobierania zmian potrzebujemy drugiej kopii repozytorium. Utworzymy ją na tej samej maszynie, w osobnym katalogu:

cd ~

git clone --branch master git@github.com:TWOJ_LOGIN/devops-lab.git devops-lab-clone

cd ~/devops-lab-clone

Opcja --branch master wskazuje gałąź, od której zaczniemy pracę w nowej kopii. Klonowanie tworzy repozytorium, pobiera historię i konfiguruje połączenie origin ze źródłem. Układ naszego laba wygląda teraz tak:

Dane autora w nowej kopii

Jeżeli w pierwszym repozytorium ustawiłeś dane autora lokalnie, nie zakładaj, że nowa kopia odziedziczy te ustawienia. Konfiguracja lokalna dotyczy konkretnego repozytorium.

Sprawdź wartości:

git config user.name
git config user.email

Gdy ich brakuje lub są nieprawidłowe, ustaw własne dane:

git config user.name "Jan Kowalski"
git config user.email "jan@example.com"

Obie kopie mogą korzystać z tego samego autora. Symulujemy osobne miejsca pracy, nie potrzebujemy drugiego konta na GitHubie.

Zmiana wykonana z drugiej kopii

Będąc w ~/devops-lab-clone, dopisz linię do README:

echo 'Modified from second clone' >> README.md

git add README.md
git diff --staged
git commit -m "Update README from second clone"
git push

Nowy commit znajduje się na GitHubie. Pierwsze repozytorium jeszcze go nie pobrało.

git fetch i git pull: pobranie zmian a aktualizacja gałęzi

Wróć do pierwszej kopii:

cd ~/devops-lab
cat README.md

W pierwszej kopii projektu nadal nie ma linii Modified from second clone. Dodaliśmy ją w drugiej kopii i wysłaliśmy na GitHuba, ale pierwsze repozytorium jeszcze nie pobrało tego commita.

Aby pobrać zmiany, przejdź do pierwszej kopii projektu i skorzystaj z polecenia git fetch:

git fetch origin

Git pobierze nowe commity z repozytorium origin. Nie zmieni jednak Twoich plików ani nie przesunie lokalnej gałęzi master. Dlatego po wykonaniu tego polecenia w README.md nadal nie zobaczysz dopisanej linii.

Pobrany commit możesz już obejrzeć w historii:

git log --oneline --decorate --graph --all

Sytuacja wygląda teraz tak:

...---D---E
      |   |
      |   origin/master
      master (HEAD)

master nadal wskazuje commit D, czyli wersję, na której pracujesz. Natomiast origin/master wskazuje już commit E, pobrany z GitHuba. To ten, w którym dodaliśmy linię w drugiej kopii projektu.

Commit E jest już dostępny w Twoim lokalnym repozytorium, ale nie jest jeszcze częścią gałęzi master. Możesz więc sprawdzić, co zawiera, zanim zaktualizujesz swoją gałąź. Właśnie do tego przydaje się git fetch: najpierw pobierasz zmiany i je przeglądasz, a dopiero później włączasz je do swojej pracy.

Sprawdzenie tego, co przyszło z GitHuba

Zmiany z drugiej kopii repozytorium zostały już pobrane za pomocą git fetch, ale nie trafiły jeszcze do naszej lokalnej gałęzi master. Zanim je połączymy, sprawdźmy, co dokładnie pobraliśmy z GitHuba.

Aby wyświetlić nowe commity, skorzystaj z polecenia:

git log --oneline master..origin/master

Powinieneś zobaczyć commit, który utworzyliśmy wcześniej w drugiej kopii projektu.

Wiemy już, że pojawił się nowy commit. Sprawdźmy teraz, jakie zmiany wprowadza.

git diff master origin/master

Git porówna zawartość plików z naszej lokalnej gałęzi master ze stanem pobranym z GitHuba. W wyniku zobaczymy między innymi:

+Modified from second clone

To linia, którą dopisaliśmy wcześniej do README.md w drugiej kopii repozytorium. Dzięki temu możemy sprawdzić zawartość pobranych zmian, zanim włączymy je do lokalnej gałęzi. Na razie nasz README.md pozostaje bez zmian. W kolejnym kroku skorzystamy z git pull, żeby zaktualizować lokalną gałąź master.

Aktualizacja lokalnej gałęzi

Sprawdziliśmy już, jakie zmiany zostały pobrane z GitHuba. Nowy commit jest dostępny w naszym repozytorium, ale lokalna gałąź master nadal wskazuje starszą wersję projektu.

Aby zaktualizować lokalną gałąź i włączyć do niej pobrane zmiany, skorzystaj z polecenia:

git pull

Git ponownie sprawdzi zdalne repozytorium, pobierze ewentualne nowe zmiany i spróbuje połączyć je z aktualną gałęzią.

W naszym przypadku wykona operację fast-forward, czyli po prostu przesunie wskaźnik lokalnej gałęzi master na nowszy commit. Nie musi tworzyć dodatkowego commita, ponieważ od ostatniej synchronizacji nie wprowadzaliśmy żadnych nowych commitów w pierwszej kopii projektu.

Możemy to przedstawić tak:

Przed git pull:

A---B---C---D---E
            ↑   ↑
          master origin/master


Po git pull:

A---B---C---D---E
                ↑
        master, origin/master

Sprawdźmy, czy zmiana pojawiła się w naszym pliku:

cat README.md
git status

W README.md powinna znajdować się teraz linia:

Modified from second clone

Oznacza to, że zmiana wprowadzona wcześniej w drugiej kopii repozytorium została pobrana z GitHuba i uwzględniona w naszej lokalnej gałęzi.

Poznaliśmy w ten sposób dwa polecenia, które służą do pracy ze zdalnym repozytorium, ale zachowują się nieco inaczej.

  • git fetch pobiera nowe commity i aktualizuje informacje o zdalnych gałęziach. Nie zmienia przy tym aktualnej gałęzi ani plików w katalogu roboczym.
  • git pull również pobiera zmiany, ale dodatkowo próbuje włączyć je do gałęzi, na której pracujemy.

W naszym ćwiczeniu wystarczyło przesunąć master do nowszego commita. Jeśli jednak lokalna i zdalna historia rozwiną się niezależnie, połączenie zmian będzie wymagało innej operacji. Taką sytuację przećwiczymy podczas pracy z branchami i poleceniem git merge.

Pierwsza gałąź: rozbudowa skryptu


Przejdźmy od README do skryptów. Wszystkie następne operacje wykonuj w pierwszym repozytorium:

cd ~/devops-lab
git switch master

W poprzedniej części nie tworzyliśmy scripts/system-info.sh, więc najpierw dodamy jego podstawową wersję na master.

mkdir -p scripts

cat > scripts/system-info.sh <<'EOF'
#!/usr/bin/env bash

printf 'Host: '
hostname
EOF

Blok od cat do końcowego EOF wklej w całości. Tworzy on plik o pokazanej zawartości. Zapiszmy skrypt w historii commitów.

bash scripts/system-info.sh

git add scripts/system-info.sh
git diff --staged
git commit -m "Add system information script"
git push origin master

Mamy punkt wyjścia do dalszej pracy: skrypt wyświetlający nazwę maszyny.

Utworzenie feature/system-info

Gałąź w Git jest referencją wskazującą commit. Utworzenie nowej gałęzi nie wymaga kopiowania projektu do osobnego katalogu. Na początku obie gałęzie mogą wskazywać dokładnie ten sam commit.

Utwórz gałąź i przełącz się na nią:

git switch -c feature/system-info
git branch

switch -c łączy utworzenie gałęzi z przełączeniem na nią. Gwiazdka na liście oznacza bieżącą gałąź.

* feature/system-info
  master

W nazwach naszego laba prefiks feature/ oznacza pracę nad nową funkcjonalnością. Dla poprawek możemy używać bugfix/, a dla dokumentacji docs/. To przyjęta tutaj konwencja.

Commit na osobnej gałęzi

Na osobnej gałęzi rozbuduj skrypt o raportowanie pamięci:

cat >> scripts/system-info.sh <<'EOF'

printf '\nMemory:\n'
free -h
EOF

Sprawdź działanie oraz dodajmy commit. Nowy commit należy do gałęzi feature/system-info. master pozostał przy wcześniejszej wersji.

bash scripts/system-info.sh

git diff
git add scripts/system-info.sh
git diff --staged
git commit -m "Add memory information to system report"

Gałąź master pozostała przy wcześniejszej wersji. Przełącz się na nią z powrotem:

git switch master
cat scripts/system-info.sh

Zobaczysz skrypt bez części dotyczącej pamięci. Przy przełączaniu Git aktualizuje katalog roboczy do stanu danej gałęzi. W tym ćwiczeniu robimy to po zapisaniu zmian w commicie, więc nie przenosimy między gałęziami niezapisanej pracy.

Łączenie zmian z master: Merge fast-forward

Zmiany w skrypcie system-info.sh są już gotowe i zapisane na gałęzi feature/system-info. Teraz chcemy przenieść je do głównej gałęzi master.

Aby to zrobić, najpierw przełącz się na master:

git switch master

Następnie połącz ją z gałęzią, na której pracowaliśmy:

git merge --ff-only feature/system-info

Git wykona teraz operację nazywaną fast-forward. O co w niej chodzi? Kiedy tworzyliśmy feature/system-info, obie gałęzie wskazywały ten sam commit. Następnie dodaliśmy informację o pamięci RAM i utworzyliśmy nowy commit na gałęzi feature/system-info. W tym czasie na master nie zapisywaliśmy żadnych nowych commitów.

Przed połączeniem historia wygląda więc tak:

A---B---C---D
        ↑   ↑
      master feature/system-info

master wskazuje commit C, a feature/system-info wskazuje nowszy commit D. Ponieważ commit D jest bezpośrednim następcą C, Git może po prostu przesunąć wskaźnik master do przodu.

Po wykonaniu merge otrzymamy:

A---B---C---D
            ↑
          master
          feature/system-info

Obie gałęzie wskazują teraz ten sam commit. Git nie musiał tworzyć dodatkowego commita łączącego historie, ponieważ master nie miał własnych nowych zmian.

Usunięcie zakończonej gałęzi

Zmiana jest już na master, więc lokalna gałąź zadania nie jest potrzebna. Dodatkowo wypychamy zmiany na github:

git branch -d feature/system-info
git push origin master

Usunięcie tej nazwy nie usuwa commitów, które należą już do historii master. Gdy -d odmawia usunięcia gałęzi, sprawdź jej historię zamiast od razu sięgać po wymuszenie przez -D

Co zrobić, gdy podczas merge pojawi się konflikt?


Opisane ćwiczenie nie wymaga ręcznego rozwiązywania konfliktów. Jeżeli jednak wykonałeś inne zmiany i podczas git merge zobaczysz CONFLICT, zatrzymaj się i sprawdź:

git status

Niedokończone scalanie możesz przerwać przez:

git merge --abort

Git spróbuje odtworzyć stan sprzed merge. Dlatego zaczynaliśmy scalanie z czystym drzewem roboczym: niezapisane wcześniej modyfikacje mogą utrudnić przywrócenie tego stanu.

Dwie gałęzie rozwijane równolegle


Teraz przygotujemy sytuację, w której obie gałęzie otrzymają własne commity. Na feature/monitoring powstanie nowy skrypt, a na main zmienimy README.

Skrypt na feature/monitoring

Będąc na master, utwórz gałąź:

git switch -c feature/monitoring

Dodaj podstawowy raport:

cat > scripts/monitoring.sh <<'EOF'
#!/usr/bin/env bash

printf 'Host: '
hostname

printf '\nUptime:\n'
uptime

printf '\nDisk usage:\n'
df -h /
EOF

Uruchom go i utwórz pierwszy commit:

bash scripts/monitoring.sh

git add scripts/monitoring.sh
git diff --staged
git commit -m "Add basic monitoring script"

Jako drugą zmianę dodaj raportowanie pamięci:

cat >> scripts/monitoring.sh <<'EOF'

printf '\nMemory:\n'
free -h
EOF

Zapisz ją osobno:

git diff
git add scripts/monitoring.sh
git diff --staged
git commit -m "Add memory usage to monitoring script"

Mamy dwa commity: utworzenie skryptu oraz jego rozbudowę.

Publikowanie gałęzi na GitHubie

Gałąź zadania możesz wysłać przed połączeniem jej z master:

git push -u origin feature/monitoring
git branch -a

Na liście zobaczysz między innymi:

* feature/monitoring
  master
  remotes/origin/feature/monitoring
  remotes/origin/master

feature/monitoring jest lokalną gałęzią. origin/feature/monitoring przechowuje lokalną informację o jej odpowiedniku na GitHubie.

Ten push publikuje wskazaną gałąź, nie włącza jej zmian do master. Do połączenia przejdziemy osobno.

Niezależna zmiana na master

Przełącz się na główną gałąź:

git switch master

Nie edytuj tutaj monitoring.sh. Zamiast tego dopisz opis projektu do README:

cat >> README.md <<'EOF'

## Project purpose
Scripts and Git exercises for the DevOps course.
EOF

Zapisz zmianę:

git diff
git add README.md
git diff --staged
git commit -m "Document project purpose"

Obejrzyj historię:

git log --oneline --decorate --graph --all

Ma teraz dwie linie rozwoju. W poniższym schemacie C oznacza stan sprzed utworzenia gałęzi monitoringu:

        D---E  feature/monitoring
/
...---C
\
F master (HEAD)

D i E dotyczą skryptu. F zawiera zmianę README.

Merge commit: połączenie rozgałęzionej historii

Na początku upewnij się czy na pewno dalej jesteś na gałęzi master.

git branch --show-current
git status

Połącz gałąź monitoringu:

git merge --ff feature/monitoring -m "Merge monitoring feature"

Opcja --ff pozwala wykonać fast-forward, gdy historia na to pozwala, albo utworzyć merge commit, gdy potrzebne jest połączenie rozgałęzień. Tutaj master ma własny commit F, więc proste przesunięcie wskaźnika do E nie wystarczy. W tym scenariuszu gałęzie zmieniały różne pliki. Po połączeniu powinieneś otrzymać zarówno opis projektu w README, jak i nowy skrypt.

Historia będzie wyglądać tak:

        D---E
/ \
...---C---F---M master (HEAD)

M to merge commit. Ma dwóch rodziców: F, czyli wcześniejszy koniec master, oraz E, czyli koniec gałęzi monitoringu. W ten sposób łączy obie linie historii.

Sprawdź wynik:

git log --oneline --decorate --graph --all -8
git status

cat README.md
bash scripts/monitoring.sh

Fast-forward z poprzedniego ćwiczenia nie wymagał nowego commita. Tutaj w historii pojawił się osobny commit łączący.

Wysłanie połączonej historii

Merge wykonaliśmy lokalnie. Wyślij zaktualizowane master na GitHuba:

git push origin master

Możesz teraz usunąć lokalną gałąź zakończonego zadania. Jej odpowiednik na GitHubie pozostanie. Usunięcie lokalnej gałęzi nie jest poleceniem usunięcia gałęzi z serwera.

git branch -d feature/monitoring

Na koniec zaktualizuj drugą kopię:

cd ~/devops-lab-clone

git switch master
git pull --ff-only
git status

cd ~/devops-lab

Obie kopie main powinny teraz zawierać tę samą historię. W projekcie masz:

devops-lab/
├── README.md
└── scripts/
    ├── system-info.sh
    └── monitoring.sh

Ściągawka z poleceń


Poniższe komendy odpowiadają operacjom wykonanym w labie.

PolecenieZastosowanie
git remote -vSprawdzenie adresów zdalnych repozytoriów
git remote add origin ADRESDodanie zdalnego repozytorium
git clone ADRES KATALOGUtworzenie lokalnej kopii repozytorium
git push -u origin mainPublikacja main i ustawienie upstream
git fetch originPobranie danych bez integracji z bieżącą gałęzią
git pull --ff-onlyPobranie i aktualizacja wyłącznie przez fast-forward
git switch -c NAZWAUtworzenie gałęzi i przełączenie się na nią
git switch mainPrzełączenie na main
git merge --ff-only GAŁĄŹPołączenie wyłącznie przez fast-forward
git merge --ff GAŁĄŹFast-forward albo merge commit, zależnie od historii
git branch -aLista gałęzi lokalnych i referencji remote-tracking
git branch -d NAZWAUsunięcie lokalnej gałęzi z kontrolą włączenia jej historii

Następny krok: Masz dwie kopie repozytorium i przećwiczone łączenie gałęzi bez konfliktów. W kolejnej lekcji celowo doprowadzimy do konfliktu, odczytamy oznaczenia w pliku i zapiszemy poprawne rozwiązanie.

Kolejna lekcja: Konflikty w Git — jak je rozwiązywać krok po kroku.

← Poprzednia lekcja: Git od podstaw – git add, git commit i historia zmian · Wróć do spisu kursu

Może Cię również zainteresować

Dodaj komentarz