Agile git Workflow

Matthew @ 2010-03-18 — Kategorie: Agile, git, Programowanie, Techblog

Dzisiaj kolejny odcinek “Wszyscy mają gita, mam i ja!”. Jednak tym razem nie kolejny rozdział Pro Git, ale też tłumaczenie. JJ Geewax był tak uprzejmy i udostępnił do tłumaczenia swoje spostrzeżenia nt. pracy z gitem w połączeniu z metodykami zwinnymi (agile). Oryginał można znaleźć tutaj: Agile git Workflow. Oczywiście jak w przypadku każdego mojego wpisu, mile widziane są zgłoszenia błędów/poprawek (czy to w komentarzach, gg, jabberze lub e-mailem). Miłego czytania.

Kiedy zaczynamy używać gita do zarządzania naszym kodem źródłowym w pracy, w zasadzie wskakujemy w to trochę za szybko. Dużo napisano o tym jak możesz zrobić dużo porządnych rzeczy z gitem, ale nie ma prawdziwego przewodnika o jednym szczególnym sposobie używani gita w Twoim projekcie. Ten post ma za zadanie opisać, jak możesz codziennie używać gita w większym projekcie typu “agile-style”.

Przegląd

Generalny przegląd jak to działa:

1. Stworzyć podgałąź z mastera do pracy nad funkcjonalnością
2. Praca, praca, praca, commit, commit, commit
3. Przenoszenie zmian do mastera
4. Rebase i squash gałęzi funkcjonalności do HEAD głównej gałęzi
5. Dołączenie gałęzi funkcjonalności do głównej gałęzi
6. Wypchanie wszystkiego do współdzielonego repozytorium

Wyszczególnimy również jak używać gałęzi release-candidate oraz produkcyjnej i dołączanie feature’ów do tych gałęzi.

Praca nad funkcjonalnością

Wyobraźmy sobie, że musisz popracować nad Issue #12. Pierwszą rzeczą którą robisz jest

stworzenie podgałęzi funkcjonalności, od gałęzi głównej, dla tego problemu. Wyizoluje to Twoją pracę, dzięki czemu będziesz mógł przełączyć się między tym nad czym pracujesz i tym co szybko potrzeba naprawić.

[cc lang="bash"](master)jj@im-jj:~/demo$ git checkout -b issue-12
Switched to a new branch ‘issue-12′
(issue-12)jj@im-jj:~/demo$[/cc]

Teraz jesteś w gałęzi konkretnej funkcjonalności, możesz chcieć zrobić całą pracę aby rozwiązać problem:

[cc lang="bash"](issue-12)jj@im-jj:~/demo$ pico feature.py
(issue-12)jj@im-jj:~/demo$ git add feature.py
(issue-12)jj@im-jj:~/demo$ git commit -m “Added feature”
[issue-12 765a6c0] Added feature
1 files changed, 1 insertions(+), 0 deletions(-)
create mode 100644 feature.py
(issue-12)jj@im-jj:~/demo$ git rm README.txt
rm ‘README.txt’
(issue-12)jj@im-jj:~/demo$ git commit -m “Removed the README file”
[issue-12 e7829cb] Removed the README file
0 files changed, 0 insertions(+), 0 deletions(-)
delete mode 100644 README.txt
(issue-12)jj@im-jj:~/demo$ pico testfile.txt
(issue-12)jj@im-jj:~/demo$ git add testfile.txt
(issue-12)jj@im-jj:~/demo$ git commit -m “Added testfile.txt”
[issue-12 f9753df] Added testfile.txt
1 files changed, 1 insertions(+), 0 deletions(-)
create mode 100644 testfile.txt
(issue-12)jj@im-jj:~/demo$ pico feature.py
(issue-12)jj@im-jj:~/demo$ git add feature.py
(issue-12)jj@im-jj:~/demo$ git commit -m “Updated feature”
[issue-12 4e39fc7] Updated feature
1 files changed, 3 insertions(+), 1 deletions(-)[/cc]

Zdobywanie feedbacku Twojego feature’u

