Sprawdzanie czy mamy konflikt w Gicie

Praca z Gitem jest bardzo przyjemna… aż do chwili kiedy pojawiają się konflikty.

Jest kilka zasad, które umożliwiają ich ograniczanie do minimum, między innymi:

  • dziel zadania na mniejsze
  • komituj często
  • stosuj narzędzia do statycznej analizy kodu

A w tym poście opiszę jeszcze jedno narzędzie, które pomoże nam ich liczbę zmniejszyć!

Napisałem skrypt, który – uruchomiony w repozytorium – pokaże czy nasza gałąź ma konflikt z jakąkolwiek inną. Oto jego użycie:

$ ~/check-conflicts.bash
conflict with refs/heads/f1
$ echo $?
1

Jak widać, pokazuje z którą gałęzią jest problem oraz zwraca status 1. W przypadku braku konfliktu, status będzie wynosił 0, a skrypt nie wypisze nic.

Opiera się on głównie jednym, nieco mniej popularnym poleceniu Gita: git merge-tree. Działa ono w następujący sposób: jako argumenty pobiera hasz komitu, który jest wspólnym przodkiem dwóch gałęzi, które mają być połączone oraz nazwy tych gałęzi, a na wyjście wypisuje wynik merge’owania – nie dokonując jednak żadnych zmian w systemie plików. W pewnym sensie jest więc czymś w stylu opcji --dry-run przy git merge (której w rzeczywistości nie ma 😉 ).

Wyniki działania tej komendy są następujące. Przy konflikcie:

changed in both
  base   100644 ce013625030ba8dba906f756967f9e9ca394464a file
  our    100644 cc628ccd10742baea8241c5924df992b5c019f71 file
  their  100644 317e9677c3bcffd006f9fc84bbb0a54ef1676197 file
@@ -1 +1,6 @@
+<<<<<<< .our world +======= +hello +hello +>>>>>>> .their

Przy braku konfliktu:

merged
  result 100644 317e9677c3bcffd006f9fc84bbb0a54ef1676197 file
  our    100644 ce013625030ba8dba906f756967f9e9ca394464a file
@@ -1 +1,2 @@
 hello
+hello

Aby sprawdzić czy jest konflikt, wystarczy więc sprawdzić czy pierwszym słowem jest merged.

Skąd wziąć wspomniany wcześniej hasz bazy? Do tego służy polecenie git merge-base, które przyjmuje jako argumenty nazwy dwóch gałęzi.

Wiedząc to wszystko, łatwo napisać funkcję check, która sprawdzi czy HEAD ma konflikt z gałęzią podaną jej jako argument:

function check() {
  if [ `git rev-parse HEAD` == `git rev-parse "$1"` ]
  then
    # don't compare with the HEAD
    return
  fi
  base=`git merge-base HEAD "$1"`
  if [ "$base" == `git rev-parse "$1"` ]
  then
    # don't compare with the merge base
    return
  fi
  git merge-tree $base HEAD $1 \
    | head -1 | grep --quiet ^merged \
    || { echo conflict with $1; exit 1; }
}

git rev-parse zwraca nam po prostu hasz komitu, na który wskazuje gałąź.

A jak wykonać taką funkcję na wszystkich gałęziach? Można podejść naiwnie i kombinować z czymś takim:

git branch | while read branch; do check $branch; done

…ale to jest nieprawidłowe podejście. Z kilku powodów do skryptów powinniśmy używać bardziej niskopoziomowego polecenia: git for-each-ref. Oto jego użycie:

git for-each-ref --shell --format='check %(refname)' refs/heads \
  | while read expression; do eval "$expression"; done

Czyli przygotowujemy sobie listę komend do wykonania:

check f1
check f2
check master

i każdą z nich wykonujemy.

Oto cały skrypt: https://gist.github.com/pchmielowski/bd0e94a489e96ad011799f880f3610cb

Jak go używać?

Przede wszystkim ten skrypt pozwala nam sprawdzić czy będą jakieś potencjalne konflikty zanim zaczniemy łączyć gałęzie. Czasem trzeba będzie wynik jego działania zignorować, wiedząc, że w niedalekiej przyszłości ktoś po prostu będzie musiał wykonać brudną robotę i rozwiązać konflikt.

Często jednak zdarza się tak, że podczas pracy nad czymś, przy okazji, zmieniamy np. nazwę funkcji. Omawiany skrypt pomoże wcześnie pokazać, że ktoś inny pracował przy kilku miejscach, gzie ta funkcja jest używa. W takim przypadku często najlepszym rozwiązaniem jest po prostu cofnąć zmianę nazwy, oraz stworzyć w backlogu zadanie na refaktoring, które zostanie wykonane jak obie zmiany funkcjonalne będą połączone z główną gałęzią.