GIT w 15 minut

Prosta instrukcja krok po kroku jak używać systemu kontroli wersji GIT przy codziennej pracy. Dowiesz się jak zainstalować i używać GITa w terminalu, przy pracy lokalnej lub ze zdalnym repozytorium takim jak np. popularny GitHub.

Po co ten GIT

GIT to narzędzie które pomaga rozwijać oraz utrzymać projekty. Dostarcza jednemu lub całym zespołom developerów systemu wersjonowania plików. Bez tego praca na kodzie przez wielu ludzi (na wielu wersjach kodu) byłaby katorgą.

Przykład z życia. Przyjmijmy że w pracy zajmujemy się wprowadzaniem poprawek do stron Klientów. Robimy to w trybie mało bezpiecznym ale jakże często używanym – „na żywca” wprowadzamy zmiany na stronie bo nie istnieją osobne wersje dev/prod. Czyli zgrywamy kopię plików (backup), pracujemy na tych plikach a zmienione wersje wysyłamy na serwer. Co jeśli przyjdzie nam jednak cofać nasze zmiany? Musimy szukać oryginalnych plików w backupach. A czasem to któraś poprawka z kolei. Mamy backup po każdej poprawce? No właśnie 😉 Wystarczy zainicjować repozytorium i zrobić zapis po każdej poprawce. To bardzo szybki i wygodny proces. Potem już tylko jedna komenda i cofamy się do której wersji chcemy!

Kolejnym przykładem jest praca nad projektem (np. systemem CRM) w zespole. Czasy pracy gdzie każdy ma swoje konto na FTP i wrzuca swoje moduły, które potem są podgrywane przez adminów na serwer produkcyjny… mam nadzieję że się skończyły. Dzięki systemowi kontroli wersji GIT każdy programista może uzupełniać repozytorium o swoją porcję kodu, od razu widząc czy nie zmienił czegoś w tym samym pliku co inny programista. Na taką sytuację może odpowiednio zareagować dostosowując swój kod, ponieważ widzi kod i zmiany wszystkich współpracowników. W razie potrzeby może cofać zmiany z łatwością, bo istnieje pełna historia ich wprowadzania. Gdy zespół uzna że dany kod jest stabilny, można scalić go do głównej gałęzi gdzie już w bardzo prosty sposób może trafić na odpowiedni serwer, np. testowy.

GIT jako narzędzie w konsoli, o którym jest niniejszy poradnik, w przejrzysty i szybki sposób pozwala programiście komponować kod, zapisywać go i wysyłać na serwer. Po drugiej stronie, na serwerze, może czekać dodatkowo „nakładka” na ten system, w formie GitHuba, GitLaba czy podobnego rozwiązania. Pozwala to na webowy sposób poruszania się i interakcji z kodem, co bardzo ułatwia np. przeprowadzanie rewizji kodu czy zarządzanie projektem w całości.

Instalacja GIT

Instalacja może się różnić zależnie od systemu. Ten poradnik jest skierowany do użytkowników systemów linuxowych ze względu na przykłady w konsoli ale również na Windows można bez problemu zainstalować GITa (odsyłam do instrukcji w Internecie). Można tam również używać komend jak na Linuksie jeśli użyjemy WSL.

Na Debian/Ubuntu wystarczy jedna komenda w terminalu (którego możemy otworzyć skrótem Ctrl + Alt + T):

sudo apt-get install git

Instalacja sama w sobie nie wymaga dodatkowej konfiguracji ale musimy powiedzieć GITowi coś o nas żeby wiedział jak podpisywać nasze zmiany 😉 Ustawmy zatem wymagany email oraz nazwę:

git config --global user.email "[email protected]"
git config --global user.name "Imię Nazwisko"

Super! Teraz gdy zapiszemy jakieś zmiany zobaczymy swoje imię, nazwisko oraz adres email jako autora:

Przykłądowy git log z naszymi user.name i user.email
Przykładowy git log z naszymi danymi w polu autor.

Tak ustawiliśmy nazwę i email globalnie, czyli dla dowolnego repozytorium które będziemy używać na naszym koncie użytkownika. Możemy również użyć zamiast przełącznika --global wartości --system aby zmienić ustawienia dla wszystkich kont w systemie. Najczęściej jednak możemy być zainteresowani przełącznikiem --local, dzięki któremu zmienimy ustawienia dla repo w którym akurat się znajdujemy. Każde repo możemy przecież inaczej „podpisywać” (praca/open source/prywatny projekt, etc.).

Lokalne repozytorium GIT

Lokalne repozytorium pozwala nam na o wiele wygodniejszą i bezpieczniejszą pracę z plikami projektu. Co prawda jeszcze nigdzie nie wysyłamy naszych zmian, więc bezpieczeństwo ustępuje wygodzie – ale to pierwszy krok do lepszej pracy z kodem. I czasem wystarczający, jeśli i tak robimy backup w „tradycyjny” sposób.

Będąc w terminalu przechodzimy do katalogu w którym chcemy przechowywać pliki naszego projektu, lub gdzie już mamy jakieś pliki (np. zgrane z FTP) które chcemy wersjonować. Następnie uruchamiany komendę utworzenia (zainicjowania) nowego repozytorium:

git init

Niezależnie od tego czy w katalogu były już jakieś pliki czy był pusty, po uruchomieniu tej komendy GIT utworzy w nim ukryty podkatalog .git (jeśli już istnieje – zostaniemy o tym poinformowani i GIT zakończy pracę). W tym podkatalogu GIT przechowuje wszystkie informacje o repozytorium, takie jak wersje danych czy ustawienia.

