Scalanie, aktualizowanie i pobieranie gałęzi Git bez użycia kas

628

Pracuję nad projektem, który ma 2 gałęzie, A i B. Zazwyczaj pracuję na gałęzi A i łączę rzeczy z gałęzi B. Zazwyczaj robiłbym:

git merge origin/branchB

Chciałbym jednak zachować lokalną kopię oddziału B, ponieważ od czasu do czasu mogę sprawdzić oddział bez uprzedniego połączenia z moim oddziałem A. W tym celu zrobiłbym:

git checkout branchB
git pull
git checkout branchA

Czy istnieje sposób na wykonanie powyższej czynności za pomocą jednego polecenia i bez konieczności przełączania gałęzi tam iz powrotem? Czy powinienem tego używać git update-ref? W jaki sposób?

Charles
źródło
1
Odpowiedź Jakuba na pierwsze powiązane pytanie wyjaśnia, dlaczego jest to na ogół niemożliwe. Innym wyjaśnieniem (a posteriori) jest to, że nie można scalić w nagim repo, więc najwyraźniej wymaga to drzewa roboczego.
Cascabel,
3
@Eric: Typowe powody są takie, że kasy są czasochłonne w przypadku dużych repozytoriów i że aktualizują znaczniki czasu, nawet jeśli powrócisz do tej samej wersji, więc pomyśl, że wszystko musi zostać odbudowane.
Cascabel,
Drugie pytanie, które podłączyłem, dotyczy niecodziennego przypadku - fuzji, które mogą być szybkie, ale które OP chciał scalić przy użyciu --no-ffopcji, co powoduje, że i tak rejestrowanie zatwierdzenia scalania jest rejestrowane. Jeśli jesteś zainteresowany, moja odpowiedź tam pokazuje, jak możesz to zrobić - nie tak solidna, jak moja zamieszczona tutaj odpowiedź, ale mocne strony tych dwóch można z pewnością połączyć.
Cascabel,

Odpowiedzi:

971

Krótka odpowiedź

Tak długo, jak wykonujesz szybkie przewijanie do przodu , możesz po prostu użyć

git fetch <remote> <sourceBranch>:<destinationBranch>

Przykłady:

# Merge local branch foo into local branch master,
# without having to checkout master first.
# Here `.` means to use the local repository as the "remote":
git fetch . foo:master

# Merge remote branch origin/foo into local branch foo,
# without having to checkout foo first:
git fetch origin foo:foo

Podczas gdy odpowiedź Amber będzie działać również w przypadkach szybkiego przewijania do przodu, użycie git fetchw ten sposób jest nieco bezpieczniejsze niż zwykłe przesunięcie odniesienia gałęzi, ponieważ git fetchautomatycznie zapobiegnie przypadkowemu nie przewinięciu do przodu, o ile nie użyjesz go +w refspec.

Długa odpowiedź

Nie można scalić gałęzi B z gałęzią A bez uprzedniego sprawdzenia A, jeśli spowodowałoby to scalenie nie do przodu. Wynika to z faktu, że kopia robocza jest potrzebna do rozwiązania wszelkich potencjalnych konfliktów.

Jest to jednak możliwe w przypadku połączeń szybkiego przewijania , ponieważ połączenia takie z definicji nigdy nie mogą prowadzić do konfliktów. Aby to zrobić bez uprzedniego sprawdzenia gałęzi, możesz użyć git fetchz refspec.

Oto przykład aktualizacji master(nie zezwalającej na zmiany inne niż szybkie przewijanie do przodu), jeśli masz featurewyewidencjonowany inny oddział :

git fetch upstream master:master

Ten przypadek użycia jest tak powszechny, że prawdopodobnie będziesz chciał utworzyć dla niego alias w pliku konfiguracyjnym git, taki jak ten:

[alias]
    sync = !sh -c 'git checkout --quiet HEAD; git fetch upstream master:master; git checkout --quiet -'

