Mógłbym rzec, że git commit to jedna z najbardziej niepozornych komend w git. A przecież z tej krótkiej wiadomości można się dużo dowiedzieć – między innymi tego, co autor miał na myśli.
Niestety realia są, jakie są, i w większości projektów ta wiadomość nic nie mówi. Czasami odnoszę wrażenie, że ludzie muszą tam wpisać cokolwiek, bo to jedyny wymóg, żeby opublikować kawałek swojej pracy. W niektórych projektach jedynym wymogiem jest numer ticketa. Commity o treści refactor, added XXX czy cr fixes to smutna i nudna rzeczywistość.
A gdyby dało się czytać historię zmian w kodzie z samej historii commitów, bez zaglądania w szczegóły? Gdyby trzeba było zrobić revert na produkcji – czy wiedziałbyś od razu, który commit wycofać? Na to jest prosty standard. Cytując znaną scenę z polskiego kultowego filmu: i teraz wchodzę ja, cały na biało.
W skrócie
- Format:
typ(zakres)!: opis, potem opcjonalnie pusta linia i treść, a na końcu stopki (np.Refs: #123). - Specyfikacja definiuje tylko
featifix; pozostałe typy (docs,chore,refactor…) pochodzą z konwencji Angulara i@commitlint/config-conventional. fix= wersja PATCH,feat= MINOR,!lubBREAKING CHANGE= MAJOR (SemVer).- Pilnowanie formatu: commitlint + husky (hook
commit-msg), wygodne pisanie: commitizen, automatyczne wydania: semantic-release albo release-please. - Przy „Squash and merge” na GitHubie liczy się tytuł pull requesta – to on zostaje commitem na głównej gałęzi.
Conventional Commits – format wiadomości commita
Pełny format według specyfikacji z conventionalcommits.org:
typ(zakres)!: opis
[opcjonalna treść]
[opcjonalne stopki]Obowiązkowe są tylko typ, dwukropek ze spacją i opis. Reszta jest opcjonalna. Przykład z każdym elementem:
feat(api)!: remove v1 endpoints
Endpoints /v1/* were deprecated for a year. Clients should
switch to /v2/*, which returns the same data in a paginated form.
BREAKING CHANGE: /v1/* now returns 410 Gone
Refs: #482type – rodzaj zmiany
Typ, jak sama nazwa wskazuje, określa rodzaj zmiany w kodzie: czy to nowa funkcjonalność, a może poprawka. Sama specyfikacja wymaga tylko dwóch typów: feat (nowa funkcja) i fix (poprawka błędu). Inne są dozwolone, a najpopularniejszy zestaw to ten z konwencji Angulara, używany przez @commitlint/config-conventional – lista jest niżej.
scope – zakres zmiany
Zakres podaje się w nawiasie zaraz po typie i jest opcjonalny. Specyfikacja mówi, że ma to być rzeczownik określający miejsce w kodzie, np. feat(parser):, fix(form):.
Z doświadczenia wiem (liczba i złożoność projektów), że nie zawsze jest to idealne rozwiązanie. Ta „sugestia” sprawdza się bardzo dobrze w małych projektach, gdzie funkcjonalność nie jest skomplikowana. Problem pojawia się wtedy, gdy nad projektem pracuje dużo osób – wtedy określenie zakresu robi się problematyczne.
Dlatego w zespołach, w których pracuję, używamy numeru ticketa jako zakresu, np. fix(ED-439): handle empty cart. Dodatkowa zaleta: jeśli korzystasz z systemu takiego jak Jira, taki numer jest automatycznie linkowany, co bardzo się przydaje przy generowaniu CHANGELOG-u. Domyślna konfiguracja commitlinta taki zakres przepuszcza.
description – opis
Najistotniejsza część. Trzeba się naprawdę postarać, żeby napisać dobry opis – na szczęście tylko na początku. Im lepiej (epicko) opiszesz swoje zmiany, tym lepiej dla wszystkich.
Najlepszy przykład, z jakim sam się spotkasz, to commity o wdzięcznej treści cr fixes. Ile razy to widzisz? Wiesz, co kryje się za tym magicznym zwrotem? A gdyby napisać, że zmieniłeś sposób wywołania funkcji, poprawiłeś literówkę w nazwie zmiennej albo usunąłeś wywołania razem z definicją jakiejś metody? Czy takie wiadomości nie są czytelniejsze?
Dobry opis:
- zwięźle mówi, co się zmieniło (dlaczego – to miejsce na treść commita),
- jest w trybie rozkazującym: nie
added polish translationsaniadding polish translations, tylkoadd polish translations– to zwyczaj z git i Angulara, sama specyfikacja tego nie wymaga, - zaczyna się małą literą i nie kończy kropką – tego pilnuje już commitlint w domyślnej konfiguracji (podobnie jak długości nagłówka do 100 znaków).
body i footer – treść i stopki
Treść (body) zaczyna się po jednej pustej linii pod nagłówkiem i jest dowolnym tekstem – tu wyjaśniasz kontekst i powód zmiany. Stopki (footer) idą po kolejnej pustej linii, w formacie podobnym do git trailers: Token: wartość albo Token #wartość, np. Refs: #123, Reviewed-by: Anna. W nazwie tokena zamiast spacji używa się myślnika – z jednym wyjątkiem, którym jest BREAKING CHANGE.
BREAKING CHANGE i wykrzyknik
Niektóre zmiany są łamiące, np. zmiana w API, po której stary kod klienta przestaje działać. Można je oznaczyć na dwa sposoby (albo oba naraz):
!tuż przed dwukropkiem:feat!: drop support for Node 18albofeat(api)!: …,- stopką
BREAKING CHANGE: opis– pisaną zawsze wielkimi literami (dopuszczalny jest też zapisBREAKING-CHANGE).
feat: allow provided config object to extend other configs
BREAKING CHANGE: `extends` key in config file is now used for extending other config filesPoza BREAKING CHANGE wielkość liter w elementach formatu nie ma według specyfikacji znaczenia – w praktyce jednak wszyscy piszą typy małymi literami, a commitlint tego wymaga.
Typy commitów – ściąga (feat, fix, chore i reszta)
Typy z @commitlint/config-conventional. Uwaga na liczbę pojedynczą: test i style, nie tests i styles – commitlint odrzuci wersje z „s”.
| Typ | Kiedy używać |
|---|---|
feat |
nowa funkcjonalność |
fix |
poprawka błędu |
docs |
zmiany tylko w dokumentacji |
style |
formatowanie, białe znaki, przecinki, brakujące średniki – bez zmiany działania kodu |
refactor |
zmiana kodu, która nie jest ani poprawką, ani nową funkcją |
perf |
poprawa wydajności |
test |
dodanie brakujących albo poprawa istniejących testów |
build |
system budowania i zależności zewnętrzne (np. npm, webpack) |
ci |
konfiguracja i skrypty CI (np. GitHub Actions) |
chore |
pozostałe prace porządkowe, które nie ruszają kodu źródłowego ani testów |
revert |
wycofanie wcześniejszego commita |
Co oznacza chore w git?
chore to „prace domowe” w repozytorium: aktualizacja narzędzi, porządki w konfiguracji, zmiany w .gitignore – wszystko, co nie zmienia działania aplikacji ani testów. Aktualizacje zależności według config-conventional pasują do build, ale w praktyce często widać chore(deps): (zależności deweloperskie) i fix(deps): (zależności produkcyjne) – tak oznaczają je boty aktualizujące paczki.
Revert w Conventional Commits
Specyfikacja nie narzuca formatu revertów, ale w FAQ poleca typ revert i stopkę z SHA wycofywanych commitów:
revert: let us never again speak of the noodle incident
Refs: 676104e, a215868Domyślna wiadomość z git revert (Revert "feat: …") też przejdzie przez commitlint – jest na liście wiadomości, które pomija, razem z commitami typu Merge branch … oraz fixup! i squash!.
Przykłady commitów – dobre i złe
Prawdziwa historia zgodna z Conventional Commits wygląda tak:
docs: add short circuit to hook example (#665)
docs: fix typo in code inside README.md (#662)
fix(deps): update dependency lodash to v4.17.15 (#652)
chore: update mocha, other dev deps (#653)
fix(node): remove node 6 and 8 support (#649)
fix(cli): allow argv to be overridden in bootstrap (#621)
feat(cli): implement --hook option for git hooks integration (#615)
test(ED-439): add tests for Modal component
style: format code with new prettier configA tak wyglądają typowe błędy (każdy z nich odrzuca commitlint z domyślną konfiguracją):
| Źle | Dlaczego | Lepiej |
|---|---|---|
cr fixes |
brak typu, opis nic nie mówi | fix(cart): rename total to totalGross |
Fix: Typo |
typ i opis wielką literą | fix: correct typo in login error message |
tests: add modal tests |
nie ma typu tests |
test: add modal tests |
feat: add login page. |
kropka na końcu | feat: add login page |
Język opisu nie jest określony – feat: dodaj logowanie przez Google jest poprawne. W projektach open source i w międzynarodowych zespołach standardem jest jednak angielski.
Conventional Commits a SemVer
Typy przekładają się bezpośrednio na wersjonowanie semantyczne (MAJOR.MINOR.PATCH). Dla wersji 1.4.2:
- same
fix→ 1.4.3 (PATCH), - co najmniej jeden
feat→ 1.5.0 (MINOR), - cokolwiek z
!alboBREAKING CHANGE→ 2.0.0 (MAJOR), niezależnie od typu.
docs, chore, style i reszta same w sobie nowej wersji nie wymuszają. Właśnie dlatego narzędzia potrafią wyliczyć numer wersji, założyć tag w git i wygenerować listę zmian bez udziału człowieka.
commitlint i husky – automatyczne sprawdzanie commitów
commitlint sprawdza, czy wiadomość jest zgodna z konwencją, a husky podpina go pod hook commit-msg, więc zła wiadomość w ogóle nie trafi do repozytorium. Konfiguracja dla husky v9 (sprawdzona na commitlint 21 i Node 24):
npm install --save-dev @commitlint/cli @commitlint/config-conventional husky
echo "export default { extends: ['@commitlint/config-conventional'] };" > commitlint.config.mjs
npx husky init
echo "npx --no -- commitlint --edit \$1" > .husky/commit-msgDwie rzeczy, na które warto uważać:
- Dokumentacja commitlinta podaje plik
commitlint.config.js. Nowy npm zapisuje jednak wpackage.json"type": "commonjs"i wtedyexport defaultkończy się błędemSyntaxError: Unexpected token 'export'. Rozszerzenie.mjsdziała niezależnie od tego ustawienia. npx husky initdodaje wpackage.jsonskrypt"prepare": "husky"i tworzy też hook.husky/pre-commitznpm test. Jeśli projekt nie ma testów, każdy commit się wywali – usuń ten plik albo wpisz w nim swoją komendę.
Od teraz commit git commit -m "cr fixes" zostanie odrzucony:
✖ subject may not be empty [subject-empty]
✖ type may not be empty [type-empty]
✖ found 2 problems, 0 warningsPojedynczą wiadomość możesz przetestować bez commitowania, a w CI – sprawdzić zakres commitów:
echo "fix(parser): handle empty input" | npx commitlint
npx commitlint --from HEAD~1 --to HEAD --verboseU mnie commit jest częścią code review. W erze agentów AI, każdy projekt ma krótką instrukcję o w tym jak ma tworzyć commity. Przed push uruchamiane są hooki, które sprawdzają kod i wiadomości commit.
commitizen – commit z kreatora
commitizen zamiast pustego edytora zadaje pytania w konsoli: typ, zakres, opis, czy to zmiana łamiąca, które zgłoszenie zamyka. Z gotowych odpowiedzi składa wiadomość zgodną z konwencją. Instalacja w projekcie (ostatnia linia to już samo commitowanie, zamiast git commit):
npm install --save-dev commitizen
npx commitizen init cz-conventional-changelog --save-dev --save-exact
npx czDobry wybór na start albo dla osób, które rzadko commitują i nie pamiętają typów.
semantic-release i release-please – wydania z commitów
Tu widać największy zysk z porządnych commitów:
- semantic-release działa w CI po każdym merge’u do głównej gałęzi: analizuje commity od ostatniego wydania, wylicza wersję, tworzy tag, notatki do wydania i publikuje paczkę (np. w npm). Zero ręcznych kroków.
- release-please (Google) zamiast publikować od razu, utrzymuje „release PR” z podbitą wersją i CHANGELOG-iem. Wydanie i tag powstają dopiero, gdy zmergujesz ten PR – masz więc kontrolę nad tym, kiedy wychodzi nowa wersja.
Do samego CHANGELOG-u, bez publikacji, wystarczy conventional-changelog.
Conventional Commits na GitHubie a „Squash and merge”
Jeśli zespół merguje pull requesty przez Squash and merge, wszystkie commity z gałęzi zamieniają się w jeden (więcej o tym, jak to działa, we wpisie o squashu commitów). Na głównej gałęzi – i w narzędziach od wydań – zostaje więc tylko wiadomość tego jednego commita. W praktyce:
- Tytuł PR musi być w formacie Conventional Commits, np.
feat(auth): add Google sign-in. GitHub dopisuje do niego numer PR, np.(#123). - W ustawieniach repozytorium (Settings → General → Pull Requests → Allow squash merging) warto wybrać domyślną wiadomość Pull request title. Przy ustawieniu Default PR z jednym commitem dostaje wiadomość tego commita, a dopiero PR z kilkoma commitami – tytuł PR, co łatwo przeoczyć.
- Format tytułów PR można sprawdzać w GitHub Actions, np. akcją action-semantic-pull-request.
Specyfikacja zresztą wprost to dopuszcza: przy squashu osoby prowadzące projekt mogą poprawić wiadomości przy merge’u, więc pojedyncze commity na gałęzi nie muszą być idealne.
Po co to wszystko?
- Conventional Commits wprowadza porządek w historii commitów.
- Wbrew pozorom poprawia komunikację w projekcie (zespole).
- Pozwala automatycznie wygenerować CHANGELOG (historię zmian).
- Poprawność wiadomości da się sprawdzać automatycznie – w hooku
commit-msgalbo w CI. - Łatwo określić, czy zmiany trafią do wersji major, minor czy patch.
W Conventional Commits jest jeden problem – Ty!
Oczywiście jest jeden problem w tym wszystkim: czynnik ludzki. Developerzy przede wszystkim muszą chcieć pisać lepsze opisy i rozumieć, jakiego rodzaju zmiany wprowadzili w kodzie. Przed każdym commitem trzeba się więc dodatkowo „wysilić”. Dobra wiadomość jest taka, że szybko to wchodzi w krew – a commitlint przypomni, gdy zapomnisz. Jeśli chcesz dopasować do tego także szablon wiadomości czy edytor, zajrzyj do wpisu o konfiguracji git config.
Najczęstsze pytania
01Co to jest Conventional Commits?+
02Czym różnią się feat, fix i chore?+
03Jak oznaczyć zmianę łamiącą (breaking change)?+
04Jak zrobić revert zgodnie z Conventional Commits?+
05Czy commity można pisać po polsku?+
06Jak przetestować wiadomość commita bez commitowania?+
Przydatne linki
- Conventional Commits 1.0.0 – oficjalna specyfikacja (jest też po polsku)
- commitlint – sprawdzanie poprawności wiadomości commitów
- husky – git hooki w projektach Node.js
- commitizen – tworzenie commitów przez prompty w konsoli
- semantic-release i release-please – automatyczne wydania i CHANGELOG
- Conventional commit for PHP – biblioteka znajomego do sprawdzania wiadomości commitów w PHP