Katalog .git
Co się kryje w katalogu .git

Dzięki temu GIT wie, że dany katalog stanowi repozytorium i porównuje to co jest w katalogu z tym co przechowuje w swoim podkatalogu.

W tym momencie mamy utworzone nowe repozytorium, które wykrywa zmiany w plikach, ale jeszcze nic nie zostało zapisane – powinniśmy dodać pliki do zapisania oraz ostatecznie je zapisać – to zwykle tylko dwie komendy. Przejdźmy zatem do śledzenia plików i składania zmian.

Dla uproszczenia, ten poradnik możemy „przejść” w jednym oknie konsoli, nie rozpraszając się innymi sprawami. Oczywiście możemy otworzyć nasz katalog z repo w swoim edytorze lub IDE, jak choćby w Visual Studio Code, i w ten sposób tworzyć oraz edytować pliki.

Śledzenie plików

Pierwszym krokiem przed zapisem jest oznaczenie plików jako śledzone przez GITa. Zobaczmy w ogóle na czym stoimy, uruchamiając odpowiednią komendę:

git status

Na razie dowiemy się tylko że jesteśmy na gałęzi master (pierwsza, domyślna gałąź), nic jeszcze nie zapisywaliśmy oraz nie mamy żadnych zmian do złożenia (ang. commit). Do gałęzi jeszcze wrócimy.

Komenda "git status" na pustym repozytorium
Komenda „git status” na pustym repozytorium

Teraz nie wychodząc z terminala utwórzmy dwa pliki poleceniem touch i ponownie odpalmy git status. Teraz zobaczymy dodatkowo informację, że istnieją pliki które nie są śledzone oraz podpowiedź jak je dodać do śledzenia:

Komenda git status z nowymi plikami
Git status po utworzeniu nowych plików

GIT proponuje nam, abyśmy uruchomili komendę „git add <plik>”. Dzięki temu możemy śledzić pojedyncze pliki, jeśli np. chcemy potem zapisać tylko część plików. Aby dodać oba te pliki, możemy wpisać taką komendę dwa razy:

git add index.php
git add readme.md

albo wypisać je po spacji:

git add index.php readme.md

lub jeśli wiemy że chcemy dodać wszystkie pliki, wystarczy użyć znaku kropki:

git add .

Wszystkie powyższe komendy będą miały ten sam wynik – dodamy oba nasze pliki do śledzenia. Zobaczmy co teraz nam powie „git status”:

Git status po dodaniu plików do śledzenia

Jak widać, GIT już śledzi nasze pliki. Równocześnie proponuje nam, że możemy wycofać się ze śledzenia tych plików poprzez uruchomienie komendy git rm --cached <plik>. Gdybyśmy to zrobili, wrócimy do stanu sprzed uruchomienia git add, czyli plik zostałby oznaczony jako nieśledzony. Możemy również cofnąć śledzenie wszystkich plików przy pomocy komendy git reset.

Spróbujmy teraz zmodyfikować zawartość któregoś z nich – dodajmy trochę tekstu do pliku readme.md i ponownie odpalmy polecenie git status. Aby nie otwierać edytora, zrobię to z poziomu okna konsoli wykonując echo które podobnie jak w PHP wypisuje ciąg znaków. Następnie „operatorem tworzenia lub nadpisania zawartości” > kieruję ten ciąg znaków do pliku readme.md zamiast do konsoli:

Git status po dokonaniu zmiany w śledzonym pliku

Dodaliśmy treść do wcześniej pustego pliku, a więc dokonaliśmy zmiany w śledzonym pliku. Dostajemy teraz informację, że istnieją dwa śledzone pliki a równocześnie jeden z nich zawiera zmiany nie przygotowane do złożenia. To oznacza, że GIT w momencie uruchomienia git add zapamiętał sobie stan plików podanych w tym poleceniu. Jeśli znów coś zmienimy w plikach, GIT zauważy, że ich zawartość zmieniła się względem tego co „oznaczyliśmy” przy okazji uruchomienia komendy śledzenia. To dobry moment aby trwale zapisać nasze zmiany.

Komendy GITa użyte do tej pory:

  • git init – utworzenie repozytorium w katalogu
  • git status – sprawdzenie obecnego stanu repo
  • git add – dodanie plików do śledzenia

Dodatkowe komendy do cofania git add:

  • git rm --cached <plik> – cofnięcie śledzenia konkretnego pliku
  • git reset – cofnięcie śledzenia wszystkich plików

Zapisanie zmian

Pliki oznaczone do śledzenia (widoczne na zielono po uruchomieniu git add) możemy trwale zapisać jako commit (po polsku „złożone zmiany” 😉 ). Dzięki temu utworzymy swoisty punkt w historii repozytorium do którego będziemy mogli wracać w razie potrzeby. Żeby zobaczyć nasze zapisy, możemy uruchomić komendę git log. Oczywiście teraz nasze repozytorium nie ma żadnych zapisów. Dodajmy zatem pierwszy commit (pamiętajmy że mamy na stanie dwa „zielone” pliki i jeden „czerwony”).

Commit możemy dodać na dwa sposoby, po prostu uruchamiając odpowiednią komendę:

git commit

Co uruchomi nam „interaktywny” tryb dodawania zapisu, tj. otworzy domyślny edytor (w moim przypadku nano) gdzie możemy dodać opis naszej zmiany a następnie korzystając z opcji zapisu w edytorze, zapisać commit.