Ten alias robi to:

  1. git checkout HEAD: powoduje to oderwanie kopii roboczej. Jest to przydatne, jeśli chcesz dokonać aktualizacji, mastergdy zdarzyło się, że została wyewidencjonowana. Myślę, że było to konieczne, ponieważ w przeciwnym razie odwołanie do gałęzi masternie ruszy się, ale nie pamiętam, czy to naprawdę od razu z mojej głowy.

  2. git fetch upstream master:master: szybko przesyła lokalną wiadomość masterw to samo miejsce, co upstream/master.

  3. git checkout -sprawdza twoją poprzednio wyrejestrowaną gałąź (tak dzieje się -w tym przypadku).

Składnia git fetchfor (nie) szybkiego przewijania do przodu

Jeśli chcesz, aby fetchpolecenie zakończyło się niepowodzeniem, jeśli aktualizacja nie obsługuje szybkiego przewijania, wystarczy użyć refspec formularza

git fetch <remote> <remoteBranch>:<localBranch>

Jeśli chcesz zezwolić na aktualizacje nie do szybkiego przewijania, dodajesz znak +z przodu refspec:

git fetch <remote> +<remoteBranch>:<localBranch>

Pamiętaj, że możesz przekazać swoje lokalne repozytorium jako parametr „zdalny”, używając .:

git fetch . <sourceBranch>:<destinationBranch>

Dokumentacja

Z git fetchdokumentacji wyjaśniającej tę składnię (wyróżnienie moje):

<refspec>

Format <refspec>parametru jest opcjonalnym plusem +, po którym następuje odwołanie źródłowe <src>, dwukropek :, a następnie odwołanie do miejsca docelowego <dst>.

Zdalne dopasowanie, które pasuje, <src>jest pobierane, a jeśli <dst>nie jest pustym ciągiem, lokalne odwołanie, które pasuje do niego, jest przewijane do przodu za pomocą<src> . Jeśli+zostanie użytyopcjonalny plus, lokalny numer referencyjny zostanie zaktualizowany, nawet jeśli nie spowoduje to aktualizacji do przodu.

Zobacz też

  1. Przejdź do kasy i połącz bez dotykania działającego drzewa

  2. Scalanie bez zmiany katalogu roboczego

Społeczność
źródło
3
git checkout --quiet HEADjest git checkout --quiet --detachod Git 1.7.5.
Rafa
6
Stwierdziłem, że muszę zrobić: git fetch . origin/foo:foozaktualizować mojego lokalnego foo do mojego lokalnego pochodzenia / foo
weston
3
Czy istnieje powód, dla którego części „git checkout HEAD --quiet” i „git checkout --quiet -” są zawarte w długiej odpowiedzi, ale nie w krótkiej odpowiedzi? Wydaje mi się, że to dlatego, że skrypt może być uruchamiany, gdy masz już wypisany przez mistrza, nawet jeśli możesz po prostu zrobić git pull?
Sean
8
dlaczego „pobierz” polecenie wykonania „scalenia” tutaj… to po prostu nie ma sensu; Jeśli „pull” to „fetch”, a następnie „merge”, musi istnieć bardziej logiczny odpowiednik „merge - tylko -ffff”, który aktualizowałby lokalnie „gałąź” z „źródła / gałęzi” lokalnie, biorąc pod uwagę, że „pobieranie” ma już został uruchomiony.
Ed Randall
1
Dzięki za git checkout -podstęp! Tak proste jak cd -.
Steed,
84

Nie, nie ma. Wyewidencjonowanie gałęzi docelowej jest konieczne, aby między innymi rozwiązać konflikty (jeśli Git nie jest w stanie automatycznie ich scalić).

Jeśli jednak scalanie byłoby szybkie, nie trzeba sprawdzać gałęzi docelowej, ponieważ tak naprawdę nie trzeba niczego scalać - wystarczy zaktualizować gałąź, aby wskazywała nowa głowa ref. Możesz to zrobić za pomocą git branch -f:

git branch -f branch-b branch-a

Zaktualizuje się, branch-baby wskazywać na głowę branch-a.

