Jak git-cherry-pick tylko zmiany niektórych plików?

586

Jeśli chcę scalić się z gałęzią Git, zmiany dokonane tylko w niektórych plikach zmienionych w konkretnym zatwierdzeniu, które obejmują zmiany wielu plików, jak można to osiągnąć?

Załóżmy, że git commit nazywa stuffma zmian w plikach A, B, C, a Djednak chcę scalić tylko stuff„s zmian w plikach Ai B. Brzmi jak zadanie, git cherry-pickale cherry-pickwie tylko, jak scalić całe zatwierdzenia, a nie podzbiór plików.

Tobias Kienzler
źródło

Odpowiedzi:

688

Zrobiłbym to za pomocą cherry-pick -n( --no-commit), który pozwala sprawdzić (i zmodyfikować) wynik przed zatwierdzeniem:

git cherry-pick -n <commit>

# unstage modifications you don't want to keep, and remove the
# modifications from the work tree as well.
# this does work recursively!
git checkout HEAD <path>

# commit; the message will have been stored for you by cherry-pick
git commit

Jeśli zdecydowana większość modyfikacji to rzeczy, których nie chcesz, zamiast sprawdzania poszczególnych ścieżek (środkowy krok), możesz zresetować wszystko z powrotem, a następnie dodać to, co chcesz:

# unstage everything
git reset HEAD

# stage the modifications you do want
git add <path>

# make the work tree match the index
# (do this from the top level of the repo)
git checkout .
Cascabel
źródło
10
Oprócz git checkout .tego polecam również git clean -fusunięcie wszelkich nowych, ale niechcianych plików wprowadzonych przez zatwierdzony wybór.
rlat
4
Dodatkowa uwaga dla tej drugiej metody: Używam, git add -pktóra pozwala interaktywnie decydować, które zmiany chcesz dodać do indeksu na plik
Matthaeus,
6
Nie jest to tak świetne w przypadku, gdy zatwierdzone wybranie nie ma zastosowania do bieżącej kopii roboczej, ponieważ jest tak różne, ale jeden plik miałby zastosowanie czysto.
Ograniczone Zadośćuczynienie
3
Możesz również selektywnie wycofać scenę za pomocą git reset -p HEAD. Jest to odpowiednik, add -pale bardzo niewielu wie, że istnieje.
Patrick Schlüter
1
Bardzo przydatna sztuczka. Włożyłem
Damiano
146

Inne metody nie działały dla mnie, ponieważ zatwierdzenie zawierało wiele zmian i konfliktów z wieloma innymi plikami. To, co wymyśliłem, było po prostu

git show SHA -- file1.txt file2.txt | git apply -

W rzeczywistości nie zawiera addplików ani nie wykonuje zatwierdzenia, więc być może będziesz musiał to zrobić

git add file1.txt file2.txt
git commit -c SHA

Lub jeśli chcesz pominąć dodawanie, możesz użyć --cachedargumentu dogit apply

git show SHA -- file1.txt file2.txt | git apply --cached -

Możesz także zrobić to samo dla całych katalogów