Znacznie szybciej jest wykorzystać przełącznik -m (message) i dodać opis od razu w poleceniu:

git commit -m "Init a git15 repo!"

Zanim zapiszemy naszego pierwszego commita, warto wiedzieć jak właściwie powinniśmy go opisać. Jest to niezwykle ważna rzecz, ta z kategorii „teraz wiem o co chodzi, ale jutro już mogę zapomnieć”. Dlatego dla Twojego i twoich współpracowników dobra, proponuję świetny artykuł jak pisać dobre commity.

Gdybyśmy w powyższym poleceniu git commit -m ... wcisnęli Enter przed postawieniem końcowego znaku cudzysłowu, polecenie zamiast wykonać się, przejdzie do kolejnej linii – przydatne jeśli chcemy oddzielić tytuł i dłuższy opis (zgodnie z artykułem o dobrych commitach) bez otwierania edytorka.

Uruchamiamy więc komendę git commit -m "Init a git15 repo!". Tym razem uruchomimy dodatkowo git log aby zobaczyć co mamy zapisane (czyli nasz obecny commit) oraz standardowo sprawdzimy stan naszego repo:

Pierwszy git commit. Następnie git log oraz git status.

Po wykonaniu komendy złożenia zmian GIT informuje nas, że dokonał zapisu na gałęzi master która jest główną gałęzią (korzeniem naszego drzewa), jego skrócony unikalny identyfikator (hash) to 4a7e419 i posiada opis „Init a git15 repo!”. Następnie możemy się dowiedzieć, że 2 pliki zostały zmienione (nie istniały w repo, więc zmieniły się), 0 linii zostało dodanych/usuniętych (no cóż, pliki były puste). Na koniec mamy wylistowane pliki wraz z wykonanymi na nich operacjach.

Następnie uruchamiamy dziennik zmian (git log). Widzimy że jest w nim jeden commit, o identyfikatorze (hashu) 4a7e419... (w przeciwieństwie do zwrotki z commita widzimy jego pełną wartość). Mamy również informacje o autorze (które podawaliśmy podczas konfiguracji), datę złożenia zmian oraz opis.

W statusie widzimy że „zielone” pliki zniknęły (czyli zostały złożone – „zakomitowane”). Jak widać, „czerwony” plik czyli zmiany które dokonaliśmy już po git add . nie zostały złożone i dalej na nas czekają. To oznacza, że po każdych zmianach w plikach na których pracujemy GIT oczekuje od nas decyzji czy dodać ich obecny stan do śledzenia (przygotować do zakomitowania). Dlatego zawsze przez wykonaniem git commit warto sprawdzić w jakim stanie jest nasze repozytorium aby uniknąć częściowych zapisów 🙂

Gdy skończyliśmy pracę nad porcją kodu i chcemy ją zapisać do repozytorium, wykonujemy 3 kroki:

  • git status – aby sprawdzić na czym stoimy 😉
  • git add <pliki lub wzorzec> – aby wskazać pliki do zapisu
  • git commit -m "Opis zmian" – aby zapisać zmiany

Cofanie zmian

Dokonaliśmy już pierwszych zapisów i mamy jeden „czerwony” plik czekający na oznaczenie i zapisanie, readme.md. Co jeśli chcielibyśmy cofnąć zmiany w tym pliku ale akurat nie możemy zrobić tego „ręcznie” w edytorze (np. używając skrótu Ctrl + Z) oraz nie wiemy co tam było wcześniej, więc zmiana jego zawartości też nie wchodzi w grę?

Chowanie zmian „na chwilę”

Bardzo wygodnym i bezpiecznym sposobem cofania zmian jest użycie Schowka (ang. stash). Schowek to takie miejsce w gicie gdzie możemy wrzucać nie powiązane ze sobą wersje plików (w przeciwieństwie do commitowania na gałęzi). Bardzo przydatny gdy akurat nad czymś pracujemy i musimy na chwilę wrócić do „czystego” stanu aby np. coś poprawić lub zerknąć na oryginalne pliki. Jego użycie jest bardzo proste, wystarczy użyć komendy git stash:

git stash
Chowamy obecne zmiany przy pomocy git stash

To spowoduje zapisanie wszystkich zmienionych plików do schowka. Jak widać w statusie, drzewo robocze jest czyste. Schowek zapisuje zmiany na zasadzie stosu, więc jeśli chcemy przywrócić ostatni zapis (a najczęściej właśnie to robimy), wystarczy uruchomić komendę:

git stash apply
Przywracamy zawartość schowka komendą git stash apply

Teraz wróciliśmy do stanu z przed chwili. Oczywiście trudno w dłuższej perspektywie operować tylko używając ostatniego elementu, dlatego dla zmian o dłuższej żywotności warto dodać własny opis, podobnie jak w przypadku commita. Stwórzmy sobie jeszcze jeden plik wykonując touch robots.txt i wypróbujmy schowek ponownie, tym razem dodając przykładowy opis. Użyjemy do tego pod-komendy push z flagą -m (message).

git stash push -m "Fix #1 but #2 crashed"
Tworzymy nowy plik i chowamy zmiany

Tym razem drzewo robocze nie jest czyste – został nam nowo utworzony plik robots.txt. Tak, bo domyślnie git stash zapisuje tylko śledzone pliki (ogólnie GIT bierze pod uwagę tylko śledzone pliki). Cofnijmy się wykonując git stash apply, następnie dodajmy wszystkie pliki do śledzenia git add . i ponownie schowajmy zmiany:

Przywracamy schowek i uzupełniamy go o brakujący plik