-fOpcja oznacza --force, co oznacza, że trzeba być ostrożnym podczas korzystania z niego.

Nie używaj go, chyba że masz absolutną pewność, że scalenie zostanie przewinięte do przodu.

Bursztyn
źródło
46
Uważaj tylko, aby tego nie robić, chyba że masz absolutną pewność, że połączenie będzie szybkie! Nie chcę później wiedzieć, że zgubiłeś zatwierdzenia.
Cascabel,
@FuadSaud Niezupełnie. git resetdziała tylko w aktualnie wyrejestrowanym oddziale.
Amber
9
Ten sam wynik (szybkie przewijanie do przodu) osiąga się git fetch upstream branch-b:branch-b(wzięty z tej odpowiedzi ).
Oliver
6
Aby rozwinąć komentarz @ Oliver, możesz również zrobić git fetch <remote> B:A, gdzie B i A to zupełnie inne gałęzie, ale B można szybko połączyć do przodu w A. Możesz także przekazać swoje lokalne repozytorium jako „zdalne”, używając .jako zdalnego aliasu: git fetch . B:A.
8
Pytanie PO wyjaśnia dość jasno, że scalenie rzeczywiście nastąpi szybko. Niezależnie od tego, branch -fmoże być niebezpieczne, jak zauważyłeś. Więc nie używaj tego! Użyj fetch origin branchB:branchB, która zakończy się niepowodzeniem, jeśli scalenie nie jest przewijane do przodu.
Bennett McElwee
30

Jak powiedział Amber, szybkie połączenia do przodu to jedyny przypadek, w którym można to zrobić. Każde inne scalenie prawdopodobnie musi przejść przez całą trójstronną fuzję, stosowanie poprawek, rozwiązywanie konfliktów - a to oznacza, że ​​muszą istnieć pliki.

Tak się składa, że ​​mam skrypt, którego używam właśnie do tego: wykonuję scalanie do przodu bez dotykania drzewa roboczego (chyba że łączysz się w HEAD). Jest trochę długi, ponieważ jest co najmniej trochę solidny - sprawdza, czy scalenie będzie szybkie do przodu, a następnie wykonuje go bez sprawdzania gałęzi, ale daje takie same wyniki, jak gdybyś miał - widzisz diff --statpodsumowanie zmian, a wpis w reflogu jest dokładnie jak szybkie scalanie do przodu, zamiast „resetowania”, które otrzymujesz, jeśli używasz branch -f. Jeśli to nazwę git-merge-ffi upuść go w katalogu bin, można nazwać to jako polecenie git: git merge-ff.

#!/bin/bash

_usage() {
    echo "Usage: git merge-ff <branch> <committish-to-merge>" 1>&2
    exit 1
}

_merge_ff() {
    branch="$1"
    commit="$2"

    branch_orig_hash="$(git show-ref -s --verify refs/heads/$branch 2> /dev/null)"
    if [ $? -ne 0 ]; then
        echo "Error: unknown branch $branch" 1>&2
        _usage
    fi

    commit_orig_hash="$(git rev-parse --verify $commit 2> /dev/null)"
    if [ $? -ne 0 ]; then
        echo "Error: unknown revision $commit" 1>&2
        _usage
    fi

    if [ "$(git symbolic-ref HEAD)" = "refs/heads/$branch" ]; then
        git merge $quiet --ff-only "$commit"
    else
        if [ "$(git merge-base $branch_orig_hash $commit_orig_hash)" != "$branch_orig_hash" ]; then
            echo "Error: merging $commit into $branch would not be a fast-forward" 1>&2
            exit 1
        fi
        echo "Updating ${branch_orig_hash:0:7}..${commit_orig_hash:0:7}"
        if git update-ref -m "merge $commit: Fast forward" "refs/heads/$branch" "$commit_orig_hash" "$branch_orig_hash"; then
            if [ -z $quiet ]; then
                echo "Fast forward"
                git diff --stat "$branch@{1}" "$branch"
            fi
        else
            echo "Error: fast forward using update-ref failed" 1>&2
        fi
    fi
}

