Git merge – o co chodzi z tym całem scalaniem?

W poprzedniej lekcji zajęliśmy się tworzeniem dodatkowych branchy m.in po to, aby każdy feature mógłbyć odłożony na osobnym branchu oraz wszyscy programiści mogli pracować równolegle nad jednym wspólnym projektem. W końcu taka jest idea systemu kontroli wersji – Git.

Skoro jest Git i mamy już branche to cza je scalić z gałęzią główną – scalić.

Czym jest merge?

Merge jest niczym innym jak scalaniem gałęzi projektu, nie musi to być tylko scalanie do gałęzi głównej – bez problemu możemy mergować gałęzie między sobą jak tylko nam się podoba.

No nie dokońca…

Istnieje jeszcze coś takiego jak konflikty…

Powiedziałem, że możemy mergować gałęzię projektu jak żywnie nam się to podoba, ale to nie jest do końca prawda. Podczas próby scalania gałęzi mogą wystąpić tak zwane merge conflicts – konflikty scalania.

Czym są konflikty scalania? Może lepiej będzie zapytać – kiedy one występują?

Występują one, gdy na łączonych branchach w tym samym miejscu w pliku są różne zmiany – już pokazuję na małym przykładzie.

Na początek przypuśćmy, że plik Main.java na masterze wygląda tak:

package pl.maniaq;

public class Main {

    public static String isPositive(int number) {
        if (number > 0) {
            return "Positive";
        }
        return "Negative";
    }

    public static void main(String[] args) {
        System.out.println(isPositive(5));
    }
}

Przypuśćmy, że Programista 1 zmodyfikował plik Main.java na branchu feature/any-feature w ten sposób:

package pl.maniaq;

public class Main {

    public static String isPositive(int number) {
        boolean isPositiveNumber = number > 0;
        if (isPositiveNumber) {
            return "Positive";
        }
        return "Negative";
    }

    public static void main(String[] args) {
        System.out.println(isPositive(5));
    }
}

Programista 2 stwierdził, że zrobi taką zmianę na branchu feature/next-feature:

package pl.maniaq;

public class Main {

    public static String isPositive(int number) {
        return number > 0 ? "Positive" : "Negative";
    }

    public static void main(String[] args) {
        System.out.println(isPositive(5));
    }
}

Jak widzisz mamy trzy wersje tego samego pliku – gdzie mamy konflikt podczas scalania?

Konflikt

O ile pierwszy programista może bez problemu scalić swoją gałąź feature/any-feature to po tym scaleniu drugi programista już nie będzie mógł tego zrobić. Dlaczego?

Ponieważ na branchu feature/next-feature git ma w historii inny stan Main, który nie jest równy ze stanem, który dołączył feature/any-feature i w taki o to sposób wytwarzają się merge conflicts.

Jak je rozwiązywać?

Jest kilka sposobów rozwiązywania konfliktów – ja pokażę Ci chyba najprostszy – przy użyciu git pull.

Drugi programista  będąc na branchu feature/next-feature musimy zaciągnąć zmiany, które zaszły na masterze (czyli zmiany, które wprowadził feature/any-feature branch), po zaciągnięciu zmian Git nas poinformuje, w których plikach i w którym miejscu wystąpiły konflikty.

Konflikty niestety musimy rozwiązywać ręcznie – przez rozwiązywanie mam na myśli wybranie ostatecznego rozwiązania, które ma zostać zapisane na masterze. Po tym wszystkim mamy już możliwość zmergować branch feature/next-feature do mastera.

Nie martw się – dużo teorii, ale już przechodzimy do praktyki na podstawie projektu z poprzedniej lekcji.

Git merge

Aby być na bieżąco musisz mieć projekt, który został stworzony w poprzedniej lekcji – możesz go zobaczyć tutaj.

W tym projekcie są stworzone dwa branche:

  create-user
  feature/calculator
* master

Na początku zmergujmy sobie gałąź create-user. Wystarczy do tego być na gąłęzi master – ponieważ do niej właśnie będziemy mergować innych branch. W tym celu użyjemy na początku komendy:

git checkout master

Aby zmergować gałąź musimy użyć komendy git merge <nazwa-brancha>

git merge create-user

Jeśli wszystko się powiedzie to otrzymamy taki komunikat:

Updating e09ee4d..2720852
Fast-forward
 src/pl/maniaq/Main.java | 22 +++++++++++++++++++++-
 src/pl/maniaq/User.java | 22 ++++++++++++++++++++++
 2 files changed, 43 insertions(+), 1 deletion(-)
 create mode 100644 src/pl/maniaq/User.java

Spójrz teraz na strukturę projektu i zobacz, że zostały dodane zmiany z brancha create-user m.in zmienił się plik Main na:

package pl.maniaq;
import java.util.Scanner;

public class Main {

    static Scanner scanner = new Scanner(System.in);

    public static void createUser() {
        String name, lastname;
        Integer age;

        System.out.println("Type a name: ");
        name = scanner.next();

        System.out.println("Type a lastname: ");
        lastname = scanner.next();

        System.out.println("Type your age: ");
        age = scanner.nextInt();

        User user = new User(name, lastname, age);
        System.out.println("Utworzono Usera: " + user.toString());
    }

    public static void main(String[] args) {
        createUser();
    }
}

Spróbujmy teraz dołączyć branch feature/calculate – pamiętaj, aby być na masterze!

git merge feature/calculator

I otrzymamy komunikat o konfliktach:

Auto-merging src/pl/maniaq/Main.java
CONFLICT (content): Merge conflict in src/pl/maniaq/Main.java
Automatic merge failed; fix conflicts and then commit the result.

Mergowanie się rozpoczęło – po zakończeniu rozwiązywania konfliktór musimy zrobić commita, aby zapisać zmiany.

Jak widzisz branche się połączyły, jednak kod nie jest możliwy do wykonania, ponieważ plik Main.java wygląda aktualnie tak:

package pl.maniaq;
import java.util.Scanner;

import java.util.Scanner;

public class Main {

    static Scanner scanner = new Scanner(System.in);

<<<<<<< HEAD
    public static void createUser() {
        String name, lastname;
        Integer age;

        System.out.println("Type a name: ");
        name = scanner.next();

        System.out.println("Type a lastname: ");
        lastname = scanner.next();

        System.out.println("Type your age: ");
        age = scanner.nextInt();

        User user = new User(name, lastname, age);
        System.out.println("Utworzono Usera: " + user.toString());
    }

    public static void main(String[] args) {
        createUser();
=======
    public static void calculate() {
        Integer x, y;

        System.out.println("Type first number: ");
        x = scanner.nextInt();

        System.out.println("Type second number: ");
        y = scanner.nextInt();

        System.out.println("Sum: " + Calculator.add(x, y));
        System.out.println("Subtract: " + Calculator.subtract(x, y));
        System.out.println("Multiply: " + Calculator.multiply(x, y));
        System.out.println("Divide: " + Calculator.divide(x, y));
    }


    public static void main(String[] args) {
        calculate();
>>>>>>> feature/calculator
    }

}

Przyjrzyj się całemu kodowi, wyróżnimy tam kilka zapisów, które zrobił dla nas Git.

<<<<<<< HEAD

Oznacza, gdzie zaczyna się wersja z HEAD – czyli brancha, do którego mergujemy, w naszym przypadku jest to master

=======

Oznacza, gdzie kończy się wersja HEAD, a gdzie zaczyna się wersja mergowanego brancha

>>>>>>> feature/calculator

Oraz ostatni zapis oznaczający gdzie kończy się wersja mergowanego brancha.

Oczywiście takich zapisów może być wiele w jednym pliku i projekcie – występują one wszędzie, gdzie tylko jest konflikt.

Rozwiązywanie konfliktu…

Jak wziąć się za rozwiązywanie konfliktu?

Musimy podjąć decyzję, którą wersję usunąć, którą zostawić – albo czy może możemy je scalić.

Na początku usuńmy wszystkie wpisy o konfliktach z gita, twój plik powinnien wyglądać teraz tak:

package pl.maniaq;
import java.util.Scanner;

import java.util.Scanner;

public class Main {

    static Scanner scanner = new Scanner(System.in);

    public static void createUser() {
        String name, lastname;
        Integer age;

        System.out.println("Type a name: ");
        name = scanner.next();

        System.out.println("Type a lastname: ");
        lastname = scanner.next();

        System.out.println("Type your age: ");
        age = scanner.nextInt();

        User user = new User(name, lastname, age);
        System.out.println("Utworzono Usera: " + user.toString());
    }