Świetnie, teraz mamy porządek 🙂 Zobaczmy zatem przy pomocy komendy git stash list co tam mamy w schowku:

git stash list
Sprawdzamy stan naszego schowka komendą git stash list

Teraz widzimy stos naszego schowka. Najpierw wrzuciliśmy do niego jakieś zmiany z gałęzi master opisane automatycznie przez GITa jako „WIP on master <skrót informacji o ostatnim commicie>” (WIP = work in progress) a następnie dwa razy dodaliśmy zmiany z opisem „Fix #1 but…” (bo sprzątaliśmy mały bałagan).

Jeśli chcemy przywrócić konkretny schowek z listy, można to zrobić używając jego identyfikatora „stash@{<int>}” w komendzie git stash apply. Wróćmy się do naszego pierwszego zapisu:

git stash apply stash@{2}
Wczytujemy konkretne zmiany z listy

W ten sposób wczytaliśmy stan plików, gdzie jeszcze nie było naszego nowego pliku robots.txt. Nie jest to nasz najnowszy stan plików i moglibyśmy znów go schować aby wczytać nowszą wersję, ale czy warto robić kolejny schowek? Przejdźmy zatem do kolejnego punktu, w którym zobaczymy jak trwale porzucać wprowadzone zmiany w plikach.

Aby cofnąć wprowadzone zmiany z możliwością wrócenia do nich, używamy Schowka:

  • git stash – schowanie wszystkich śledzonych plików
  • git stash list – wylistowanie stosu schowka
  • git stash apply – przywrócenie ostatnio zachowanych plików
  • git stash apply stash@{<int>} – przywrócenie konkretnego schowka

Porzucanie zmian w plikach

Jeśli tylko coś sprawdzamy na szybko (np. debugujemy) albo jesteśmy pewni że wprowadzone zmiany nie będą nam potrzebne, możemy trwale porzuć nasze zmiany. Można to zrobić na dwa sposoby. Używając komendy git reset z opcją --hard:

git reset --hard

To jak widać po nazwie, opcja tylko dla prawdziwych twardzieli 😉 Bardziej precyzyjnie będzie móc porzucić zmiany dla konkretnego pliku a i tak najczęściej modyfikujemy na raz niewiele plików. Możemy to zrobić używając bardzo przydatnej komendy git checkout -- <plik>. O samej komendzie dowiemy się więcej w punkcie o pracy na gałęziach.

Git die –hard 😉

Porzućmy zatem zmiany w pliku readme.md który wczytaliśmy z nie-najnowszego schowka:

git checkout -- readme.md
Porzucamy zmiany na pliku git checkout — <plik>

Jak widać zmiany na pliku readme.md wyparowały w nicość i nasze drzewo robocze jest czyste. Przejdźmy do kolejnego punktu i zobaczmy jak cofać zmiany które już zapisaliśmy do gałęzi poleceniem git commit.

Cofanie commitów

Czasem chcemy cofnąć cały commit, bo np. wykonaliśmy go przez pomyłkę, jest w nim błąd lub z dowolnie innego powodu nie jesteśmy z niego zadowoleni. Bezpieczną metodą jest użycie komendy odwracania zmian zamiast usuwania. Komenda ta wykona dokładnie taką pracę, jakbyśmy ręcznie usunęli zmiany które wprowadził dany commit i ponownie go zapisali. Git utworzy commit ze zmianami odwrotnymi do wskazanego commita i zapisze go.

Dodajmy kolejny plik, bad-file.php i zakomitujmy go. Ponieważ to zły plik, chcemy teraz cofnąć nasz ostatni commit przy pomocy komendy git revert <hash>. Aby nie musieć sprawdzać jaki hash ma nasz ostatni commit, możemy posłużyć się wskaźnikiem HEAD. Ten wskaźnik można rozumieć jako to „na co właśnie patrzymy”. Zawsze patrzymy na jakąś gałąź (teraz master) oraz, jeśli już mamy, jakiś commit. Gdzie my, tam HEAD 😉

git revert HEAD
Droga od złego pliku, zapisania go a potem wycofania
git revert otworzy edytor (np. nano)

Po utworzeniu pliku (zmian), dodaniu do śledzenia i zapisaniu naszła nas ochota na wycofanie ostatniego commita. Używając komendy git revert wskazaliśmy że chcemy cofnąć zmiany względem ostatniego commita przy pomocy HEAD zamiast sprawdzać hash w git log (który miał w tym przypadku wartość ed1084e). Po wykonaniu komendy odwracania, otworzony zostaje edytor (w moim przypadku nano) gdzie możemy opisać dlaczego cofamy zmiany lub po prostu przejść dalej wychodząc z edytora (w nano to widoczny skrót Ctrl + X). Następnie sprawdzamy status – drzewo jest czyste. Listujemy pliki w katalogu – nie ma już bad-file.php. Sprawdzamy jak sytuacja wygląda w historii, uruchamiając git log, tym razem z opcją --oneline która skraca wpisy do hashu i tytułu (pierwszej linii opisu). Widzimy, że jesteśmy na gałęzi master (a HEAD na nią wskazuje) i że pojawił się nowy commit, gdzie GIT automatycznie dopisał do tytułu „Revert…”.

Zmiany zostały bezpiecznie wycofane z gałęzi. Jeśli chcemy, nadal możemy do nich wrócić bo nie zniszczyliśmy żadnego punktu w historii gita.

Co jeśli, szczególnie pracując lokalnie, chcemy trwale usunąć zapisane zmiany? O tym w kolejnym punkcie.