while getopts "q" opt; do
    case $opt in
        q ) quiet="-q";;
        * ) ;;
    esac
done
shift $((OPTIND-1))

case $# in
    2 ) _merge_ff "$1" "$2";;
    * ) _usage
esac

PS Jeśli ktoś zauważy jakiekolwiek problemy z tym skryptem, proszę o komentarz! To była praca polegająca na pisaniu i zapominaniu, ale chętnie ją poprawię.

Cascabel
źródło
możesz być zainteresowany stackoverflow.com/a/5148202/717355 dla porównania
Philip Oakley,
@PhilipOakley Od szybkiego spojrzenia, że ​​w rdzeniu robi dokładnie to samo, co moje. Ale moje nie jest zakodowane na stałe w pojedynczej parze gałęzi, ma znacznie większą obsługę błędów, symuluje wyjście git-merge i robi to, co masz na myśli, jeśli nazywasz go, gdy jesteś w gałęzi.
Cascabel,
Mam twój w moim katalogu \ bin ;-) Właśnie o tym zapomniałem i rozejrzałem się, zobaczyłem ten skrypt i przypomniałem sobie o nim! Moja kopia ma ten link i # or git branch -f localbranch remote/remotebranchprzypomina mi o źródle i opcjach. Daj komentarz do drugiego linku +1.
Philip Oakley,
3
+1, można również domyślnie "$branch@{u}"
ustawić
Dzięki! Czy jest jakaś szansa, że ​​wrzucisz prosty przykładowy przykład do odpowiedzi?
nmr
20

Możesz to zrobić tylko wtedy, gdy scalenie jest przewijaniem do przodu. Jeśli nie, to git musi pobrać pliki, aby je scalić!

Aby to zrobić tylko w przypadku szybkiego przewijania do przodu :

git fetch <branch that would be pulled for branchB>
git update-ref -m "merge <commit>: Fast forward" refs/heads/<branch> <commit>

gdzie <commit>jest pobrany commit, ten, do którego chcesz szybko przewinąć do przodu. Zasadniczo jest to jak użycie git branch -fdo przeniesienia gałęzi, z tym wyjątkiem, że rejestruje go również w dzienniku reflogu, tak jakby to było scalenie.

Proszę, proszę, proszę , nie rób tego za coś, co nie jest do przodu, czy to po prostu być zresetowanie oddziału na drugi popełnić. (Aby sprawdzić, sprawdź, czy git merge-base <branch> <commit>daje SHA1 oddziału).

Cascabel
źródło
4
Czy istnieje sposób, aby spowodować awarię, jeśli nie można szybko przewinąć do przodu?
gman
2
@gman możesz użyć git merge-base --is-ancestor <A> <B>. „B” to rzecz, którą należy połączyć w „A”. Przykładem może być A = master i B = develop, upewniając się, że develop jest szybko przewijalny do master. Uwaga: Istnieje z 0, jeśli nie jest w stanie FF, istnieje z 1, jeśli jest.
eddiemoya
1
Nie udokumentowane w git-scm, ale na kernal.org. kernel.org/pub/software/scm/git/docs/git-merge-base.html
eddiemoya
Zrobiłem szybki pomysł, aby podsumować to, o czym mówię (tak naprawdę go nie testowałem, ale powinien cię tam głównie zabrać). gist.github.com/eddiemoya/ad4285b2d8a6bdabf432 - - na marginesie, miałem większość tego przydatnego, mam skrypt, który sprawdza ff i jeśli nie, pozwala najpierw zmienić bazę żądanego oddziału - a następnie ff łączy się - wszystko bez sprawdzania czegokolwiek.
eddiemoya
12

W twoim przypadku możesz użyć

git fetch origin branchB:branchB

co robi, co chcesz (zakładając, że scalanie jest szybkie przewijanie do przodu). Jeśli gałąź nie może zostać zaktualizowana, ponieważ wymaga scalenia nie do szybkiego przewijania, oznacza to, że komunikat nie powiedzie się bezpiecznie.