Jeżeli chcesz współdzielić całą Twoją pracę z kimś innym poprzez patche (na potrzeby code review lub czegoś innego), jest to bardzo proste:

[cc lang="bash"](issue-12)jj@im-jj:~/demo$ git diff master[/cc]

Znaczy to tyle co “porównaj co jest w gałęzi w której teraz jestem, z tym co jest w gałęzi głównej i wypisz na standardowe wyjście”.

Jeżeli chcesz możesz przekierować wyjście tego polecenia do pliku (np. [cc inline="true" no_cc="true"]git diff master > issue12.diff[/cc]).

Pakowanie Twojej pracy dla współdzielonego repozytorium

Kiedy jesteś gotowy podzielić się tym feature’m z resztą zespołu, prawdopodobnie, nie będzie ich interesowało wszystkie Twoje pośrednie commity (np. dodałeś a potem zmodyfikowałem plik feature.py). Możemy spakować wszystkie commity w jeden duży commit (coś jak jeden duży patch względem mastera którego używałeś wcześniej) poprzez użycie polecenie [cc inline="true" no_cc="true"]git rebase[/cc].

[cc lang="bash"](issue-12)jj@im-jj:~/demo$ git rebase -i master[/cc]

Flaga [cc inline="true" no_cc="true"]-i[/cc] jest “interaktywna”, pozwalająca na ustalenie które commity mają być zawarte przez gita. Powinien Ci się pojawić edytor tekstu z zawartością wyglądającą mniej więcej tak:

[cc lang="bash"]pick 765a6c0 Added feature
pick e7829cb Removed the README file
pick f9753df Added testfile.txt
pick 4e39fc7 Updated feature

# Rebase 309291d..4e39fc7 onto 309291d
#
# Commands:
# p, pick = use commit
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
#
# If you remove a line here THAT COMMIT WILL BE LOST.
# However, if you remove everything, the rebase will be aborted.
#[/cc]

Żeby spakować wszystkie te commity w jeden commit, zmień wszystkie oprócz jednego na górze, na s lub squash. Powie to “zachowaj zmiany z commitu, ale zwiń je do rodzica commitu. Domyślnie, możesz zobaczyć, że “pick” jest tym, który zostawia commity podzielone. Jeżeli z jakiegoś powodu chcesz “cofnąć” commit, którego już masz, możesz usunąć całą linię. Spowoduje to całkowite usunięcie zmian wprowadzonych w tym konkretnym commitcie.

Aby “zapakować” gałęzi w pojedyńcz commit, nasz edytor powinien wyglądać tak:

[cc lang="bash"]pick 765a6c0 Added feature
s e7829cb Removed the README file
s f9753df Added testfile.txt
s 4e39fc7 Updated feature

# Rebase 309291d..4e39fc7 onto 309291d
#
# Commands:
# p, pick = use commit
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
#
# If you remove a line here THAT COMMIT WILL BE LOST.
# However, if you remove everything, the rebase will be aborted.
#[/cc]

Jeżeli już zapiszesz ten plik zostaniesz poproszony o wiadomość która zostanie dołączona do przepakowanego commita. Zazwyczaj warto napisać coś o funkcjonalności. Prośba ktorą ja dostałem:

[cc lang="bash"]# This is a combination of 4 commits.
# The first commit’s message is:
Added feature

# This is the 2nd commit message:

Removed the README file

# This is the 3rd commit message:

Added testfile.txt

# This is the 4th commit message:

Updated feature

# Please enter the commit message for your changes. Lines starting
# with ‘#’ will be ignored, and an empty message aborts the commit.
# Not currently on any branch.
# Changes to be committed:
# (use “git reset HEAD …” to unstage)
#
# deleted: README.txt
# new file: feature.py
# new file: testfile.txt
#[/cc]

Który mógłbyś zmienić na:

[cc lang="bash"]Issue 12 – Added new feature XYZ

# Please enter the commit message for your changes. Lines starting
# with ‘#’ will be ignored, and an empty message aborts the commit.
# Not currently on any branch.
# Changes to be committed:
# (use “git reset HEAD …” to unstage)
#
# deleted: README.txt
# new file: feature.py
# new file: testfile.txt
#[/cc]