Usuwanie commitów

Trwałe usuwanie commitów ma swoje uzasadnienie choćby przez fakt, że np. opis który dodaliśmy jest „nieładny”, ot, literówka bądź nie to słowo. Trzeba jednak pamiętać że zaburzamy w ten sposób utworzoną historię gita i jeśli naszą pracę wysyłamy potem na zdalny serwer, gdzie ktoś inny też pracuje na repo, może to stwarzać problemy.

Usuńmy zatem commit z informacją o odwracaniu zmian, czyli zresetujmy historię do commita dodanego przed nim przy pomocy komendy git reset <hash>. To ta sama komenda której użyliśmy do cofnięcia komendy git add ale tym razem wskazujemy konkretny commit (domyślnie to nasz HEAD).

git reset ed1084e
Resetujemy stan repo do wskazanego commita

Sprawdzamy naszą historię i pozyskujemy hash commitu do którego chcemy się cofnąć, porzucając WSZYSTKO po drodze (nie możemy po prostu „wyjąć” jednego commita z historii). Uruchamiamy git reset <hash> i otrzymujemy informację że posiadamy nieprzygotowane zmiany. Dzieje się tak, ponieważ domyślnie git reset uruchamia się z opcją --mixed która przenosi resetowane zmiany (ze wskazanego commita) do naszego drzewa roboczego. Widzimy więc, że wrócił plik bad-file.php który jest oznaczony jako usunięty (bo wtedy usunęliśmy go revertem). Punkt w historii został usunięty, co widać w logu.

Aby zobaczyć jak zadziała komenda git reset --hard <hash>, pójdźmy za radą gita i przywróćmy plik (cofnijmy jego status „usunięto”). Następnie zakomitujmy go, aby mieć co resetować:

Cofamy usunięty plik i komitujemy

Skoro znów mamy co resetować, wypróbujmy trwałe resetowanie:

git reset --hard ed1084e
Wykonujemy git reset –hard

Resetujemy commit ed1084e, tym razem z opcją --hard. Drzewo robocze jest czyste a z historii zniknął nasz poprzedni commit. Ponieważ wcześniej przywróciliśmy „usunięty” plik, mamy go z powrotem na liście ale jak widać jest pusty – nie ma zmian które dodaliśmy we wcześniejszym kroku.

Aby trwale usunąć wprowadzone zmiany w drzewie roboczym:

  • git checkout -- <plik> – usuwanie zmian z konkretnego pliku
  • git reset --hard – usuwanie zmian z wszystkich plików

Aby cofnąć lub trwale usunąć commity:

  • git revert <hash> – odwracanie (cofanie) zmian z wskazanego commitu
  • git reset <hash> – usuwanie commitów aż do wskazanego commitu, z pokazaniem zmian po nim
  • git reset --hard <hash> – usuwanie aż do wskazanego commitu

Praca na gałęziach

Do tej pory operowaliśmy na gałęzi master, ale jak nazwa wskazuje, to powinna być główna gałąź naszego repo czyli zawierająca kod w jego najlepszej jakości (produkcyjny). Zobaczmy zatem co oferuje nam GIT.

Tworzymy nową gałąź (branch)

Mógłbym teraz zaproponować użycie komendy git branch <nazwa_gałęzi> która jasno wskazuje do czego służy, ale o wiele wygodniejsze i intuicyjne jest utworzenie gałęzi komendą git checkout która służy do przemieszczania się np. do innej gałęzi czy commita (a gdzie my, tam HEAD ;)) i jest dość wszechstronna (np. porzucaliśmy przy jej pomocy zmiany).

Obecnie znajdujemy się na gałęzi master, na której mamy już jakiś dorobek. Oczywistym jest, że chcemy kontynuować jego rozwijanie, a wiec chcemy bazować na tym kodzie. Wykonajmy polecenie git branch <nazwa_gałęzi> tworząc nową gałąź:

git branch bad-branch

Następnie utwórzmy kolejną gałąź poleceniem git checkout (przełączanie na inną gałąź) razem z flagą -b (jak ang. branch) i podając nazwę nowej gałęzi:

git checkout -b develop

Następnie wylistujmy nasze gałęzie, używając wreszcie git branch:

git branch
Tworzymy dwie gałęzie, na dwa sposoby

Utworzyliśmy tym sposobem dwie gałęzie, bad-branch oraz develop. Jak widać różnica pomiędzy tymi dwoma sposobami jest taka, że ten drugi automatycznie przeniósł nas na nowo utworzoną gałąź (na liście aktywna gałąź jest poprzedzona gwiazdką i ma zielony kolor). Tak się składa że to bardzo pożądane, więc skoro i tak często używamy git checkout, warto używać takiego skrótu. Dodatkowo checkout przeniesie ze sobą śledzone pliki. 3 w 1 😉

Przełączamy się pomiędzy gałęziami

W pracy często przełączamy się pomiędzy gałęziami i tu wystarczy użyć znanego nam już polecenia git checkout <nazwa_gałęzi> podając po nim nazwę interesującej nas gałęzi. Ponieważ jesteśmy aktualnie na gałęzi develop, wróćmy na gałąź bad-branch:

git checkout bad-branch

Następnie namieszajmy trochę, usuwając plik index.php i zapisując zmiany. Potem przełączmy się z powrotem na developa:

Przełączanie i bałaganienie na branchu

