W związku z tym, że ostatnio coraz częściej korzystam z systemu kontroli wersji GIT (zamiast używanego przez kilka ostatnich lat Subversion) postanowiłem napisać kilka słów na jego temat.

  1. Wprowadzenie
  2. GIT jest systemem kontroli wersji, którego używał Linus Torvalds podczas pracy nad kernelem linuxa – z resztą do tej pory kernel jest kontrolowany przez GIT. Główną różnicą od svn, która skłoniła mnie do korzystania z niego, jest jego decentralizacja. W Subversion niezbędne jest stworzenie jednego głównego repozytorium które kontroluje nasz projekt. Każdy klient łączy się z tym centralnym repozytorium podczas pracy nad projektem i każda zmiana musi być wysłana na serwer. GIT działa w architekturze P2P repozytorium nie musi być centralne. Może istnieć kilka równoległych, które będą potem synchronizowane. Istnieje nawet możliwość synchronizacji przez mail :) Oczywiście nic nie stoi na przeszkodzie, aby używać centralnego repozytorium, zwłaszcza podczas pracy nad dużym projektem. Niejednokrotnie jednak miałem odwrotną sytuację – pracowałem nad czymś małym, a mój edytor (vim ;) ) nie posiada wbudowanej obsługi kontroli wersji. Chcąc objąć projekt kontrolą nie chciało mi się tworzyć repozytorium svn – bo za dużo roboty. Więc praca którą wykonywałem była ‚na żywioł’. Cofnięcie wersji było równoznaczne z napisaniem kodu od nowa.

    Od tego czasu ropocząłem korzystanie z git. Sami zobaczcie jakie to proste. Opiszę kilka podstawowych operacji potrzebnych do pracy z lokalnym repozytorium. Jeżeli mi się uda, to w późniejszym czasie opiszę sposoby pracy z repozytorium zdalnym.

  3. Tworzymy projekt
  4. Na początek coś o niezbyt skomplikowanej strukturze :

    git_tree

    W języku „GITowym” projekt, nad którym pracujemy jest nazwany working tree.

    Wszystko co musimy zrobić, aby objąć projekt kontrolą wersji jest użycie komendy:

    Spowoduje to utworzenie ukrytego folderu .git, w którym umieszczane są informacje o naszym projekcie.

    W większości systemów kontroli wersji, przechowywanie informacji o plikach umiejscowia się w dwóch miejscach. Bezpośrednio w projekcie lub w centralnym repozytorium. GIT wprowadził coś nowego – warstwę pośrednią pomiędzy projektem, a repozytorium: Staging area (lub Staging Index). W indeksie tym przechowywane są informacje o całym naszym projekcie, tu kontroluje się zmiany i w przypadku, gdy jesteśmy ze zmian zadowoleni to je commitujemy do repozytorium. Daje to nam taką korzyść, że nie zaśmiecamy ani projektu, ani repozytorium zbędnymi informacjami.

  5. Staging index
  6. Aby wprowadzić plik do indexu wystarczy wydać komendę:

    Warto zauważyć, że index przechowuje informacje o wszystkich plikach, nie tylko o plikach zmienionych.

    Dodajemy nasz plik do projektu:

    Oraz cały folder:

    Teraz wszystkie pliki, które posiadamy znajdują się w Staging area.

    staging index

    W każdej chwili, możemy zobaczyć, co się zmieniło w naszym projekcie od ostatniego zatwierdzenia (commit) wydając komendę:

    > git status

    Pokazywany nam jest raport ze zmianami w naszym working tree. Pliki mogą być wymienione w dwóch oddzielnych sekcjach raportu: pliki zmienione i dodane do staging area oraz pliki zmienione i nie dodane do staging area.

  7. Zatwierdzanie zmian
  8. Gdy wszystkie zmiany, których dokonaliśmy są OK musimy je zatwierdzić. Dokonujemy tego poleceniem:

    > git commit

    otworzy się edytor, gdzie należy wpisać opis commitu. Jeżeli tytuł jest krótki, możemy go wpisać od razu przy commicie za parametrem ‚-m’. Na przykład:

    > git commit -m „initial commit”

    commit

    W GIT nie nadaje się liczbowych numerów rewizji, tak jak ma to miejsce np. w Subversion. Tutaj każdy commit jest określony kluczem SHA wyliczanym na podstawie zawartości working tree. Dla przykładu w moim prostym projekcie pierwszy commit otrzymał identyfikator:

    52e0188f4b5f082427cc9df07451cc5daa169f63

    Może na początku wydawać nam się dziwne takie identyfikowanie wersji, ale jest skuteczne. Zwłaszcza, że musimy pamiętać o architekturze systemu git. Tutaj nie ma centralnego repozytorium i można prowadzić dwa równoległe. Gdy po jakimś czasie zdecydowalibyśmy się na synchronizację to byłby problem, bo w każdym repozytorium znalazłyby się rewizje 1, 2, 3 itd. ale zawartości tych rewizji byłyby różne. Aby zapobiec takim sytuacjom wprowadzono klucz SHA jako identyfikator. Co jednak jeżeli w przyszłości chcielibyśmy cofnąć nasz projekt do któregoś momentu? Czy musimy podać cały klucz? Otóż nie. Wystarczy podać tyle pierwszych znaków ile wystarczy na jednoznaczne zidentyfikowanie. W przypadku na przykład gdy mamy dwie rewizje i klucz każdej zaczyna się od innej literki (albo cyferki) to wystarczy podać pierwszy znak klucza aby jednoznacznie zidentyfikować. Niepisaną regułą jest podawanie 7-iu znaków do identyfikacji rewizji. Jest to wystarczająca w większości przypadków liczba jednoznacznie identyfikująca. Tyle też znaków jest podawanych po zcommitowaniu zmian w raporcie.

    Commit_SHA

    Commit tak samo jak Staging area jest „stemplem” całego working tree – są tam informacje o wszystkich plikach i folderach znajdujących się w danym momencie, a nie tylko te, które się zmieniły.

    staging Tree

    Podsumowując: cała operacja kontroli wersji wykonywana jest w trzech krokach: Pierwszym krokiem jest zmiana zawartości working tree, np. dodanie pliku, usunięcie pliku lub edycja pliku. Drugim krokiem jest zatwierdzenie tej zmiany i przeniesienie jej do Staging area poprzez wykonanie komendy git add. Trzecim krokiem jest zatwierdzenie zmian poprzez git commit i przeniesienie całego Staging index do Commit.

    Co to nam daje? Wyobraźmy sobie, że pracujemy z większym projektem. Dokonaliśmy już kilkadziesiąt modyfikacji i nagle otrzymujemy wiadomość, że w jednym z plików jest poważny błąd. Naprawiamy go, stage’ujemy go (git add) a następnie commitujemy poprawkę. Po czym spokojnie wracamy do naszej pracy, bo reszta plików, którą zmieniliśmy podczas naszej dotychczasowej pracy została nietknięta w naszym working tree. Jak dokonalibyśmy tego w subversion? Musielibyśmy się nakombinować z filelock albo z branchami. Tu mamy to out-of-the-box.

    Może wydawać się trudne prowadzenie w ten sposób repozytorium, ale po krótkim czasie przyzwyczajamy się, a po kilku chwilach potem nie będziemy potrafili bez tego żyć :)

    Istnieje także ułatwienie aby oba kroki dokonać za jednym razem. Podczas commita dodajemy parametr -a i wszystkie pliki, które zostały zmienione (ale nie te co zostały dodane) automatycznie przenoszone są do Staging index i wykonywany jest commit.

  9. Sprawdzanie zmian
  10. Aby zobaczyć co działo się w naszym repozytorium wystarczy wykonać komendę :

    > git log

    Wyświetli nam się lista zmian, rozpoczynając od ostatniej. Podane są informacje o symbolu commita, autorze, dacie i wiadomość, która została wpisana wraz z commitem.
    Dodanie parametru --pretty=oneline pokaże w jednej lini klucz sha i message, a dodanie jeszcze parametru --abbrev=commitskróci klucz do 7-miu znaków.

  11. Przykład
  12. Myślę, że w tym miejscu należy się jakiś solidny przykład do komend, które właśnie poznaliśmy. Zatem wracając do przykładu z początku artykułu – pracujemy nad bardzo ubogim projektem posiadającym jeden plik w folderze głównym oraz jeden folder z drugim plikiem.

    Wykonujemy

    aby zainicjować kontrolę wersji. Zaraz potem wykonujemy

    aby zobaczyć co się zmieniło. Otrzymujemy:

    Czyli zarówno plik jak i folder nie są pod kontrolą wersji. Dodajmy plik:

    i sprawdźmy status raz jeszcze:

    W tej chwili gdybyśmy wykonali commit – dodałoby do commita tylko plik a nie folder. Nie chcemy tego (na razie) więc dodajmy także folder.

    i zatwierdzamy zmiany jako „inital commit” czy jak tam chcemy nazwać:

    Od razu otrzymujemy odpowiedź:

    widać nazwę commita oraz 7znaków klucza SHA. Sprawdźmy status:

    Czyli jest ok. Sprawdźmy zatem staging w praniu. Zmieńmy zawartość obu plików i sprawdźmy status:

    teraz jeden plik dodajmy do staging area i zcommitujmy:

    Czyli jest tak jak zamierzaliśmy – w commicie znalazł się tylko plik1.txt – plik2.txt pozostał dalej w working tree. Zmieńmy ponownie plik1.txt i dodajmy do staging area – wrócimy do poprzedniej sytuacji..

    czyli posiadamy dwa edytowane pliki, ale jeden tylko w staging index. Spróbujmy zatwierdzić je oba razem:

    Nice… dodało oba pliki, ten który był w staging area oraz drugi który nie był. Pamiętajmy, że w przypadku, gdy dodamy nowy plik to musimy go ręcznie dodać do staging index.
    Rzućmy okiem na log naszego repozytorium:

    Możemy także z ciekawości zerknąć na to co wyświetli nam samo git log

    Skoro przyjęliśmy już jakiś zakres wiedzy jedźmy dalej:)

  13. Cofanie zmian
  14. Co jednak jeżeli chcemy cofnąć zmiany, które wykonaliśmy na plikach? Wszystko zależy od tego jak daleko chcemy się cofnąć. Inaczej jest gdy zmiany nie zostały dodane do staging area a inaczej, jeżeli pliki już są dodane.

    Jeżeli pliki nie są w staging area wystarczy wykonać komendę

    Sprawdźmy:

    Jeżeli nie podamy nazwy pliku to zostaną cofnięte wszystkie zmiany w naszym working tree
    checkout

    Jeżeli pliki są dodane już do staging area, musimy je najpierw z niego „wyjąć” a potem dopiero cofnąć zmiany. Do tego służy komenda

    Jeżeli nie podamy nazwy pliku to zostaną zresetowane wszystkie. Dopiero jak plik zresetujemy to możemy go cofnąć poprzez git checkout
    HEAD odnosi się zawsze do ostatniego commita

    domyślne 9

    Przykład:

  15. Cofanie zmian zatwierdzonych
  16. Zawsze podczas pracy jest tak, że trzeba się cofnąć do poprzedniego commita. Jeżeli będziemy do tego zmuszeni to dokonamy tego poprzez

    (Oczywiście, że klucz wystarczy podać w postaci 7-io zakowej. ) Co się wtedy stanie? Technicznie nie cofamy się lecz idziemy do przodu – zostaje utworzony nowy commit, który jest identyczny wraz z commitem do którego się odnosimy:

    Nic nie stoi na przeszkodzie, abyśmy cofnęli się dalej niż do ostatniego commita, jednak to może powodować problemy z synchronizacją (a o tym jak je rozwiązywać będzie później). Cofamy się do commita „2nd commit”:

    Revert

  17. Porównywanie wersji
  18. Jeżeli chcemy obejrzeć różnice między naszym working tree a commitem używamy komendy :

    używając oczywiście klucza commita, do którego chcemy się porównywać.
    Jeżeli jednak chcemy obejrzeć różnice pomiędzy dwoma różnymi commitami to używamy komendy:

    Gdy chcemy obejrzeć całą historię zmian, która nastąpiła w naszym projekcie to używamy:

  19. Tagowanie
  20. Głównym problemem Subversion jest to, że tagowanie repozytorium polega na kopiowaniu całego repozytorium w inne miejsce. Przy dużych projektach może to być problem ze względu na szybko rozrastające się repozytorium. W GIT’cie tego nie ma. Gdy chcemy robić taga, to po prostu oznaczamy jednego z commitów i nie tracimy miejsca na dysku. Tagowanie jest przydatne o tyle, że możemy używać taga zamiast klucza SHA odwołując się do tego konkretnego commita. W jaki sposób tagujemy? Jeżeli chcemy otagować ostatniego commita wystarczy podać:

    Jeżeli jednak chcemy otagować commita, który nie jest ostatnim, to musimy podać jego klucz SHA na końcu:

    Naturalną czynnością jest tagowanie kolejnych wersji naszego projektu, np.

    Jak już wspomniałem, można używać tagów zamiast kluczy SHA, np.:

    porównuje commity otagowane kolejno „wesja_1” oraz „wersja_1.2”
    tags

  21. Praca równoległa
  22. Pracując w większych zespołach nad jednym projektem zdarza się, że każdy z członków zespołu pracuje nad inną funkcjonalnością. Są jakoby prowadzone dwie (lub więcej) równoległych ścieżek powstawania projektu. Oba te ‚podprojekty’ będą połączone w całość. Może być nawet tak, że chcemy „wypróbować” czy np. nowa technologia będzie dobrze działać z naszym projektem. Nie warto wtedy robić tego w głównej ścieżce powstawania tylko wypada zrobić równoległą. Takie równoległe nazywa się gałęziami (branches). Branchowanie to rozdzielanie projektu na kilka równoległych ścieżek powstawania, gdzie na koniec oba branche mogą zostać połączone z powrotem w jedną ścieżkę (merging). Automatycznie gdy inicjujemy projekt powstaje branch o nazwie master. Jest to główna ścieżka powstawania projektu. Mogliśmy zauważyć tą nazwę np. podczas dokonywania commitu. Jeżeli chcemy zobaczyć który branch jest aktualny wydajemy komendę:

    Ukaże nam się informacja o nazwie brancha, kluczu commita i jego nazwie, np.:

    Listę branchy możemy w każdej chwili zobaczyć wydając komendę:

    gwiazdka przy nazwie oznacza aktualny branch
    Tworzenie oddzielnej gałęzi

    Stworzenie nowego brancha sprowadza sie do wydania komendy git branch wraz z jego planowaną nazwą oraz kluczem commita, który ma być jakby punktem startowym dla tej gałęzi. Jak zawsze, jeżeli nie podamy klucza to będzie użyty aktualny commit.

    Przełączanie się między branchami
    Aby przełączyć się pomiędzy gałęziami musimy użyć komendy git checkout z nazwą brancha którego chcemy użyć.

    branch
    domyślne 14

    Teraz wszystkie zmiany których dokonamy będą zatwierdzane w tej gałęzi

    commitowanie do brancha
    Przełączając się między gałęziami aktywujemy ostatni commit tej gałęzi, np. wykonianie

    aktywuje commit, w którym gałęzie się rozdzieliły (akurat w tym przypadku) i każdy koleny commit będzie od tego miejsca w tej właśnie ścieżce.
    branch_back

    commit to master
    W systemie GIT tworzenie branchy jest tak samo „tanie” jak tagów. To jest jeden z powodów „wyższości GIT nad Subversion” (moim zdaniem). W związku z tanim kosztem utworzenia brancha, niektórzy programiści tworzą je przy każdej okazji (nowy feature w nowym branchu).
    W życiu każdego projektu podzielonego na kilka gałęzi nadchodzi taka chwila, gdzie trzeba będzie się z powrotem połączyć.

  23. Łączenie równoległych gałęzi
  24. Jak już prędzej gdzieś napisałem – łączenie dwóch równoległych gałęzi nazywa się mergingiem(nie podoba mi się spolszczanie angielskich wyrazów, jeśli jednak chodzi o język techniczny to nie mam nic przeciw – bo każdy może sobie przetłumaczyć inaczej i nie zrozumiemy nawet prostych przekazów.)
    Łączenia dokonujemy z gałęzi do której łączymy ( przeważnie jest to master, czyli główna linia projektu) i podajemy nazwę brancha który ma być włączony. Dla przykładu aktualnie jesteśmy w masterze w Commit’cie „5th commit” i chcemy połączyć się z branchem „slave”:

    tym razem obyło się bez problemów.
    W gałęzi master został utworzony nowy commit (nazwany „merge branch ‚slave'”) w którym zostały uwzględnione poprawki z obu gałęzi.

    merging

    Czasami jednak nie jest tak wesoło. Zwłaszcza gdy w obu ścieżkach wyedytujemy ten sam plik. Spróbujmy wyedytować plik1.txt w obu gałęziach jednocześnie.

    Teraz przełączamy się z powrotem do głównej gałęzi i robimy merge

    Niestety w równoległej pracy często tak się zdarza, że sa konflikty. Jak je ominąć? Musimy wyedytować konfliktowy plik. Zobaczymy tam coś takiego:

    Dopisane zostały wersje z obu plików wraz z nagłówkami, która wersja pochodzi z której gałęzi. W takim wypadku musimy ręcznie usunąć tą wersją, która jest zła, oraz oba nagłówki. Następnie musimy dodać plik do staging area poprzez git add plik1.txt i to wszystko – po konflikcie.

    Na koniec wypada usunąć nieużywaną gałąź używając komendy:

    i po kłopocie :)

Mam nadzieję, że chociaż troszeczkę przybliżyłem pracę z systemem kontroli wersji GIT. W najbliższym czasie postaram się opisać bardziej zaawansowane możliwości tego systemu.
Miłego mergowania.
.