Trzymanie współdzielonego repozytorium liniowo

W tym momencie gałąź “issue-12″ zawiera jeden commit. Jakkolwiek, jeżeli ktoś inny wypchnął jakieś zmiany do współdzielonego repozytorium, dołączając gałąź funkcjonalności będzie miał dwa commity: jeden z feature’m i kolejny z połączonym feature’m z czymkolwiek innym co zostało wypchnięte przez resztę zespołu.

Wszystkie te commity będące “łączeniami moich lokalnych gałęzi” będą wyglądały jak zbędny szum, co nie wygląda dobrze. Dlatego git pozwala Ci “odtworzyć” Twoje commity na głównym repozytorium, co jest celem “przepakowania” commita, żeby wyglądał jakbyś zrobił gałąź z najbardziej aktualnej wersji kodu.

Czasami nazywane jest to jako “zmiana historii”, ponieważ aktualizujesz rodzica Twojego aktualnego commita, jakby był przesunięty bardziej w przyszłości, niż faktycznie miało to miejsce.

Żeby zilustrować jak to działa, możesz zrobić mały commit do głównej gałęzi, a następne użyć rebase żeby odtworzyć commit poprzedzający zaktualizowane główne repozytorium.

[cc lang="bash"](issue-12)jj@im-jj:~/demo$ git checkout master
Switched to branch ‘master’
(master)jj@im-jj:~/demo$ pico fixed_typo.txt
(master)jj@im-jj:~/demo$ git add fixed_typo.txt
(master)jj@im-jj:~/demo$ git commit -m “Added fixed typo”
[master c348893] Added fixed typo
1 files changed, 1 insertions(+), 0 deletions(-)
create mode 100644 fixed_typo.txt
(master)jj@im-jj:~/demo$[/cc]

Teraz master ma jednego dodatkowego commita i issue-12 też ma jednego dodatkowego commita, gdzie każdy ma takiego samego rodzica. Jeżeli dołączymy issue-12 do mastera będziemy mieli dwa commity, o których wspomnieliśmy wcześniej (jeden dla przepakowanego feature’a jako jeden commit, a kolejny to rekursyjny merge). Żeby pozbyć się drugiego commita, który nie wnosi zbyt wiele informacji dla zespołu, zrobimy rebase commita, tak, żeby wyglądało jakby stał się po commitcie, którego przed chwilą wrzuciliśmy do mastera. To również da nam szansę aby rozwiązać ew. konflikty.

Ten obrazek pokazuje stan sprzed zmiany rodzica:

git przed rebase

[cc lang="bash"](master)jj@im-jj:~/demo$ git checkout issue-12
Switched to branch ‘issue-12′
(issue-12)jj@im-jj:~/demo$ git rebase master
First, rewinding head to replay your work on top of it…
Applying: Issue 12 – Added new feature XYZ[/cc]

Ten obrazek pokazuje stan po rebase zmienionego rodzica:

git po rebase

Teraz możemy zmerge’ować zmiany z powrotem do mastera dzięki fast forward:

[cc lang="bash"](issue-12)jj@im-jj:~/demo$ git checkout master
Switched to branch ‘master’
Your branch is ahead of ‘origin/master’ by 1 commit. # < -- Always be 1 commit ahead.
(master)jj@im-jj:~/demo$ git merge issue-12
Updating c348893..d0e9912
Fast forward
feature.py | 3 +++
testfile.txt | 1 +
2 files changed, 4 insertions(+), 0 deletions(-)
delete mode 100644 README.txt
create mode 100644 feature.py
create mode 100644 testfile.txt[/cc]