git show SHA -- dir1 dir2 | git apply -
Michael Anderson
źródło
2
Ciekawa metoda, dzięki. Ale czy show SHA -- file | applyzasadniczo nie robi tego samego, checkout SHA -- fileco w odpowiedzi Marka Longaira ?
Tobias Kienzler,
4
Nie, checkout SHA -- fileprzejdzie dokładnie do wersji SHA, podczas gdyshow SHA -- file | apply zastosuje tylko zmiany w SHA (tak jak robi to cherry-pick). Ma znaczenie, czy (a) istnieje więcej niż jeden zatwierdzenie zmieniające dany plik w gałęzi źródłowej, lub (b) istnieje zatwierdzenie zmieniające plik w bieżącej gałęzi docelowej.
Michael Anderson
9
Właśnie znalazłem inne świetne zastosowanie do tego: selektywne przywracanie, gdy chcesz przywrócić tylko jeden plik (ponieważ git revertcofa całe zatwierdzenie). W takim przypadku wystarczy użyćgit show -R SHA -- file1.txt file2.txt | git apply -
Michael Anderson
2
@RoeiBahumi, które ma zupełnie inne znaczenie. git diff SHA -- file1.txt file2.txt | git apply -oznacza zastosowanie wszystkich różnic między bieżącą wersją pliku a wersją w SHA do bieżącej wersji. W istocie jest to to samo co git checkout SHA -- file1.txt file2.txt. Zobacz mój wcześniejszy komentarz, dlaczego różni się on od git showwersji.
Michael Anderson
5
Jeśli musisz rozwiązać konflikt, użyj git apply -3 -zamiast po prostu git apply -, a jeśli wystąpi konflikt, możesz użyć standardowej techniki rozwiązywania konfliktów, w tym przy użyciu git mergetool.
qwertzguy 4.04.16
87

Zwykle używam -p flagi z kasą git z innej gałęzi, co uważam za łatwiejsze i bardziej szczegółowe niż większość innych metod, z którymi się spotkałem.

Zasadniczo:

git checkout <other_branch_name> <files/to/grab in/list/separated/by/spaces> -p

przykład:

git checkout mybranch config/important.yml app/models/important.rb -p

Następnie pojawia się okno dialogowe z pytaniem, jakie zmiany chcesz w „obiektach blob”, co właściwie działa na każdą część ciągłej zmiany kodu, którą możesz następnie zasygnalizować y(Tak) n(Nie) itp. Dla każdej części kodu.

The -p lub patchdziała z wieloma poleceniami w git, w tym git stash save -pco pozwala ci wybrać, co chcesz ukryć w bieżącej pracy

Czasami używam tej techniki, kiedy wykonałem dużo pracy i chciałbym ją rozdzielić i zatwierdzić więcej tematycznych zatwierdzeń, używając git add -pi wybierając to, co chcę dla każdego zatwierdzenia :)

Tyrone Wilson
źródło
3
Regularnie używam git-add -p, ale nie wiedziałem git-checkoutteż, że ma -pflagę - czy to rozwiązuje problemy z-p łączeniem, których brak odpowiedzi ?
Tobias Kienzler
1
przynajmniej -ppozwoliłoby na ręczną edycję takiej sprzecznej sekcji, która cherry-pickprawdopodobnie i tak by się przyniosła. Przetestuję to następnym razem, gdy będę tego potrzebować, zdecydowanie interesujące podejście
Tobias Kienzler
2
Jedna z dwóch najlepszych odpowiedzi, które nie zabijają jednoczesnych zmian w gałęziach.
akostadinov,
1
Zobacz tę odpowiedź, aby dowiedzieć się, jak wybrać przystojniaki: stackoverflow.com/a/10605465/4816250 Opcja „s” była szczególnie pomocna.
jvd10
1
git reset -p HEADzezwala również na to, -pco może być przydatne, gdy chcesz usunąć tylko niektóre poprawki z indeksu.
Patrick Schlüter
42

Być może przewaga tej metody nad odpowiedzią Jefromiego to, że nie musisz pamiętać, które zachowanie git reset jest właściwe :)

 # Create a branch to throw away, on which we'll do the cherry-pick:
 git checkout -b to-discard

 # Do the cherry-pick:
 git cherry-pick stuff

 # Switch back to the branch you were previously on:
 git checkout -

 # Update the working tree and the index with the versions of A and B
 # from the to-discard branch:
 git checkout to-discard -- A B

 # Commit those changes:
 git commit -m "Cherry-picked changes to A and B from [stuff]"

 # Delete the temporary branch:
 git branch -D to-discard
