728 x 90

‘git push -‌-force-with-lease’, bezpieczniejsza alternatywa do ‘git push -‌-force’

‘git push -‌-force-with-lease’, bezpieczniejsza alternatywa do ‘git push -‌-force’
Yoda używa git push --force

Kiedy chcemy opublikować zatwierdzone zmiany w zdalnym repozytorium za pomocą polecenia `git push`, aby stały się one dostępne dla innych, Git domyślnie wymaga aby można to było zrobić bez utraty danych. Innymi słowy wymaga stanu fast-forward. Czy istnieje bezpieczniejsza alternatywa dla wspomnianego polecenia?

W powyższym przypadku oznacza to, żeby w zdalnym repozytorium gałąź do której wysyłamy zmiany (gałąź master w repozytorium „origin” na poniższym obrazku) była przodkiem zmian które wysyłamy. Inaczej mówiąc, do zintegrowania nowych zmian z aktualnym stanem zdalnego repozytorium wymagane jest  przewinięcie wskaźnika gałęzi (właśnie taki stan nazywany jest w dokumentacji Git stanem „fast-forward”).

Graf przedstawiający sytuację przed wykonaniem `git push`

Sytuacja przed wykonaniem `git push`, nowe zmiany są potomkiem opublikowanych (tzw. fast-forward).