Sprawdźmy dwa razy czy nadal możemy zrobić fast-forward na współdzielonym repozytorium:
[cc lang="bash"](master)jj@im-jj:~/demo$ git status
# On branch master
# Your branch is ahead of ‘origin/master’ by 2 commits. # < -- 1 for issue-12, 1 for the typo-fix.
#
nothing to commit (working directory clean)
(master)jj@im-jj:~/demo$ git remote show origin
* remote origin
URL: my-server:/git/demo.git
HEAD branch: master
Remote branch:
master tracked
Local branch configured for 'git pull':
master merges with remote master
Local ref configured for 'git push':
master pushes to master (fast forwardable) # <-- Always be able to fast-forward.[/cc]

Dopóki możemy robić fast forward, po prostu uruchamiamy [cc no_cc="true" inline="true"]git push[/cc]:
[cc lang="bash"](master)jj@im-jj:~/demo$ git push origin
Counting objects: 8, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (5/5), done.
Writing objects: 100% (7/7), 699 bytes, done.
Total 7 (delta 0), reused 0 (delta 0)
To my-server:/git/demo.git
309291d..d0e9912 HEAD -> master[/cc]

I gdy już nie będziemy potrzebowali gałęzi issue-12, możemy ją usunąć:

[cc lang="bash"](master)jj@im-jj:~/demo$ git branch -d issue-12
Deleted branch issue-12 (was d0e9912).[/cc]

Co, jeżeli inni wypchnęli kod w międzyczasie?

Jeżeli ktoś inny zdecyduje się wypchnąć swój kod w międzyczasie Twojego rebase i [cc inline="true" no_cc="true"]git remote show origin[/cc] (będziesz o tym wiedział, ponieważ push nie pozwoli Ci na fast forward) powinieneś najpierw ściągnąć wszystkie zmiany, a następnie zrobić kolejny rebase. To wszystko prowadzi do (ponownego) powtórzenia Twoich commitów nad tym nowym, który został wypchnięty do współdzielonego repozytorium, więc nie powinno sprawdzić dużo zamieszania przy łączeniu Twojej gałęzi danej funkcjonalności. Odkąd masz gotowy spakowany commit, jeżeli chcesz, możesz pominąć [cc inline="true" no_cc="true"]-i[/cc] i po prostu zrobić:

[cc lang="bash"](issue-12)jj@im-jj:~/demo$ git rebase master
First, rewinding head to replay your work on top of it…
Applying: Issue 12 – Added new feature XYZ[/cc]

Morał z tej historii jest taki, że jeżeli chcesz trzymać swoje współdzielone repozytorium tylko z ważnymi rzeczami, miej pewność, że wszystkie Twoje pushe są fast-forward. Jest to łatwe jak pamiętanie, żeby za każdym razem zrobić rebase bezpośrednio przed pushem.

Przechowalnia i produkcja

Teraz masz pojęcie o gałęziach poszczególnych funkcjonalności. Kolejną rzeczą z którą się zmierzymy jest to jak wykorzystać gałęzie do oznaczania różnych wersji naszego kodu.

Idealnie by było jakbyśmy mieli “trunk” (aka master) a następnie gałąź z ostatnim release-candidate (powiedzmy “rc-1.0″) a potem gałąź oficjalnego wydania (powiedzmy “1.0″).

Podstawową ideą jest traktowanie gałęzi release-candidate tak jak głównej (przepakowywanie commitów, gałęzie funkcjonalności, itp) a gałęzi produkcyjnych jako gałęzi “merge-only” lub “cherry-pick-only”. Niektórzy ludzie preferują używanie tagów do tego, jednak używanie tagów oznacza, że cokolwiek teraz jest testowe będzie przeniesione do produkcyjnej części. Jest to wybór, którego musisz dokonać. Gałęzie dają Ci trochę więcej niezależności niż RC na tagach.

Tworzenie gałęzi

Pierwszą rzeczą którą powinno się zrobić jest stworzenie gałęzi RC:

[cc lang="bash"](master)jj@im-jj:~/demo$ git checkout -b rc-1.0
Switched to a new branch ‘rc-1.0′
(rc-1.0)jj@im-jj:~/demo$ git push origin rc-1.0
Total 0 (delta 0), reused 0 (delta 0)
To my-server:/git/demo.git
* [new branch] rc-1.0 -> rc-1.0
(rc-1.0)jj@im-jj:~/demo$ git remote show origin
* remote origin
URL: my-server:/git/demo.git
HEAD branch (remote HEAD is ambiguous, may be one of the following):
master
rc-1.0
Remote branches:
master tracked
rc-1.0 tracked
Local branch configured for ‘git pull’:
master merges with remote master
Local refs configured for ‘git push’:
master pushes to master (up to date)
rc-1.0 pushes to rc-1.0 (up to date)[/cc]

Teraz masz nową gałąź release candidate do której możesz pushować zmiany. Przeprowadź tą samą procedurę opisaną powyżej, żeby mieć pewność, że wszystko pozostało liniowe i działa dobrze.

Tak powinna wyglądać Twoja gałąź RC:

git rc branch

Co jeżeli ktoś inny już stworzył taką gałąź?

Może już ktoś inny z Twojego zespołu stworzył taką gałąź i jedyne co musisz zrobić to pobrać ją lokalnie. Jest to całkiem proste w gitcie. Polecenie ma format [cc inline="true" no_cc="true"]git checkout -b mybranch -t origin/mybranch[/cc]. Dla przykładu, jeżeli ktoś już stworzył gałąź rc-1.0, chcielibyśmy ją pobrać:

[cc lang="bash"](master)jj@im-jj:~/demo$ git checkout -b rc-1.0 -t origin/rc-1.0
Branch rc-1.0 set up to track remote branch rc-1.0 from origin.
Switched to a new branch ‘rc-1.0′
(rc-1.0)jj@im-jj:~/demo$ git remote show origin
* remote origin
URL: my-server:/git/demo.git
HEAD branch: master
Remote branches:
master tracked
rc-1.0 tracked
Local branches configured for ‘git pull’:
master merges with remote master
rc-1.0 merges with remote rc-1.0
Local refs configured for ‘git push’:
master pushes to master (up to date)
rc-1.0 pushes to rc-1.0 (up to date)[/cc]

Merge’owanie między dwoma gałęziami

Jeżeli wszystko poszło dobrze, dojdziemy do miejsca w którym przyszedł czas na złączenie wszystkich zmian, które zrobiłeś w swojej gałęzi RC w głównej gałęzi. To nic więcej niż [cc inline="true" no_cc="true"]git merge[/cc]. (Zapamiętaj, że jest to jedno z miejsc które nie będzie fast-forward, ale zawsze powinno mieć commita łączącego, aby pokazać, że złączyłeś gałąź RC z główną.)

[cc lang="bash"](rc-1.0)jj@im-jj:~/demo$ git checkout master
Switched to branch ‘master’
(master)jj@im-jj:~/demo$ git merge rc-1.0
Merge made by recursive.
mynewfile.txt | 1 +
mynewfile2.txt | 1 +
2 files changed, 2 insertions(+), 0 deletions(-)
create mode 100644 mynewfile.txt
create mode 100644 mynewfile2.txt
(master)jj@im-jj:~/demo$ git push
Counting objects: 4, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (2/2), 331 bytes, done.
Total 2 (delta 1), reused 0 (delta 0)
To my-server:/git/demo.git
af218a9..b4c7ac5 HEAD -> master[/cc]

Po tym złączeniu, Twoje repozytorium powinno wyglądać mniej więcej tak:

git po merge back

Gotowy do wydania

Kiedy Twój release candidate przeszedł przez wszystkie testy których potrzebował, powinieneś być gotowym do stworzenia gałęzi produkcyjnej. Wygląda to tak, jak w poprzednim przykładzie gdzie wszystko co potrzebujesz to stworzenie zdalnej gałęzi od gałęzi RC.

[cc lang="bash"](master)jj@im-jj:~/demo$ git checkout rc-1.0
Switched to branch ‘rc-1.0′
(rc-1.0)jj@im-jj:~/demo$ git checkout -b 1.0
Switched to a new branch ’1.0′
(1.0)jj@im-jj:~/demo$ git push origin 1.0
Total 0 (delta 0), reused 0 (delta 0)
To my-server:/git/demo.git
* [new branch] 1.0 -> 1.0[/cc]