Mark Longair
źródło
2
dzięki za odpowiedź. To zainspirowało mnie do myślenia, dlaczego nie pominąć cherry-picki bezpośrednio użyć git checkout stuff -- A B? A przy git commit -C stuffprzekazie zatwierdzenia również pozostanie to samo
Tobias Kienzler
8
@Tobias: To będzie działać tylko wtedy, gdy pliki zmodyfikowane na stuffnie zostały zmienione na bieżącym oddział lub w dowolnym miejscu pomiędzy wspólnego przodka HEADi stuffa końcówką stuff. Jeśli tak, to cherry-picktworzy poprawny wynik (zasadniczo wynik scalenia), podczas gdy twoja metoda odrzuci zmiany w bieżącej gałęzi i zachowa wszystkie zmiany od wspólnego przodka do stuff- nie tylko tych w tym pojedyncze zatwierdzenie.
Cascabel
2
@Tobias Kienzler: Zakładałem, że twój punkt początkowy był wystarczająco różny od rodzica, stuffże wynik wybrania wiśni odszedł Ai Bmiał inną treść niż ich zawartość w zatwierdzeniu stuff. Jeśli jednak byłoby tak samo, masz rację - możesz zrobić tak, jak mówisz.
Mark Longair,
@Jeromi, @Mark: dzięki za opinie, w moim przypadku traktuję oddziały z całkowicie rozłącznymi plikami, które doprowadziły mnie do mojej sugestii. Ale rzeczywiście wcześniej czy później miałem z tym kłopoty, więc dziękuję za poruszenie tej
kwestii
Myślę, że moja odpowiedź w tym innym wątku może być tym, czego szukasz.
Ian
30

Cherry pick to wybieranie zmian z określonego „zatwierdzenia”. Najprostszym rozwiązaniem jest wybranie wszystkich zmian określonych plików

 git checkout source_branch <paths>...

W przykładzie:

$ git branch
* master
  twitter_integration
$ git checkout twitter_integration app/models/avatar.rb db/migrate/20090223104419_create_avatars.rb test/unit/models/avatar_test.rb test/functional/models/avatar_test.rb
$ git status
# On branch master
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
#   new file:   app/models/avatar.rb
#   new file:   db/migrate/20090223104419_create_avatars.rb
#   new file:   test/functional/models/avatar_test.rb
#   new file:   test/unit/models/avatar_test.rb
#
$ git commit -m "'Merge' avatar code from 'twitter_integration' branch"
[master]: created 4d3e37b: "'Merge' avatar code from 'twitter_integration' branch"
4 files changed, 72 insertions(+), 0 deletions(-)
create mode 100644 app/models/avatar.rb
create mode 100644 db/migrate/20090223104419_create_avatars.rb
create mode 100644 test/functional/models/avatar_test.rb
create mode 100644 test/unit/models/avatar_test.rb

Źródła i pełne wyjaśnienie http://jasonrudolph.com/blog/2009/02/25/git-tip-how-to-merge-specific-files-from-another-branch/

AKTUALIZACJA:

Dzięki tej metodzie git nie połączy pliku, po prostu nadpisze wszelkie inne zmiany dokonane w gałęzi docelowej. Konieczne będzie ręczne scalenie zmian:

$ git diff nazwa pliku HEAD

cminatti
źródło
5
Też tak myślałem , ale to nie powiedzie się okropnie, jeśli pliki uległy zmianie w obu gałęziach, ponieważ odrzuca zmiany w bieżącej gałęzi
Tobias Kienzler
Masz rację, należy wyjaśnić, że w ten sposób git NIE ŁĄCZY się, po prostu zastępuje. Następnie możesz wykonać „git diff HEAD filename”, aby zobaczyć, co się zmieniło i scalić ręcznie.
cminatti
18

Sytuacja:

Powiedzmy, że jesteś w swoim oddziale master i masz swoje zatwierdzenie w dowolnym innym oddziale. Musisz wybrać tylko jeden plik z tego konkretnego zatwierdzenia.