Jak widać jeśli chcemy pracować nad nową funkcjonalnością lub po prostu coś potestować, wystarczy że przełączymy się na nową gałąź i wszystko zostanie właśnie tam. Należy jednak pamiętać o naszej gałęzi roboczej. Nie śledzone pliki „przechodzą” pomiędzy gałęziami gdy się przełączamy. Śledzone pliki również przechodzą, ale zostaniemy o tym poinformowani podczas lub po przełączeniu (jeśli nie było konflików).

Przełączamy się pomiędzy commitami

Podobnie jak między gałęziami, możemy „wracać” do wcześniejszych commitów, np. aby podejrzeć zmiany lub po prostu na tej podstawie zrobić nową gałąź. Przełączmy się zatem na konkretny commit komendą git checkout <hash>:

Przełączamy się na konkretny commit

Jak widać, GIT bardzo wylewnie informuje nas, że nie jesteśmy już na żadnej gałęzi (odłączyliśmy się naszym wskaźnikiem HEAD w jakieś inne miejsce). git log pokazuje jako najnowszy commit ten na który się przełączyliśmy (nie widzimy nowszych). W statusie widać że patrzymy na ed1084e ale nie jest to żadna konkretna gałąź.

Co teraz? Możemy oczywiście wykonać komendę git checkout -b <nazwa_nowej_gałęzi> żeby na bazie dotychczasowego logu zrobić nową gałąź. Możemy też zobaczyć np. w IDE jak wygląda kod w tym miejscu (pliki zostały coænięte do tego stanu).

Aby wyjść ze stanu „odłączonego HEAD” (ang. detached HEAD) wystarczy przełączyć się np. na jakąś gałąź poleceniem: git checkout develop. I tyle, znów jesteśmy bezpieczni 😉

Usuwamy gałęzie

Do usuwania gałęzi dochodzi raczej rzadko i robimy to albo żeby posprzątać bo mamy za dużo gałęzi albo… nie, innego powodu nie widzę. Od nadmiaru głowa nie boli, a kto wie kiedy kod z danej gałęzi może się przydać 😉

Możemy usunąć gałąź komendą git branch z przełącznikiem -d (delete) i podając nazwę gałęzi. Jesteśmy na developie więc prawdopodobnie możemy usunąć nieaktywną gałąź bad-branch:

git branch -d bad-branch
Usuwamy gałąź – git branch -d <nazwa_nowej_gałęzi>

Po uruchomieniu komendy usuwania GIT informuje nas że gałąź ta nie jest w pełni scalona i powinniśmy użyć innej opcji (co robimy). Zadziało się tak dlatego, że przy usuwaniu GIT sprawdza czy dana gałąź jest aktualna względem HEAD (czyli tam gdzie teraz jesteśmy – gałąź develop). Zamiana małej litery „d” na wielką „D” spowodowała to samo co w wielu innych poleceniach Linuxa – został dodany tryb „force”.

Łączymy gałęzie

Kwintesencją GITa jest scalanie, czyli merge gałęzi ze sobą. Pozwala to na połączenie pracy wielu ludzi w jedną spójną całość.

Utwórzmy zatem nową gałąź „rozwojową”, dodajmy co nieco do pliku index.php, zapiszmy zmiany i scalmy gałąź develop do gałęzi master przy pomocy polecenia git merge <nazwa_gałęzi>.

git merge develop
Dodajemy zmiany na developie i mergujemy do mastera

Dodaliśmy trochę tekstu do pliku index.php. Zapisujemy zmiany znaną nam procedurą. Widzimy że pojawił się nowy punkt w logu – HEAD wskazuje na develop, ostatnia aktualny commit z gałęzią master jest poniżej. Przełączamy się na gałąź master gdzie widzimy że nie mamy najnowszego commita więc wykonujemy polecenie git merge <nazwa_gałęzi> w celu scalenia zmian ze wskazanej gałęzi. Otrzymujemy informację że trwa aktualizowanie z <hash> do <hash>, następnie widzimy że użyty został tryb „szybkiego przewijania”. Ponieważ nasze commity następowały po sobie, GIT nie musiał scalać niczego wewnątrz plików więc tylko „przewinął” w przód. Potem widzimy listę plików w których nastąpiły zmiany, numeryczną wartość zmodyfikowanych linii kodu oraz graficzną reprezentację ilości dodanych/usuniętych linii. Jeden plus/minus to jedna linia a jeśli ilość znaków przekraczałaby długość linii w konsoli, plusy/minusy wskażą tylko na proporcję dodanego/usuniętego kodu.

Unikanie konfliktów

Droga scalania czasem najeżona jest konfliktami (GIT nie może ustalić która wersja jest najnowsza). Można jednak zminimalizować ten problem prawie do zera. Gdy stworzymy nową gałąź np. na podstawie gałęzi master i popracujemy nad nią jakiś czas, praktycznie pewnym jest (szczególnie przy pracy ze zdalnym repo, czyli innymi ludźmi) że gałąź master będzie posiadała zmiany których nie mamy na swojej gałęzi. Wykonanie potem git merge do mastera przyniosłoby nam szereg konfliktów w plikach, które musielibyśmy poprawić ręcznie.

Dobrą praktyką jest okresowe (np. rano przy rozpoczęciu pracy) mergowanie zawartości gałęzi „bazowej” do naszej „roboczej” gałęzi. To pozwoli zorientować się nam w powstających zmianach i będziemy mogli rozwiązać ewentualne konflikty w małych dawkach, od ręki.

Kolejną jeszcze lepszą praktyką jest zrobienie tego samego co powyżej, ale bezpośrednio przed próbą merge (lokalnie) lub wysłaniem zmian na serwer (praca ze zdalnym repo).