Ta forma pobierania ma również kilka bardziej przydatnych opcji:

git fetch <remote> <sourceBranch>:<destinationBranch>

Pamiętaj, że <remote> może to być lokalne repozytorium i <sourceBranch>może być gałąź śledzenia. Możesz więc zaktualizować lokalny oddział, nawet jeśli nie jest wyewidencjonowany, bez dostępu do sieci .

Obecnie mój dostęp do serwera nadrzędnego odbywa się za pośrednictwem wolnej sieci VPN, więc okresowo łączę się, git fetchaby zaktualizować wszystkie piloty, a następnie rozłączam się. Jeśli, powiedzmy, zdalny mistrz zmienił się, mogę to zrobić

git fetch . remotes/origin/master:master

aby bezpiecznie zaktualizować mojego lokalnego mistrza, nawet jeśli obecnie mam jakiś inny oddział. Nie wymaga dostępu do sieci.

Bennett McElwee
źródło
11

Innym, co prawda dość brutalnym sposobem jest po prostu odtworzenie oddziału:

git fetch remote
git branch -f localbranch remote/remotebranch

Wyrzuca to lokalną nieaktualną gałąź i odtwarza ją o tej samej nazwie, więc używaj jej ostrożnie ...

kkoehne
źródło
Właśnie widziałem teraz, że pierwotna odpowiedź już wspomina gałąź -f ... Mimo to nie widzę korzyści z połączenia w dzienniku referencyjnym dla pierwotnie opisanego przypadku użycia.
kkoehne
7

Możesz sklonować repozytorium i wykonać scalenie w nowym repozytorium. W tym samym systemie plików będzie to dowiązanie twarde zamiast kopiowania większości danych. Zakończ, przeciągając wyniki do oryginalnego repozytorium.

wnoise
źródło
4

Wprowadź polecenie git-forward-merge :

Bez konieczności kasowania miejsca docelowego git-forward-merge <source> <destination>scala źródło w gałęzi docelowej.

https://github.com/schuyler1d/git-forward-merge

Działa tylko w przypadku automatycznych połączeń, jeśli występują konflikty, należy użyć zwykłego scalania.

Lkraider
źródło
2
Myślę, że jest to lepsze niż git fetch <remote> <source>: <destination>, ponieważ szybkie przewijanie do przodu jest operacją scalania, a nie pobierania i łatwiej jest pisać. Zła rzecz, to nie jest domyślny git.
Binarian
4

W wielu przypadkach (takich jak scalanie) możesz po prostu użyć zdalnego oddziału bez konieczności aktualizacji lokalnego oddziału śledzenia. Dodanie wiadomości do reflogu brzmi jak przesada i przestanie być szybsze. Aby ułatwić odzyskiwanie, dodaj następujące elementy do konfiguracji git

[core]
    logallrefupdates=true

Następnie wpisz

git reflog show mybranch

aby zobaczyć najnowszą historię swojego oddziału