Aby uzyskać stan, przedstawiony na powyższym obrazku, należy zazwyczaj przed wykonaniem `git push zintegrować aktualny stan projektu, czy to za pomocą połączenia gałęzi (ang. merge) — na przykład za pomocą `git pull`, czy to przebazowując własne zmiany na szczycie aktualnego stanu projektu — na przykład za pomocą `git pull --rebase`. Prawdopodobieństwo, że w celu opublikowania własnych zmian trzeba będzie dokonać aktualizacji, rośnie wraz z aktywnością projektu.

Oczywiście problem związany z koniecznością dokonania aktualizacji przed wykonaniem `git push` mamy tylko wtedy, jeśli zdalne repozytorium, w którym publikowany jest aktualny stan projektu, jest współdzielone przez wielu deweloperów (tzw. scentralizowany sposób pracy (workflow)). Nie jest to jednak jedyny możliwy sposób pracy ze zdalnymi repozytoriami. Przy pracy z wyznaczoną osobą integrującą zmiany (opiekunem projektu), gdzie każdy deweloper wysyła zmiany do swojego własnego publicznego repozytorium, i dopiero wtedy zgłasza prośbę o ich integrację (ang. pull request), nie pojawi się ten problem. Po więcej informacji odsyłam na przykład do rozdziału 5.1 Rozproszony Git – Rozproszone przepływy pracy polskiego tłumaczenia pierwszego wydania książki „Pro Git” (dostępnej darmowo online na licencji Creative Commons).

Jeśli repozytorium zdalne znajduje się w stanie „fast-forward”, to będziemy mogli wykonać `git push` bez problemu.

$ git push
Counting objects: 48, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (6/6), 1469 bytes | 0 bytes/s, done.
Total 48 (delta 0), reused 0 (delta 0)
To https://git.company.com/repo.git
f30abad..3fb00bd master -> master

Po opublikowaniu naszych zmian (po wykonaniu poprzedniej operacji), sytuacja w obu repozytoriach będzie się przedstawiała następująco:

Graf przedstawiający sytuację po wykonaniu `git push`

Sytuacja po opublikowaniu zmian w zdalnym repozytorium za pomocą `git push`.

Gałąź zdalna origin/master, a właściwie gałąź śledząca zmiany w zdalnym repozytorium (ang. remote-tracking branch), jest automatycznie uaktualniana przez `git push` do stanu gałęzi master w zdalnym repozytorium origin.

Może jednak zdarzyć się, że wykryjemy błąd w naszym kodzie już po opublikowaniu zmian. Jeśli zmienilibyśmy historię, a właściwie ją przepisali, czy to za pomocą na przykład `git commit --amend`, czy `git rebase --interactive`, to nie będziemy już mogli opublikować zmian za pomocą `git push`.

$ git commit -a --amend
[...]
$ git push
To https://git.company.com/repo.git ! [rejected] master -> master (non-fast-forward)
error: failed to push some refs to 'https://git.company.com/repo.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g. 'git pull ...')
hint: before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

Zakładając, że zmiany zostały dokonane za pomoca `git commit --amend`, czyli zmiany ostatnio zatwierdzonej wersji, stan repozytoriów byłby następujący:

Graf przedstawiający sytuacje po wykonaniu `git commit --amend`

Sytuacja po `git commit --amend`, wykonanym już po `git push`. Nowe zmiany nie są potomkiem zmian opublikowanych (tzw. non fast-forward).

W takiej sytuacji, kiedy chcemy zastąpić opublikowane przez siebie zmiany (tak aby dostępną publicznie była wersja po wprowadzeniu poprawek), możemy nakazać Git nadpisać historię. Nadpisanie można wykonać wymuszając publikację zmian za pomocą `git push --force`. Jeśli nie jesteśmy jedyną osobą publikującą do danego repozytorium, nie jest to jednak bezpieczne. Może się zdarzyć, że w międzyczasie (pomiędzy wykonaniem `git push`, a wykonaniem `git commit --amend` a następnie `git push --force`) ktoś inny opublikował swoje zmiany. Używając `git push --force` nadpiszemy też te zmiany, a nie tylko swoje własne.

Bezpieczniejszą alternatywą jest użycie dostępnej od wersji 1.8.5 (opublikowanej 27 listopada 2013) opcji `git push --force-with-lease`. Sprawdza ona, czy rzeczywiście nikt nie zmienił stanu zdalnego repozytorium, czyli czy gałąź master w zdalnym repozytorium origin dalej wskazuje na tą samą wersję co zdalna gałąź origin/master w lokalnym repozytorium. Jeśli tak jest, to nadpisze ona własne zmiany.

$ git push --force-with-lease
Counting objects: 3, done.
Writing objects: 100% (3/3), 269 bytes | 0 bytes/s, done.
Total 3 (delta 0), reused 0 (delta 0)
To https://git.company.com/repo.git
+ 3fb00bd...8d110bd master -> master (forced update)

Po wykonaniu tego polecenia stan repozytoriów będzie się przedstawiał następująco:

Graf przedstawiający sytuacje po wykonaniu `git push --force-with-lease`

Stan repozytoriów po wykonaniu `git push --force-with-lease`; poprzednio opublikowane zmiany zostały nadpisane.

Jeśli w międzyczasie ktoś inny opublikowałby swoje zmiany (powodując że origin/master oraz gałąź master w repozytorium origin miałyby różne wartości), to operacja nie powiedzie się, zapobiegając utracie danych. Nie nadpiszemy zmian wprowadzonych przez innego dewelopera.

$ git push
To https://git.company.com/repo.git ! [rejected] master -> master (fetch first)
error: failed to push some refs to 'https://git.company.com/repo.git'
hint: Updates were rejected because the remote contains work that you do hint: not have locally. This is usually caused by another repository pushing
hint: to the same ref. You may want to first integrate the remote changes
hint: (e.g., 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
 
$ git push --force-with-lease
To https://git.company.com/repo.git ! [rejected] master -> master (stale info)
error: failed to push some refs to 'https://git.company.com/repo.git'

W tej samej sytuacji `git push --force` nadpisał by zmiany.

Uwagi końcowe

Użycie `git push --force-with-lease` nie jest jednak uniwersalnym rozwiązaniem. Należy po nie sięgać w wyjątkowych przypadkach.

Po pierwsze, chociaż użycie tej opcji zapobiegnie nadpisaniu zmian opublikowanych przez innych, nie zapobiegnie (i nie może zapobiec) sytuacji, w której inna osoba zaczęła pracę bazując na poprzedniej, starej wersji zmian. Nie jest trudno popełnić błąd, i opublikować zmiany w taki sposób, że w historii projektu jeden commit pojawia się w dwu wersjach: z poprawkami i sprzed poprawek. Aby zminimalizować ryzyko, operację nadpisywania zmian warto ograniczyć tylko jeśli poprawka jest wysłana w krótkim przedziale czasu.

Po drugie, najprostsza w użyciu, pokazana tutaj bezargumentowa wersja opcji `--force-with-lease` używa do sprawdzenia bezpieczeństwa wartości gałęzi zdalnej. Użycie tej formy nie jest zatem bezpieczne, gdy w tle wykonywany jest `git fetch` (na przykład przez IDE lub edytor programistyczny, które wykonują go, aby zapewnić, że stan wskazywany przez gałęzie zdalne jest zawsze aktualny). Można sprawdzić, czy taka sytuacja zachodzi za pomocą na przykład `git reflog origin/master` — kolejne położenia origin/master w lokalnym repozytorium opisywane są odpowiednio jako “update by push“, lub zaczynają się od “fetch:” lub “push:“. W takiej sytuacji nadal możemy użyć bezpieczniejszej formy `--force`, ale trzeba podać argumenty: stan, który chcemy opublikować i oczekiwany stan zdalnego repozytorium `--force-with-lease=<gałąź>:<oczekiwany stan>`.

Należy także pamiętać, że `git push -f` (skrótowa wersja opcji) oznacza `git push --force`, a więc mniej bezpieczną wersję (niestety nie jest to `git push --force-with-lease`. Jeśli tab-completion (uzupełnianie komend, argumentów i opcji przez powłokę poleceń) nie wystarczy, to zawsze można użyć aliasów.  Przykładowo jeśli byśmy chcieli by `git pushf` wykonywał `git push --force-with-lease`, wystarczy na przykład wykonać `git config --global alias.pushf "push --force-with-lease"`. Użyta tu opcja `--global`  powoduje zapisanie zmian do pliku konfiguracyjnego użytkownika, tak by alias był dostępny we wszystkich repozytoriach.

Źródło obrazów: opracowanie własne autora

Leave a Comment

Your email address will not be published. Required fields are marked with *

Cancel reply

Inne artykuły