Praca w takim trybie jest wysoce wskazana dla swojego i współpracowników dobra 😉 Dzięki temu rozwiązujemy konflikty na bieżąco i merge przechodzą bezproblemowo. Jesteśmy też w stanie szybciej dostarczyć zmiany jeśli dojdzie do krytycznej sytuacji w projekcie i potrzebujemy czegoś „na już”.

Aby utworzyć/usunąć gałąź:

  • git branch <nazwa_gałęzi> – stworzenie nowego brancha
  • git checkout -b <nazwa_gałęzi> – stworzenie brancha i przełączenie się na niego
  • git branch -d <nazwa_gałęzi> – usunięcie brancha (będąc na innej gałęzi)

Aby przełączyć się na inną gałąź lub commit:

  • git checkout <nazwa/hash> – przełączenie na inną gałąź/commit (a gdzie my, tam HEAD 😉 )

Aby scalić (zmergować) gałęzie:

  • git merge <nazwa_gałęzi> – scalanie brancha <nazwa_gałęzi> do gałęzi na której jesteśmy

Zdalne repozytorium GIT

Gdy już wiemy jak pracować z lokalnym repozytorium – dodawać, cofać i składać zmiany, nadszedł czas na podłączenie się do jakiegoś fajnego serwera i wysyłanie tam naszej pracy. To pozwoli na większe bezpieczeństwo kodu i oczywiście na dzielenie się nim lub wspólną pracę. Zdalne repozytorium (ang. remote) może znajdować się np. na firmowym serwerze czy też u odpowiedniego usługodawcy. Możemy logować się do niego za pomocą loginu/hasła lub (bezpieczniej i wygodniej) klucza SSH. Następnie możemy pobierać oraz wysyłać tam zmiany z naszego lokalnego repozytorium.

Tworzymy zdalne repozytorium

Na początek weźmy na cel utworzenie nowego repozytorium na GitHubie, co jest bardzo proste – wszystko „wyklikujemy” w przeglądarce. Możemy tam tworzyć publiczne ale także prywatne repozytoria, więc to świetne miejsce na udostępnianie swojego dorobku czy też trzymanie swoich projektów. Takie repozytorium nie różni się od tego które spotkasz w pracy, więc jest to dobry wybór.

Po utworzeniu konta proponuję w ustawieniach konta dodać swój klucz SSH. Dzięki temu nie będziemy musieli wciąż wpisywać loginu/hasła i praca będzie o wiele przyjemniejsza i bezpieczniejsza.

Gdy już wszystko mamy ustawione, korzystamy z opcji „New repository”, następnie nadajemy mu nazwę:

Opcja tworzenia nowego repozytorium
Opcja tworzenia nowego repo
Ustalenie nazwy nowego repozytorium na GitHubie
Dodajemy nazwę repo

Po nadaniu nazwy możemy w zasadzie kliknąć „Create repository” lub jeszcze np. zmienić dostępność z (domyślnie) publicznej na prywatną. Zostaniemy przeniesieni do strony głównej naszego repozytorium, gdzie prezentowany jest jego adres:

[email protected]:duch512/git15.git

Dodajemy zdalny serwer do lokalnego repo

Mając jego adres, możemy dodać zdalny serwer do repo na którym do teraz pracowaliśmy:

git remote add origin [email protected]:duch512/git15.git
Dodajemy git remote add origin…

Tym sposobem, przy pomocy komendy git remote add <origin> <adres> dodaliśmy zdalny serwer o nazwie „origin” z adresem „[email protected]:duch512/git15.git” z dostępem po SSH. Teraz autentykacja będzie odbywała się w tle przy użyciu naszego klucza.

Jeśli chcemy logować się w tradycyjny sposób (podając login i hasło przy wykonywaniu zdalnych operacji), powinniśmy podać adres z przedrostkiem HTTPS:

git remote add origin https://[email protected]:duch512/git15.git

Jak można zauważyć, zdalny serwer ma swoją nazwę („origin”). Co oznacza, że możemy w jednym lokalnym repozytorium dodać wiele zdalnych repo i wysyłać/pobierać zmiany z którego chcemy. Jednak w typowym projekcie będziemy korzystać tylko z jednego zdalnego repo.

Wysyłanie zmian na serwer

Repozytorium które właśnie utworzyliśmy oraz podpięliśmy jako origin do naszego repo jest całkowicie puste.

Wyślijmy zatem nasze zmiany na serwer przy użyciu komendy git push:

git push
Wysyłamy zmiany na serwer

Sprawdzamy stan – jesteśmy na gałęzi master którą chcemy wysłać na serwer. Uruchamiamy komendę git push ale GIT informuje nas, że nasza gałąź nie jest powiązana do żadnej gałęzi w zdalnym repozytorium. To oznacza, że nazwa naszej lokalnej gałęzi nie musi się zgadzać z nazwą gałęzi zdalnej, ale dobrą praktyką jest jednak pozostawianie identycznych nazw. Używamy zatem proponowanego rozwiązania, czyli do komendy dodajemy flagę -u (set upstream), następnie podajemy nazwę zdalnego serwera (origin) oraz nazwę zdalnej gałęzi (jeśli nie istnieje – zostanie stworzona). Po uruchomieniu widzimy informację na temat przesyłania zmian do serwera, m.in. fakt, że została stworzona nowa zdalna gałąź i że nasza lokalna gałąź zaczęła śledzić zdalną gałąź.

Wyślijmy na serwer również naszą drugą gałąź:

Wysyłamy drugą gałąź na serwer