Zwykle ta gałąź zostanie aktualizowana z gałęzią RC i poprawki bugów w RC są dołączane do gałęzi produkcyjnej.

[cc lang="bash"](1.0)jj@im-jj:~/demo$ git checkout rc-1.0
Switched to branch ‘rc-1.0′
(rc-1.0)jj@im-jj:~/demo$ echo ‘print “fixing a bug”‘ >> feature.py
(rc-1.0)jj@im-jj:~/demo$ git add -A && git commit -m “Added bug fix”
[rc-1.0 52a6d49] Added bug fix
1 files changed, 1 insertions(+), 0 deletions(-)
(rc-1.0)jj@im-jj:~/demo$ git push
Counting objects: 5, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 338 bytes, done.
Total 3 (delta 1), reused 0 (delta 0)
To my-server:/git/demo.git
97a0f2a..52a6d49 HEAD -> rc-1.0[/cc]

A kiedy ktoś zweryfikuje, że bug jest naprawiony:

[cc lang="bash"]rc-1.0)jj@im-jj:~/demo$ git checkout 1.0
Switched to branch ’1.0′
(1.0)jj@im-jj:~/demo$ git merge rc-1.0
Updating 97a0f2a..52a6d49
Fast forward
feature.py | 1 +
1 files changed, 1 insertions(+), 0 deletions(-)
(1.0)jj@im-jj:~/demo$ git push
Total 0 (delta 0), reused 0 (delta 0)
To my-server:/git/demo.git
97a0f2a..52a6d49 HEAD -> 1.0[/cc]

To samo należy zrobić kiedy chcesz ściągnąć te zmiany do mastera:

[cc lang="bash"](1.0)jj@im-jj:~/demo$ git checkout master
Switched to branch ‘master’
(master)jj@im-jj:~/demo$ git merge rc-1.0
Merge made by recursive.
feature.py | 1 +
1 files changed, 1 insertions(+), 0 deletions(-)
(master)jj@im-jj:~/demo$ git push
Counting objects: 4, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (2/2), 285 bytes, done.
Total 2 (delta 1), reused 0 (delta 0)
To my-server:/git/demo.git
b4c7ac5..4834481 HEAD -> master[/cc]

Co jeżeli chcę tylko jeden commit?

Może się zdarzyć, że gałąź produkcyjna wymaga poważnych łatek, ale są inne funkcjonalności na gałęzi, które jeszcze nie zostały odpowiednio przetestowane. Problem ten jest dobrze rozwiązać przez “cherry-picking”. Łączenie nie zadziała, ponieważ merge weźmie wszystkie commity będące rodzicami dla naszego wybranego commita, dopóki nie natrafi na wspólnego przodka, co zdecydowanie nie jest tym co chcesz, ponieważ ci rodzice nie zostali jeszcze przetestowani.

Podstawową ideą cherry-picking jest to, że git weźmie wybraną zmianę, zapakują w oddzielny commit i przeniesie do kolejnej gałęzi bez brania czegokolwiek innego.

Zróbmy dwie małe poprawki błędów na rc-1.0 a następnie weźmy ostatnią z cherry pick.

[cc lang="bash"](master)jj@im-jj:~/demo$ git checkout rc-1.0
Switched to branch ‘rc-1.0′
(rc-1.0)jj@im-jj:~/demo$ echo ‘# bugfix1!’ >> feature.py
(rc-1.0)jj@im-jj:~/demo$ git add -A && git commit -m “Added bugfix number 1″
[rc-1.0 92ab376] Added bugfix number 1
1 files changed, 1 insertions(+), 0 deletions(-)
(rc-1.0)jj@im-jj:~/demo$ echo ‘# bugfix2!’ >> feature2.py
(rc-1.0)jj@im-jj:~/demo$ git add -A && git commit -m “Added bugfix number 2″
[rc-1.0 1dec2d3] Added bugfix number 2
1 files changed, 1 insertions(+), 0 deletions(-)
create mode 100644 feature2.py
(rc-1.0)jj@im-jj:~/demo$ git push
Counting objects: 8, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (6/6), done.
Writing objects: 100% (6/6), 629 bytes, done.
Total 6 (delta 3), reused 0 (delta 0)
To my-server:/git/demo.git
52a6d49..1dec2d3 HEAD -> rc-1.0[/cc]