Podejście:

Krok 1: Kasa w wymaganym oddziale.

git checkout master

Krok 2: Upewnij się, że skopiowałeś wymagany hash zatwierdzenia.

git checkout commit_hash path\to\file

Krok 3: Masz teraz zmiany wymaganego pliku w wybranym oddziale. Wystarczy je dodać i zatwierdzić.

git add path\to\file
git commit -m "Your commit message"
techdreams
źródło
1
Niesamowite! Pracowałem także dla wszystkich zmian w katalogu z \ path \ to \ directory \ dla mnie
zaggi
13

Po prostu wybrałbym wszystko, a następnie zrobiłbym to:

git reset --soft HEAD^

Następnie cofnę zmiany, których nie chcę, a następnie dokonam nowego zatwierdzenia.

funroll
źródło
11

Użyj git merge --squash branch_nametego, aby uzyskać wszystkie zmiany z drugiego oddziału i przygotować dla ciebie zatwierdzenie. Teraz usuń wszystkie niepotrzebne zmiany i pozostaw tę, którą chcesz. I git nie będzie wiedział, że nastąpiła fuzja.

Nieposkromiony
źródło
Dzięki, nie wiedziałem o tej opcji scalania. Jest to realna alternatywa, jeśli chcesz wybrać większość gałęzi (ale w przeciwieństwie do wyboru nie zadziała, jeśli nie ma wspólnego przodka)
Tobias Kienzler
4

Znalazłem inny sposób, który zapobiega wszelkim konfliktowym połączeniom podczas zbierania wiśni, które IMO jest w pewnym sensie łatwe do zapamiętania i zrozumienia. Ponieważ tak naprawdę nie wybierasz zatwierdzenia, ale jego część, musisz najpierw go podzielić, a następnie utworzyć zatwierdzenie, które będzie odpowiadać twoim potrzebom i wybierz go.

Najpierw utwórz gałąź z zatwierdzenia, które chcesz podzielić, i sprawdź:

$ git checkout COMMIT-TO-SPLIT-SHA -b temp

Następnie cofnij poprzednie zatwierdzenie:

$ git reset HEAD~1

Następnie dodaj pliki / zmiany, które chcesz wybrać:

$ git add FILE

i popełnij to:

$ git commit -m "pick me"

zwróć uwagę na skrót zatwierdzenia, nazwijmy go PICK-SHA i wróć do głównej gałęzi, na przykład master wymuszając kasę:

$ git checkout -f master

i wybieraj zatwierdzenie:

$ git cherry-pick PICK-SHA

teraz możesz usunąć gałąź temp:

$ git branch -d temp -f
Alexey
źródło
2

Połącz gałąź w nową (squash) i usuń niepotrzebne pliki:

git checkout master
git checkout -b <branch>
git merge --squash <source-branch-with-many-commits>
git reset HEAD <not-needed-file-1>
git checkout -- <not-needed-file-1>
git reset HEAD <not-needed-file-2>
git checkout -- <not-needed-file-2>
git commit
nvd
źródło
2

Dla kompletności najlepsze dla mnie jest:

git show YOURHASH --no-color -- file1.txt file2.txt dir3 dir4 | git apply -3 --index -

git status

Robi dokładnie to, czego chce OP. W razie potrzeby rozwiązuje konflikty, podobnie jak mergeto robi. To robi, addale nie committwoje nowe zmiany.

kubańczyk
źródło
1

Możesz użyć:

git diff <commit>^ <commit> -- <path> | git apply

Notacja <commit>^określa (pierwszego) rodzica <commit>. Dlatego to polecenie diff wybiera zmiany dokonane <path>w zatwierdzeniu<commit> .

Zauważ, że to jeszcze niczego nie popełni (jak to git cherry-pickrobi). Więc jeśli tego chcesz, musisz zrobić:

git add <path>
git commit
Garogolun
źródło