Casebash
źródło
Myślę, że część musi być [core]nie [user]? (i jest domyślnie włączony, w przypadku repozytoriów z obszarem roboczym (tj. non-go).
eckes
3

Napisałem funkcję powłoki dla podobnego przypadku użycia, z którym spotykam się codziennie w projektach. Jest to w zasadzie skrót do aktualizowania lokalnych oddziałów za pomocą wspólnego oddziału, takiego jak rozwijanie przed otwarciem PR itp.

Publikowanie tego, nawet jeśli nie chcesz go używać checkout, na wypadek, gdyby inni nie mieli nic przeciwko temu ograniczeniu.

glmh(„git pull and merge here”) automatycznie checkout branchB, pullnajnowsze, ponownie checkout branchAi merge branchB.

Nie rozwiązuje potrzeby przechowywania lokalnej kopii oddziału A, ale można ją łatwo zmodyfikować, dodając krok przed sprawdzeniem oddziału B. Coś jak...

git branch ${branchA}-no-branchB ${branchA}

W przypadku prostych operacji szybkiego przewijania następuje przejście do monitu komunikatu zatwierdzenia.

W przypadku połączeń bez szybkiego przewijania powoduje to, że oddział znajduje się w stanie rozwiązywania konfliktu (prawdopodobnie konieczna jest interwencja).

Aby skonfigurować, dodać do .bashrclub .zshrcitp .:

glmh() {
    branchB=$1
    [ $# -eq 0 ] && { branchB="develop" }
    branchA="$(git branch | grep '*' | sed 's/* //g')"
    git checkout ${branchB} && git pull
    git checkout ${branchA} && git merge ${branchB} 
}

Stosowanie:

# No argument given, will assume "develop"
> glmh

# Pass an argument to pull and merge a specific branch
> glmh your-other-branch

Uwaga: Nie jest to wystarczająco odporne na przekazywanie argumentów poza nazwą oddziału dogit merge

rkd
źródło
2

Innym sposobem na skuteczne wykonanie tego jest:

git fetch
git branch -d branchB
git branch -t branchB origin/branchB

Ponieważ jest to mała litera -d, usunie ją tylko, jeśli dane gdzieś nadal będą istnieć. Jest podobny do odpowiedzi @ kkoehne, tyle że nie wymusza. Z tego powodu -tponownie skonfiguruje pilota.

Miałem nieco inną potrzebę niż OP, która polegała na utworzeniu nowej gałęzi funkcji poza develop(lub master) po scaleniu żądania ściągnięcia. Można tego dokonać w jednej linijce bez użycia siły, ale nie aktualizuje ona lokalnego developoddziału. To tylko kwestia sprawdzenia nowego oddziału i oparcia go na origin/develop:

git checkout -b new-feature origin/develop
Benjamin Atkin
źródło
1

po prostu wyciągnąć mistrza bez sprawdzania mistrza, którego używam

git fetch origin master:master

Sodhi Saab
źródło
1

Absolutnie możliwe jest wykonanie dowolnego scalenia, a nawet nie szybkich połączeń do przodu git checkout. worktreeOdpowiedź przez @grego to dobra wskazówka. Aby rozwinąć tę kwestię:

cd local_repo
git worktree add _master_wt master
cd _master_wt
git pull origin master:master
git merge --no-ff -m "merging workbranch" my_work_branch
cd ..
git worktree remove _master_wt

Teraz połączyłeś lokalny oddział pracy z lokalnym masteroddziałem bez zmiany kasy.

zanerock
źródło
1

Jeśli chcesz zachować to samo drzewo co gałąź, którą chcesz scalić (tzn. Nie jest to prawdziwe „scalenie”), możesz to zrobić w ten sposób.

# Check if you can fast-forward
if git merge-base --is-ancestor a b; then
    git update-ref refs/heads/a refs/heads/b
    exit
fi

# Else, create a "merge" commit
commit="$(git commit-tree -p a -p b -m "merge b into a" "$(git show -s --pretty=format:%T b)")"
# And update the branch to point to that commit
git update-ref refs/heads/a "$commit"
phkb
źródło
1
git worktree add [-f] [--detach] [--checkout] [--lock] [-b <new-branch>] <path> [<commit-ish>]

Możesz spróbować git worktreeotworzyć dwie gałęzie obok siebie, to może brzmieć tak, jakbyś tego chciał, ale bardzo różni się od niektórych innych odpowiedzi, które tu widziałem.

W ten sposób możesz mieć dwie osobne gałęzie śledzące w tym samym repozytorium git, więc musisz pobrać tylko raz, aby uzyskać aktualizacje w obu drzewach roboczych (zamiast klonować dwa razy i ciągnąć każde z nich)

Worktree utworzy nowy katalog roboczy dla twojego kodu, w którym możesz jednocześnie wyewidencjonować inną gałąź zamiast zamiany gałęzi na miejsce.

Kiedy chcesz go usunąć, możesz to wyczyścić

git worktree remove [-f] <worktree>
grego
źródło