    public static void main(String[] args) {
        createUser();

    public static void calculate() {
        Integer x, y;

        System.out.println("Type first number: ");
        x = scanner.nextInt();

        System.out.println("Type second number: ");
        y = scanner.nextInt();

        System.out.println("Sum: " + Calculator.add(x, y));
        System.out.println("Subtract: " + Calculator.subtract(x, y));
        System.out.println("Multiply: " + Calculator.multiply(x, y));
        System.out.println("Divide: " + Calculator.divide(x, y));
    }


    public static void main(String[] args) {
        calculate();
    }

}

I czas się za rozwiązywanie konfliktów – w tym przypadku oba branche wprowadziły nowe feature, czyli nie możemy za bardzo nic usunąć – musimy oba zostawić.

W tym celu zlikwidujemy jedną metodę main i zostawimy jedną w takiej postaci:

public static void main(String[] args) {
    createUser();
    calculate();
}

Metodę calculate przeniesiemy nad metodę main, aby było czytelniej i ostatecznie cały plik wygląda tak:

package pl.maniaq;
import java.util.Scanner;

import java.util.Scanner;

public class Main {

    static Scanner scanner = new Scanner(System.in);

    public static void createUser() {
        String name, lastname;
        Integer age;

        System.out.println("Type a name: ");
        name = scanner.next();

        System.out.println("Type a lastname: ");
        lastname = scanner.next();

        System.out.println("Type your age: ");
        age = scanner.nextInt();

        User user = new User(name, lastname, age);
        System.out.println("Utworzono Usera: " + user.toString());
    }

    public static void calculate() {
        Integer x, y;

        System.out.println("Type first number: ");
        x = scanner.nextInt();

        System.out.println("Type second number: ");
        y = scanner.nextInt();

        System.out.println("Sum: " + Calculator.add(x, y));
        System.out.println("Subtract: " + Calculator.subtract(x, y));
        System.out.println("Multiply: " + Calculator.multiply(x, y));
        System.out.println("Divide: " + Calculator.divide(x, y));
    }

    public static void main(String[] args) {
        createUser();
        calculate();
    }
}

To jeszcze nie koniec mergowania…

O ile rozwiązaliśmy wszystkie konflikty to naszym obowiązkiem jest teraz sprawdzić czy, aby na pewno cały kod aplikacji jest poprawny i czy aplikacja działa poprawnia.

W tym celu musimy sprawdzić czy aplikacji się kompiluje, czy wszystkie testy się wykonują (o ile są napisane wcześniej) oraz można się dodatkowo “przeklikać” przez podstawowe funkcjonalności aplikacji, aby sprawdzić czy nic nie popsuliśmy.

Nigdy nie omijaj tego punktu – zawsze sprawdzaj konfliktowy kod – choć nawet, gdy na pozór wydaje się być wszystko ok!

I na koniec!

Jeśli wszystko mamy już rozwiązane i przetestowane możemy zapisać zmiany na gałąź:

git add *
git commit -m "Resolve merge conflicts" -m "Merged branch: feature/calculator"

Zajrzyjmy na koniec do historii zmian – git log:

commit 8c02384963085966731eb923c759e22f17cd13e8
Merge: 2720852 e86c8d9
Author: klimson <klimson@gitlab.com>
Date:   Wed Oct 3 17:40:46 2018 +0200

    Resolve merge conflicts
    
    Merged branch: feature/calculator

commit e86c8d90aa05187f7105f81a62c507a16851842f
Author: klimson <klimson@gitlab.com>
Date:   Tue Oct 2 19:47:36 2018 +0200

    Implement method calculate in main class
    
klymek@klymek ~/programming/java/branch&merge $ git log
commit 8c02384963085966731eb923c759e22f17cd13e8
Merge: 2720852 e86c8d9
Author: klimson <klimson@gitlab.com>
Date:   Wed Oct 3 17:40:46 2018 +0200

    Resolve merge conflicts
    
    Merged branch: feature/calculator

commit e86c8d90aa05187f7105f81a62c507a16851842f
Author: klimson <klimson@gitlab.com>
Date:   Tue Oct 2 19:47:36 2018 +0200

    Implement method calculate in main class
    
    Calculate method use Calculator class to calculating basic math operations

commit 4431d049ae8dfeadb5446200d16d1d7c45a5d5b2
Author: klimson <klimson@gitlab.com>
Date:   Tue Oct 2 19:43:25 2018 +0200

    Create calculator class
    