Jeżeli po prostu chcesz dołączyć poprawkę numer 2, merge będzie chciał również zabrać poprawkę numer 1 aż do wspólnego przodka. Nie przetestowaliśmy jeszcze poprawki numer 1, więc musimy zrobić cherry pick poprawki numer 2 do gałęzi 1.0.

[cc lang="bash"](rc-1.0)jj@im-jj:~/demo$ git log –pretty=oneline –abbrev-commit
1dec2d3 Added bugfix number 2 # < -- This is the commit we want, note the short-hash.
92ab376 Added bugfix number 1
52a6d49 Added bug fix
97a0f2a Minor commit
099214d Minor commit
d0e9912 Issue 12 - Added new feature XYZ
c348893 Added fixed typo
309291d Initial Commit
(rc-1.0)jj@im-jj:~/demo$ git checkout 1.0
Switched to branch '1.0'
(1.0)jj@im-jj:~/demo$ git cherry-pick 1dec2d3
Finished one cherry-pick.
[1.0 8e3a5f9] Added bugfix number 2
1 files changed, 1 insertions(+), 0 deletions(-)
create mode 100644 feature2.py
(1.0)jj@im-jj:~/demo$ git push
Counting objects: 4, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 289 bytes, done.
Total 3 (delta 1), reused 0 (delta 0)
To my-server:/git/demo.git
52a6d49..8e3a5f9 HEAD -> 1.0[/cc]

Teraz masz zestaw zmian z poprawki numer 2, ale technicznie całkowicie oddzielny commit (z własnym hashem commitu i wszystkim innym). Pozwala to na przemieszczenie rzeczy z gałęzi RC z pominięciem innych rzeczy w tej gałęzi. Również, kiedy zdecydujesz się na dołączenie reszty z gałęzi RC, nie powinno być problemu, gdyż git wie, że nowy commit jest tą samą treścią tylko z innym rodzicem.

Pomocy, zepsułem i chcę zacząć jeszcze raz

Kolejną podstawową rzeczą, którą znalazłem podczas całej pracy jest “spróbuj ponownie” po przypadkowym połączeniu złego commitu lub zrobieniu zmian, które nie są właściwe. W takich momentach pomocną dłoń wyciąga polecenie [cc inline="true" no_cc="true"]git reset[/cc].

Jeżeli właśnie scommitowałeś zanim byłeś gotowy, standardowe [cc inline="true" no_cc="true"]git reset HEAD~1[/cc] działa świetnie. W mowie gita brzmi tp “cofnij sprawy o jeden commit, ale nie zmiany treści plików na dysku”.

Jeżeli chcesz chcesz cofnąć jakiekolwiek zmiany w plikach lokalnych użyj flagi [cc inline="true" no_cc="true"]–hard[/cc] aby tego dokonać: [cc inline="true" no_cc="true"]git reset –hard HEAD~1[/cc].

[cc inline="true" no_cc="true"]HEAD~1[/cc] jest notacją mówiącą o jednym commicie wcześniej w stosunku do głowy aktualnej gałęzi. Więc [cc inline="true" no_cc="true"]HEAD~4[/cc] znaczy “cztery commity temu”.

Możesz również ustalić punkt cofnięcia do wybranego commita poprzez krótki hash. I pamiętaj, że flaga [cc inline="true" no_cc="true"]–hard[/cc] przeniesie Cie wstecz do miejsca w którym kod dokładnie wtedy tak wyglądał.