Ponieważ wcześniej wysłana gałaź zawierałą te same commity, przesłane zostały tylko metadane, stąd same zera w statystykach. Oprócz innych standardowych wiadomości dostaliśmy również w bloku „remote” dodatkowe info od GitHuba, który proponuje nam utworzenia czegoś takiego jak pull request po przejściu do przeglądarki. Jest to swego rodzaju narzędzie pomagające zarządzać repozytorium w zespole, gdzie nie każdy ma uprawnienia do wprowadzania zmian do produkcyjnej gałęzi master (co jest zrozumiałe ze względów bezpieczeństwa). Pull request (znany również jako „merge request”) to po prostu prośba o zmergowania zmian z naszego brancha do innego, ważniejszego/stabilniejszego brancha.

Zróbmy jeszcze kilka zmian na gałęzi develop i wyślijmy je na serwer. Pamiętacie nasz Schowek? Czekają tam na nas zmiany które teraz bardzo nam się przydadzą:

Przywracamy nasz schowek oraz pushujemy na serwer

GIT jak zwykle informował nas jakie nastepne kroki możemy wykonać, np. że 1 commit oczekuje na wypchnięcie na serwer. Teraz mamy jeszcze więcej zmian na serwerze 😉

Klonowanie zdalnego repozytorium

Jeśli dołączamy do istniejącego projektu lub chcemy pobrać jakieś repo z internetu, nie musimy najpierw inicjować lokalnego repozytorium. Wystarczy że mamy adres repo (wersję https lub ssh) i użyjemy go w poleceniu git clone.

git clone [email protected]:duch512/git15.git

Utwórzmy sobie nowy katalog i sklonujmy wcześniej wysłane repo (wskazane repo w przykładzie jest publiczne, więc możesz je pobrać na swój komputer – znajdziesz tam wszystkie pliki które są w tym poradniku).

Cofnęliśmy się o jeden katalog w górę i utworzyliśmy nowy, bwrite-clone w którym wykonaliśmy komendę git clone <adres>. GIT pobrał repozytorium z danego adresu, dodatkowo tworząc katalog git15 (tak nazywa się repo) w którym umieszczone jest repozytorium. Po przejściu do niego widzimy że jesteśmy na gałęzi master i jest ona na bieżąco z origin/master – czyli automatycznie śledzimy zdalne repo. Lista lokalnych gałęzi pokazuje tylko gałąź master. Sprawdzamy więc listę branchy z przełącznikiem -a (all), aby zobaczyć również zdalne gałęzie. Próba git push kończy się informacją że wszystko jest aktualne.

Tworzenie nowego katalogu aby mieć w nim kolejny – mało wygodne prawda? Niech zatem git zrobi to za nas! Cofnijmy się do naszego „głównego” katalogu. Podajmy po wcześniej użytej komendzie nazwę katalogu w jakim ma znaleźć się repo:

git clone [email protected]:duch512/git15.git tutorial-git
Klonujemy z opją utworzenia katalogu

Pobranie zmian z serwera

Trudno będzie pobrać coś „nowego” z serwera na który tylko my coś wysyłamy, dlatego w trakcie punktu o wysyłaniu zmian na serwer zrobiłem jeszcze jednego klona repo, który jest teraz „w tył” z tym co jest na serwerze. Przejdźmy więc na gałąź develop i wykonajmy polecenie git pull które równocześnie pobierze zmiany z serwera (można to zrobić przez git fetch, ale po co? 😉 ) oraz wykona git merge, czyli połączy zdalną zawartość z naszą lokalną. 2 w 1 😉

git pull
Pobieramy zmiany z serwera do kopii repo w innym katalogu

Przełączyliśmy się na gałąź develop – jest to gałąź która występuje na zdalnym repo więc mogliśmy się nia przełączyć i od razu została ustawiona na śledzenie zdalnego odpowiednika. Następnie poleceniem git pull pobraliśmy i scaliliśmy zmiany. git log pokazuje, że jesteśmy na developie, jest aktualny z originem a commit poniżej jest również aktualny do obu masterów.

W rozpoczęciu pracy ze zdalnym repozytorium przyda ci się:

  • git add remote <origin> <adres> – podpięcie zdalnego repo do lokalnego repo
  • git clone <adres> – skopiowanie istniejącego repo

Podczas pracy ze zdalnym repo najczęściej użyjesz:

  • git checkout <nazwa_gałęzi> – przełączenie się na zdalną gałąź (jeśli istnieje) i śledzenie jej
  • git pull – pobiera i merguje zmiany (wykonuje git fetch i git merge ze zdalnej gałęzi)
  • git push – wysłanie zmian do zdalnej gałęzi
  • git push -u <origin> <nazwa_gałęzi> – utworzenie zdalnej gałęzi, wysłanie zmian i ustawienie śledzenia zdalnej gałęzi

Słowo na koniec

GIT jest super, prawda? 😉 Mam nadzieję że tak Cię to wciągnęło, że nie masz mi za złe zatytułowanie tego artykułu „GITem w 15 minut”. Przeczytanie a co dopiero przerobienie jego zawartości to troszkę więcej niż zakładany kwadrans, jednak mam nadzieję że taka konkretna porcja wiedzy okaże się przydatna. Oczywiście te kilka akapitów powyżej to naprawdę minimum i warto wiedzieć więcej. Dlatego zachęcam do używania GITa w „pisaniu do szuflady” oraz w codziennej pracy, nawet jeśli pracodawca czy twoi koledzy nie widzą potrzeby pracy z systemem wersjonowania.

Baj!