TEORIA

Czym jest “patch” - jest to zbiór zmian jakie można wprowadzić do pliku/struktury plików. O patchu wspomniałem w wpisie odnoście SEGGER Sysytem Viewer - aplikacja ta wymaga zmodyfikowania źródeł FreeRTOSa i te zmiany zapisane są w pliku patch - dzięki temu łatwo można je “zaaplikować” do swoich używanych plików.

Plik patch nie jest to tylko “głupie wypisanie w linii xx dodaj taki text” - każda pojedyncza zmiana to tzw. “hunk” - z dodatkowymi informacjami np. zawartością kolejnych/poprzednich linii, dzięki czemu można zpatchować pliki, które w delikatny sposób już zmodyfikowaliśmy wcześniej. Jeśli nie da się w łatwy sposób tego zrobić automatycznie - dostaniemy o tym informację i musimy sami przeprowadzić “merge” - dokładnie jak w przypadku “merge conflict” w przypadku pracy z GITem.

Dokumentacja odnośnie tego czym jest “hunk” i inne pojęcia związane z różnicami pomiędzy plikami itp: dokumentacja chunk.

Czyli patch/diff jest to zbiór wielu “hunków” w jednym pliku.

Przykłady użycia:

  • w kilku projektach używamy takiej samej biblioteki i znajdujemy w niej błąd - możemy poprawić go, a następnie wygenerować patcha zawierającego te zmiany i użyć go w innych projektach. Aczkolwiek osobiście uważam, że taka sytuacja nie powinna mieć miejsca - biblioteki lepiej trzymać jako coś typu submoduły - co ułatwi tego typu operacje.
  • producent udostępnia SDKa, znajduje błąd w bibliotece i poprawkę udostępnia też jako patch - dzięki temu firmy, które korzystają z tego SDKa, ale wprowadziły tam swoje modyfikacje mogą łatwiej naprawić ten błąd - z doświadczenia wiem, że często aktualizacje SDKów od producentów to dość bolesny proces - jeśli pojedyncze krytyczne zmiany są wprowadzane w formie małych patchy - aplikacja ich przechodzi dość płynnie.
  • dla STM32CubeIDE z generatorem - jeśli nasz projekt po wygenerowaniu wymaga dopisania kilku rzeczy do przegenerowanych plików - możemy je zebrać właśnie jako patch i aplikować po przegenerowaniu projektu. Taka sytuacja może powstać jeśli nie wszystkie “komponenty” HW są inicjalizowane przez CubeMX - tylko niektóre mają swoją konfigurację wpisaną “z palca” - CubeMX może wtedy nie wygenerować potrzebnych wektorów przerwań/callbacków.
  • zamiast dodawać “ręcznie” rzeczy związane z SSV do projektów CubeIDE - mam utworzony odpowiedni patch, który robi to za mnie.

PRAKTYKA

Bardzo prosty przykład z komendami - na bazie plików z biblioteki do I2C - https://gitlab.com/embedownik/i2c - bazuje tutaj na commicie d0514c752c185c2dea4203cc06e4ab9044fbe023 - aczkolwiek do pokazania idei i komend nie jest to ważne.

Załóżmy, że krytyczną poprawką jest zmiana w pliku I2C.c linia 123 - zmienna “size” na “size + 1U”.

Po wprowadzeniu takiej modyfikacji Git diff pokaże taki wynik:

$ git diff
diff --git a/I2C/src/I2C.c b/I2C/src/I2C.c
index 7f88852..111c6e1 100644
--- a/I2C/src/I2C.c
+++ b/I2C/src/I2C.c
@@ -120,7 +120,7 @@ bool I2C_writeBytes(uint8_t devAddress, uint8_t regAddress, const uint8_t *value
     /* because of lack of consts ST HAL library :/ */
 #pragma GCC diagnostic push
 #pragma GCC diagnostic ignored "-Wdiscarded-qualifiers"
-    HAL_StatusTypeDef comStatus = HAL_I2C_Mem_Write(I2CHandler, devAddress, regAddress, I2C_MEMADD_SIZE_8BIT, values, size, calculateTimeout(size));
+    HAL_StatusTypeDef comStatus = HAL_I2C_Mem_Write(I2CHandler, devAddress, regAddress, I2C_MEMADD_SIZE_8BIT, values, size + 1U, calculateTimeout(size));
 #pragma GCC diagnostic pop

     if(comStatus != HAL_OK)

Offtop - nawet domyślnie w Jekyllu jest dostępne kolorowanie składni dla typu “diff” - nie wiedziałem wcześniej, a nawet czytelnie to wygląda na blogu.

W outpucie komendy widzimy jaka linijka ma być usunięta, jaka dodana i w jakim “otoczeniu” się to dzieje. To co widzimy jest już w odpowiednim formacie pliku *.patch - więc aby go utworzyć wystarczy przekierować output komendy do pliku zwykłym:

git diff > naszPatch.patch

Wcześniej użyłem pojęcia “aplikowanie” patcha - kalka językowa z komendy GITa która do tego służy - ponieważ jest to:

git apply <file>

dla sprawdzenia możemy zrobić tak:

git diff > patch.patch
git stash
git apply patch.patch
git diff

W ten sposób pliku I2C.c została wprowadzona wcześniejsza zmiana (którą “skasowaliśmy” przez “git stash”).

Jeszcze jedna przydatna rzecz - wydobywanie zmian z określonego commita jako patch - służy do tego komenda:

git format-patch -1 <commit_sha>

z poprzedniego commita to komenda:

git format-patch -1 HEAD

Warto zaznaczyć, że jeśli poruszamy się w obrębie jednego repozytorium - lepszą opcją dla patcha z pojedynczego commita jest “cherry-pick”.

Tak możemy tworzyć patche z wielu commitów:

git diff <commit2_sha> <commit1_sha> > mypatch.patch

Istnieje też możliwość “negacji” patcha - czyli po jego zaaplikowaniu i poprawieniu czegoś w kodzie możemy skasować tylko zmiany z tego patcha. Służy do tego komenda:

git apply -R <patch>

I tutaj też przykład - kiedy nie możemy trzymać w źródłach projektu kodu SSV - patche umożliwiają jego łatwe dodanie i usunięcie z projektu.

Problemy z aplikacją patcha

Czasem gdy projekt się rozrośnie/wystąpi bardzo dużo zmian - wcześniej działający patch może nie zadziałać.

Jeśli zmiany są dość prymitywne - dotyczą spacji itp - można posłużyć się komendą:

git apply --ignore-space-change --ignore-whitespace mychanges.patch

W przypadku większych zmian można spróbować podejścia z 3way mergem - mechanizm jak przy merge confliktach - standard przy pracy z GITem:

git apply --3way patchFile.patch

Pliki z problemami pojawią się w git status jako “Unmerged paths”. Należy je ręcznie zmergować przed kolejnymi operacjami.

Niestety/stety mogą się zdarzyć takie przypadki kiedy automatyczna aplikacja patch będzie całkowicie niemożliwa z powodu zbyt wielu zmian.