Commit, branch i diff, czyli obsługa Gita za pomocą konsoli

Commit, branch i diff, czyli obsługa Gita za pomocą konsoli

Korzystanie z systemu kontroli wersji Git z poziomu konsoli obrosło wieloma mitami. W dzisiejszym poście chciałbym pokazać wam, że nie taki diabeł straszny, go malują. Co więcej, takie podejście często może okazać się bardziej korzystne w porównaniu do klientów Gita z interfejsem graficznym.

Git – tutorial i wprowadzenie

Jeśli do tej pory nie mieliście do czynienia z Gitem, to pierwszym krokiem jaki polecam jest przeczytanie znakomitego wprowadzenia do tego systemu kontroli wersji które przygotował jakiś czas temu Adrian – dostępne jest ono pod tym linkiem.

W podejściu które zaprezentowaliśmy we wspomnianym wpisie zaczynamy od stworzenia projektu na portalu GitHub, następnie przy pomocy narzędzia SourceTree klonujemy repozytorium na nasz komputer i możemy zaczynać pracę – SourceTree daje nam wszystko czego potrzebujemy jeśli chodzi o kontrolowanie stanu naszego repozytorium, commitowanie zmian, czy pracę na wielu gałęziach (branchach).

Dzisiaj popracujemy co nieco z „surowym” Gitem, który sam w sobie jest pełnoprawnym systemem kontroli wersji. Interfejsem, którym będziemy się posługiwać, będzie konsola – nie będzie graficznych klientów ani portali typu GitHub. Wszystko po to, żeby zrozumieć jakie możliwości daje nam Git sam w sobie, a co jest nakładką i dodatkami które go opakowują (np. porównując sposób pracy tutaj z wpisem załączonym powyżej). Na podstawie wprowadzenia do Gita z konsoli przeanalizujemy też typowy „workflow” z Gitem w stylu master / feature branch, który pozwoli nam nauczyć się podstawowych komend oraz operacji które należy wykonać dodając i modyfikując pliki w naszym repozytorium.

Zanim poświęcisz swój czas na przeczytanie tego posta chciałbyś pewnie dowiedzieć się jednego – po co ta konsola?! Czy nie wystarczy opierać się na narzędziach z interfejsem graficznym?

  • Jeśli nauczysz się posługiwać Gitem w konsoli, to system operacyjny ani platforma nie będą dla ciebie przeszkodą – jest to podejście najbardziej uniwersalne które sprawdzi się w każdych warunkach
  • Często jest to podejście szybsze niż uruchamianie obok siebie trzech klientów graficznych do trzech zadań – w jednym okienku konsoli zainicjalizujesz nowy projekt (np. przez npm), stworzysz repozytorium a jeśli trzeba to wprowadzisz szybkie poprawki dzięki edytorom typu Vim. Dodatkowo, dzięki konsoli możemy pisać proste skrypty przyśpieszające pracę – po czasie można zrozumieć jak dużo czasu zyskujemy nie skacząc po przyciskach i okienkach.
  • Poznasz Gita takiego jakim miał być w zamyśle autora – często Git kojarzy się z czymś skomplikowanym, jednak z perspektywy czasu wydaje mi się, że to skomplikowanie bierze się z wszystkich nakładek jakie miałyby uprościć sposób pracy z nim

Nic nie stoi oczywiście na przeszkodzie, żebyś pozostał np. przy SourceTree – sam przez kilka lat używałem tej właśnie aplikacji i wszystko działało bezproblemowo. W tym poście chciałbym ci pokazać Gita i jego obsługę z konsoli, natomiast wybór jak zwykle należy do ciebie. Jeśli jesteś gotowy to możemy zaczynać!

Pierwsze kroki z repozytorium

Po zainstalowaniu Gita możemy sprawdzić, czy narzędzie to jest dostępne z poziomu konsoli – zaczynamy od wykonania komendy git:

W odpowiedzi otrzymujemy spis komend które są dostępne dla użytkownika, takich jak add czy commit – wygląda to tak jak oczekiwaliśmy, a więc możemy zaczynać.

Aby zainicjalizować nowe repozytorium, przechodzimy do wybranego katalogu i wykonujemy komendę git init:

Git informuje nas, że w danym katalogu zainicjalizowano puste repozytorium, którego od teraz możemy używać. Wewnątrz pojawił się katalog o nazwie .git, w którym przechowywana jest cała historia zmian repozytorium, wszystkie informacje o commitach, zdalnych adresach czy branchach.

Teraz pozostaje nam jeszcze dodać do konfiguracji repozytorium dwie dane – nazwę użytkownika oraz nasz adres e-mail. Jest to niezbędne do tego, aby Git mógł do każdej zmiany która powstanie w tym repozytorium przypisać kogoś kto ją spowodował. W naszym przypadku zatrzymamy się na konfiguracji lokalnej (takie dane można jeszcze ustawić globalnie, dodając flagę —global):

Pierwszy plik w nowym repozytorium

Załóżmy, że w naszym repozytorium będziemy chcieli trzymać pliki związane ze stroną internetową którą właśnie tworzymy. Możemy więc rozpocząć od stworzenia pliku index.html w którym umieścimy podstawowy szablon strony:

Jak z poziomu konsoli sprawdzić teraz, jak wygląda stan naszego repozytorium? Służy do tego komenda git status:

Po pierwsze widzimy, że znajdujemy się na branchu master. Do tego, w sekcji „untracked files” widzimy nasz plik index.html – Git wie, że taki plik istnieje, jednak nie będzie śledził historii jego zmian do momentu kiedy jawnie go o to poprosimy.  W strukturze Gita położenie tego pliku to na razie „Working Directory” – surowy katalog roboczy.

My oczywiście chcielibyśmy śledzić zmiany w naszym index.html – w tym celu wykonujemy komendę git add index.html i sprawdzamy ponownie stan repozytorium komendą git status:

Git informuje nas, że index.html jest plikiem, który może być zacommitowany – jego położenie to tzw. Staging Area – w skrócie można powiedzieć, że w Staging Area znajdują się pliki które będą częścią kolejnego commitu (czyli migawki w której zawiera się pewna ilość zmian w repozytorium).

Aby zmiana stanu repozytorium została zapisana musimy dodać nowy commit – tworzymy go poprzez komendę git commit -m „[MESSAGE]” – zamiast [MESSAGE] możemy wpisać przyjazny użytkownikowi opis zmian:

Mamy więc pierwszy commit, którego Git identyfikuje dzięki nadaniu mu pewnego hasha (skrótu, w naszym przypadku to 712659f). Od teraz utworzenie pliku index.html będzie widoczne w historii repozytorium.

Zmiany, zmiany…

Dodajmy teraz w tym samym pliku, w tagu body, nowy element…

…i zobaczmy co zmieniło się w repozytorium od ostatniego commitu za pomocą komendy git diff:

Widzimy tutaj zmiany (u nas to nowa linijka z tagiem p) jakie wykonaliśmy od ostatniego commitu. Aby za pomocą jednej komendy dodać przenieść plik do Staging Area oraz utworzyć nowy commit możemy wykonać:

Git potwierdza utworzenie nowego commitu, zwraca nam tzw. commit message, informuje ile linijek dodano a ile usunięto, i tak jak poprzednio – podaje nam hash danego commitu. Zobaczmy teraz historię zmian wykonaną do tej pory w repozytorium za pomocą komendy git log:

Dzięki tej komendzie otrzymujemy spis wszystkich commitów które wykonano na danym branchu. Mamy tutaj informację o dacie i autorze zmiany, hash commitu oraz commit message. Jeśli chcielibyśmy sprawdzić, co wchodzi w zakres danego commitu możemy to podejrzeć używając komendy git show [commit]:

Na tym etapie wiemy już, jak za pomocą konsoli inicjalizować repozytorium, dodawać do niego pliki i sprawdzać jego historię. Co dalej?

Master / feature branch

Aktualnie pracujemy tylko na jednej gałęzi (master).

Niestety, ma to swoje minusy – aktualnie działająca wersja projektu (na branchu master) może być zaśmiecana commitami zawierającymi eksperymenty – chcielibyśmy oddzielić coś, co na pewno działa, od czegoś co dopiero powstaje i jest niestabilne. Trudno też patrząc na jeden strumień commitów powiedzieć, kiedy rozpoczęła się i zakończyła praca nad daną funkcjonalnością – wszystko miesza się z wszystkim.

Aby rozwiązać takie problemy Git oferuje nam możliwość pracy na wielu gałęziach danego repozytorium.

Zobaczmy teraz, jak możemy wykorzystać nową gałąź do pracy nad eksperymentalną funkcjonalnością. Podejście które zaraz pokażę czasami określa się mianem „master / feature branch„.

Aby stworzyć nową gałąź na podstawie aktualnego stanu repozytorium wykonujemy komendę git checkout -b [BRANCH_NAME]:

W naszym przypadku tworzymy branch add-js na którym będziemy pracować nad dodaniem do naszej strony prostego skryptu JavaScript który wywoła się po kliknięciu na przycisk. Za pomocą komendy git log można jeszcze zobaczyć, że w tym konkretnym momencie stan naszego repozytorium nie różni się w żaden sposób od mastera – jesteśmy na lustrzanej kopii brancha głównego.

Zmodyfikujmy teraz nasz index.html i dodajmy do niego nowy element – przycisk.

Aby łatwiej kontrolować historię zmian i mieć możliwość odwracania tego co robimy poleca się tworzenie częstych commitów dla działającej funkcjonalności. Dla nas dodanie przycisku jest dobrym momentem na nowy commit:

Utwórzmy teraz nowy, pusty plik – app.js – i dodajmy go do index.html:

Zobaczmy jak wg Gita wygląda teraz stan naszego repozytorium:

 

Jest dokładnie tak jak na powyższym obrazku – index.html jest śledzony od jakiegoś czasu, więc zmiana w nim powoduje określenie go jako „modified”. Inaczej jest jednak z app.js – to nowy plik, więc Git umieszcza go w osobnej sekcji. Dodajmy teraz oba pliki do Staging i utwórzmy na ich podstawie nowy commit:

Polecenie git add . sprawi, ze zarówno pliki śledzone-zmodyfikowane, jak i te nieśledzone zostaną dodane do Staging jako kandydaci na kolejny commit. Po dodaniu ich można wykonać już git commit, podać odpowiedni opis i w ten sposób zacommitować zmianę do repozytorium.

Dodajmy ostatni element naszej nowej funkcjonalności – po kliknięciu na przycisk chcielibyśmy, aby użytkownik zobaczył prosty komunikat powitalny. Zmodyfikujmy więc app.js w ten sposób:

Dla sprawdzenia możesz teraz otworzyć plik index.html w przeglądarce i sprawdzić czy wszystko działa tak jak powinno. Jeśli tak, to znowu mamy dobry moment na commit.

Przez to, że plik app.js jest już śledzony przez Gita, nowy commit z tą zmianą można dodać za pomocą jednej komendy:

git commit -am „welcoming message”

Tworzymy commit i dodajemy go.

Sekundę później przychodzi nam na myśl, że ten opis nie jest przyjazny użytkownikowi patrzącemu na historię zmian. Nie wiadomo czym właściwie jest ta wiadomość powitalna – czy da się coś z tym zrobić? Tutaj przychodzi nam na pomoc komenda git commit –amend dzięki której możemy zmienić wiadomość ostatniego commitu:

Edytor (w którym jest szansa, że się znajdziemy) to Vim – o jego obsłudze polecam poczytać w sieci, jednak kroki które możemy teraz wykonać to:

  1. naciśnięcie klawisza i aby przejść do trybu wstawiania
  2. poprawienie wiadomości commitu jak w typowym edytorze tekstowym – dopisujemy szczegóły zmiany (ja zmieniłem treść opisu na welcoming message displayed on button click)
  3. powrót do trybu podglądu klawiszem ESC
  4. wyjście z zapisem kombinacją klawiszy :wq

Uff… 😀 Jeśli wszystko poszło dobrze to zostaniemy poinformowani, że ostatni commit ma teraz nowy opis:

Warto dbać o jakość opisów naszych commitów, aby mówiły maksymalnie dużo o zmianie którą wprowadzają. W tym wpisie używam wielu uproszczeń, jednak aby commit message być użyteczny możemy się posłużyć wzorcem: „After applying this commit… [MESSAGE]”. Wiadomością jest więc opis tego co stanie się, jeśli ktoś ustawi się w repozytorium na tym konkretnym commicie.

Zobaczmy teraz jak wygląda stan naszego repozytorium na branchu add-js – ponownie korzystajmy z git log:

Historia posortowana jest od najnowszego commitu – dwa najstarsze commity pochodzą z brancha master, natomiast najnowsze umieszczane są na samej górze. Jeśli jesteśmy pewni, że wszystko działa tak jak powinno, możemy teraz wrzucić zmiany z brancha add-js na branch master, gdzie trzymamy projekt w stanie wzorcowym i w pełni działającym.

Podejście pierwsze – git merge

Najprostszym sposobem w jaki możemy dociągnąć zmiany z jednego brancha na drugi jest komenda git merge. Opis tej komendy znajdziecie tutaj.

Dla nas nie jest to jednak rozwiązanie docelowe (o czym za chwile) dlatego, aby nie zaśmiecać mastera, wróćmy na mastera i utwórzmy na jego podstawie nową gałąź fake-master:

Teraz dociągnijmy do naszego udawanego mastera gałąź add-js poleceniem git merge add-js:

To jeden z najprostszych scenariuszy – od momentu odłączenia się od mastera nie pojawił się tam żaden nowy commit, więc za pomocą strategii zwanej „fast-forward” zmiany z brancha add-js są po prostu dokładane jako kolejne do naszej historii zmian na masterze.

Na koniec sprawdźmy jeszcze jak wygląda log:

Historia brancha fake-master wygląda teraz identycznie jak add-js – dociągnęliśmy wszystkie commity zawierające pracę nad przyciskiem i skryptem app.js. 

Czy to źle? Zależy od twojego stylu pracy – jeśli nie musisz kłaść nacisku na czystą historię, to takie podejście jest dla ciebie wystarczające. Czasami jednak takie opisywanie ze szczegółami jak dochodzono do danej funkcjonalności nie jest mile widziane na branchu głównym. Niepotrzebnie zaśmiecamy historię wszystkimi commitami z brancha którego chcemy „domergować”.

Zobaczmy więc, jak można dorzucić zmiany z jednego brancha na drugi tak, aby po tej operacji historia zmian była bardziej przejrzysta.

Squash!

Na początek, jeśli jesteś na branchu fake-master wracamy do mastera poleceniem git checkout master. 

Chcielibyśmy teraz dociągnąć zmiany z brancha add-js w taki sposób, aby na masterze pojawił się tylko jeden commit podsumowujący całość zmian. Służy do tego komenda git merge –squash [BRANCH]:

Trzy kroki jakie możecie zobaczyć na powyższym screenshocie to:

  1. Pobranie zmian z feature brancha komendą git merge z dodatkową opcją –squash – opcja ta sprawi, że wszystkie zmiany z brancha add-js pojawią się na masterze (jako pliki w staging area), jednak nie powstanie automatycznie żaden commit który ich dotyczy.
  2. Wszystkie zmiany możemy zamknąć w jeden commit standardową komendą git commit i to właśnie robimy w kroku drugim.
  3. Na koniec sprawdzamy jak wygląda historia na masterze. Nasza nowa funkcjonalność została zamknięta w jeden commit, w związku z czym historia jest bardziej czytelna – osiągnęliśmy to, o co nam chodziło.