[cc lang="bash"](1.0)jj@im-jj:~/demo$ git log –abbrev-commit –pretty=oneline
8e3a5f9 Added bugfix number 2
52a6d49 Added bug fix
97a0f2a Minor commit
099214d Minor commit
d0e9912 Issue 12 – Added new feature XYZ
c348893 Added fixed typo
309291d Initial Commit
(1.0)jj@im-jj:~/demo$ git reset –hard 52a6d49
HEAD is now at 52a6d49 Added bug fix
(1.0)jj@im-jj:~/demo$ git log –abbrev-commit –pretty=oneline
52a6d49 Added bug fix
97a0f2a Minor commit
099214d Minor commit
d0e9912 Issue 12 – Added new feature XYZ
c348893 Added fixed typo
309291d Initial Commit[/cc]

Przypadkowo zacząłem moją pracę na masterze

Czasami zdarza mi się zrobić kilka małych zmian na gałęzi głównej, jedna prowadzi do kolejnej i kiedy nagle sobie uświadomię, że zrobiłem dużo zmian na gałęzi głównej, zamiast konkretnej funkcjonalności. Przyjrzyjmy się tej sytuacji:

[cc lang="bash"](rc-1.0)jj@im-jj:~/demo$ git checkout master
Switched to branch ‘master’
(master)jj@im-jj:~/demo$ touch myothernewfile.txt
(master)jj@im-jj:~/demo$ git add myothernewfile.txt
(master)jj@im-jj:~/demo$ git commit -m “Added another new file”
[master 617f46d] Added another new file
0 files changed, 0 insertions(+), 0 deletions(-)
create mode 100644 myothernewfile.txt
(master)jj@im-jj:~/demo$ touch onemorefile.txt
(master)jj@im-jj:~/demo$ git add onemorefile.txt
(master)jj@im-jj:~/demo$ git commit -m “Added one more new file”
[master 23290ed] Added one more new file
0 files changed, 0 insertions(+), 0 deletions(-)
create mode 100644 onemorefile.txt[/cc]

OK, teraz jesteśmy w gałęzi głównej z dwoma commitami, które przez przypadek zrobiliśmy na masterze zamiast gałęzi odpowiedniej funkcjonalności. Sposobem którym przeniesiemy to do odpowiedniej gałęzi i przywrócimy główną do normalnego stanu jest branching i resotowanie.

Najpierw stworzymy gałąź z gałęzi głównej, która ma te dwa commity. Następnie zresetujemy gałąź główną do stanu sprzed tych przypadkowych commitów.

[cc lang="bash"](master)jj@im-jj:~/demo$ git status
# On branch master
# Your branch is ahead of ‘origin/master’ by 2 commits.
#
nothing to commit (working directory clean)
(master)jj@im-jj:~/demo$ git branch feature1
(master)jj@im-jj:~/demo$ git reset –hard origin/master
HEAD is now at 4834481 Merge branch ‘rc-1.0′
(master)jj@im-jj:~/demo$ git status
# On branch master
nothing to commit (working directory clean)[/cc]

To zatrzyma Twoje zmiany w gałęzi “feature1″ a następnie przeniesie wskaźnik “master” z powrotem do odpowiedniego miejsca we współdzielonym repozytorium. Jeżeli sprawdzisz gałąź feature1, zobaczysz że zmiany których dokonałeś pierwotnie na głównej gałęzi, są teraz tutaj. Możesz łatwo dołączyć je do gałęzi głównej, jeżeli przyjdzie czas na nie.

To wszystko

To wszystko na teraz. Jeżeli jest coś co pominąłem co chciałbyś zobaczyć czuj się swobodnie wysyłając do mnie e-mail (adres na stronie autora – przyp. tłum.).

Dodatkowe podziękowania dla Mark’a Chadwick’a za bycie inspiracją dla tego postu. Jeżeli również pracujesz na wielu repozytoriach gita i wielu różnych gałęziach, zobacz mój poprzedni post (http://geewax.org/2009/11/15/git-workspace-magic.html) żeby zobaczyć jak zrobić swój PS1 pokazujący na której teraz jesteś.

Komentarze:

Link do oryginału się zmienił: http://geewax.org/agile-git-workflow

Dodaj komentarz:

 

Subscribe without commenting