    Class contains methods: add/substract/multiply/divide

commit 2720852d2eb7afde997c907d990a8b4a8b14f3b8
Author: klimson <klimson@gitlab.com>
Date:   Tue Oct 2 19:34:12 2018 +0200

    Call createUser function in main method

commit ed4163a50658c78d7339a1b93fc05cc265ce2e93
Author: klimson <klimson@gitlab.com>
Date:   Tue Oct 2 19:32:00 2018 +0200

    Implement create user method in main class

commit 0514884fc4dd0d355c7e72da54655fa3b2e76e08
Author: klimson <klimson@gitlab.com>
Date:   Tue Oct 2 19:28:39 2018 +0200

    Create User class

commit e09ee4d2f7d3fc13bad8094b42d2cae758c08ad0
Author: klimson <klimson@gitlab.com>
Date:   Tue Oct 2 19:23:57 2018 +0200

    Init commit

Jak widać widzimy wszystkie commity ze wszystkich branchy, które zostały scalone do mastera.

Pamiętaj, że całe lokalne repozytorium możesz wypchnąć na zdalne przy użyciu komendy:

git push origin --all

Dzięki temu możesz zobaczyć moje repozytorium tutaj. 😉

Podsumowanie

I to na tyle podstaw pracy z wieloma gałęziami (i dodatkowo wieloma programistami) równolegle. Choć jeszcze chcę wspomnieć o jednej ważnej zalecie pracy z branchem – w każdej chwili możemy przełączać się między branchami zajmować się dzieki temu różnymi zagadnieniami jednocześnie.

Przypuśćmy, że tworzymy jakieś super-ultra-extra nowy feature do kolejnej wersji aplikacji, ale dostajemy informację od klienta, że znalazł błąd – co robimy?

Tworzymy szybko nowy branch np. hotfix/any-bug, przełączamy się na niego, robimy potrzebne zmiany, mergujemy go do mastera i na koniec wystarczy już tylko wrócić do brancha tworzonego przez Ciebie feature  – nic Ci nie zginie.

Jeśli chcesz przećwiczyć sobie tworzenie branchy i ich mergowanie to stwórz najprostszy projekt – może nawet zawierać tylko pliki tekstowe, albo może być projektem Javowym. Zainicjalizuj w nim repozytorium git, stwórz kilka branchy, na każdym z nich twórz nowe pliki, rób przeróżne zmiany i próbuj je mergować do mastera.

Nie będę w tej lekcji pisał dla Ciebie zadania, wystarczy, że popiszesz kilkukrotnie poznane komendy i sam spróbujesz zmergować branche – jeśli coś Ci nie wyjdzie to pamiętaj, aby pisać w komentarzu.

Jeśli zaś wszystko rozumiesz to pamiętaj, aby wyrabiać w sobie dobry nawyk i tworzenie każdego nowego zadania rozpoczynać od stworzenia nowego brancha – i do tego będę Cię namawiał w kolejnych aplikacjach domowych. 😉

 

Kamil Klimek

Od 2016 jestem programistą Java. Przez pierwsze 4 lata pracowałem jako Full Stack Java Developer. Później postanowiłem postawić nacisk na Javę, żeby jeszcze lepiej ją poznać.

Subscribe
Powiadom o
guest
3 komentarzy
najstarszy
najnowszy oceniany
Inline Feedbacks
View all comments
Poul
Poul
5 lat temu

Mam problem z plikiem .gitignore, jest stworzony w katalogu projektu, są dodane foldery, ale to nic nie daje, i tak do zdalnego dodaje mi wszystko?
https://github.com/Poul12/ShuffleMachine

Kamil Klimek
Kamil Klimek
5 lat temu
Reply to  Poul

Czy nie umieściłeś przypadkiem .gitignore w katalogu .git? Prawdopodobnie wtedy to nie zadziała.

Jeśli nie to nie mam pojęcia czemu może to nie działa, zawsze możesz zignorować pliki dopisując również do pliku .git/info/exclude

Jeśli będzie chciał usunąć katalogi z repozytorium to użyj komendy:
git rm -r
i dla plików
git rm
I po tym wszystkim dać oczywiście commita tak jak po komendzie add.

Daj znać jaki jest rezultat problemu. 😉

Poul
Poul
5 lat temu
Reply to  Kamil Klimek

Dzięki, pomogło dodanie wykluczonych folderów do pliku exclude 😉

3
0
Would love your thoughts, please comment.x