I to właściwie tyle! Nasz feature branch pozostał w stanie oryginalnym, na masterze mamy czystą historię a najnowszy commit zawiera wszystkie zmiany jakie pobraliśmy.

W przypadku kolejnych funkcjonalności które chcielibyśmy dodać proces wyglądałby tak samo. Ustawiamy się na branchu głównym, tworzymy na jego podstawie feature brancha, implementujemy co trzeba i na koniec mergujemy wszystko do mastera. Dodatkowo, w przypadku kolejnych osób pracujących nad projektem praca może odbywać się równolegle – tworzymy brancha na każdą funkcjonalność jakiej nam potrzeba i możemy nad nimi pracować w kilka osób.

Podstawy za nami!

Wprowadzeniem do pracy na kilku branchach kończymy dzisiejszego posta o obsłudze Gita z poziomu konsoli.

Komendy które dzisiaj przedstawiłem to tylko ułamek możliwości tego narzędzia, jednak jeśli do tej pory nie wiedzieliście jak ugryźć ten temat to mam nadzieję, że kilka kwestii wam się wyjaśniło. Dla bardziej zaawansowanych przygotowujemy posta o najbardziej użytecznych elementach Gita, których być może nie mieliście jeszcze okazji poznać – to wszystko już niebawem!

Jak wygląda wasz sposób pracy z tym narzędziem – jesteście fanami konsoli, czy nakładek na Gita takich jak np. Source Tree? Koniecznie zostawcie trzy grosze od siebie w komentarzach!

  • Paweł Szczygielski

    Próbowałem różnych narzędzi i najbardziej pasuje mi SmartGit – darmowy do projektów typu OpenSource.
    Podczas czytania artykułu przypomniałem sobie z czym miałem największe problemy podczas nauki Gita – była to wizualizacja, jak działają poszczególne komendy. Swego czasu aż napisałem o tym kilka słów – http://bit.ly/2rPUVQG – może jakieś rozwinięcie tego tematu?

    • Przemek Smyrdek

      Wizualizacja jest chyba najczęściej używanym narzędziem przy tłumaczeniu komend – nie ma to jak obrazowe zrozumienie tego co się dzieje. Na teraz nie mogę potwierdzić, że w najbliższym czasie zamieścimy coś na ten temat na blogu, jednak mogę polecić znakomitą prezentację gdzie poza Gitem i konsolą wykorzystywane jest narzędzie które pokazuje strukturę branchy i commitów:
      https://vimeo.com/163822148

  • Dominik

    Polecam wykorzystać kilka fajnych pomysłów na aliasy w gicie np. z tego wpisu https://blog.scottnonnenberg.com/better-git-configuration/
    Szczególnie fajnie wygląda wówczas git glog – polecam!

    Jestem też fanem polecenia ‚git merge branch –no-ff’, dzięki czemu na masterze nie ma poszczególnych commitów z brancha, ale na logu (np. w gitk) widać je jako takie zgrabne odgałęzienie.

    Fajnie jest też korzystać z Powershella i narzędzia Posh-Git (szczególnie w cmderze), dzięki czemu widzimy aktualny status zmian nad promptem
    https://uploads.disquscdn.com/images/660c66b027d850327487d0724b3852e2637a1f7f359c596e4b8b0d77995c8952.png

    Super, że pojawił się tu taki fajny wprowadzający wpis. Czekam na następny 🙂

    • Przemek Smyrdek

      Kolejna osoba wspominająca Posh-Git – muszę sprawdzić o co chodzi 😉

      Osobiście pracuje na Macu, w konfiguracji Z Shell i OhMyZsh i tam podgląd stanu brancha na żywo to standard – zdecydowanie polecam coś takiego.