Что произойдет, если я перенесу изменения из одной ветки в другую?
У меня есть две ветви в моем проекте.
main
и test_deploy
Неправильно переносить изменения без тестов в основную ветку, но не важно.
Я использую ветку main
для внесения некоторых изменений локально и ветку test_deploy
, где у меня другие изменения в settings.py (например, DEBUG=False, использование облака для хранения изображений моделей и так далее) и файлы для развертывания (Procfile, runtime.txt и так далее).
Например, я собираюсь добавить новое приложение и отправить его в ветку main
и в ветку test_deploy
(чтобы получить самую новую версию моего проекта)
У меня вопрос. Что произойдет, если я зафиксирую эти изменения в ветке main
, перенесу их в эту ветку и также перенесу их в ветку test_deploy
? Возникнут ли у меня конфликты, которые мне придется исправлять вручную? Или я должен сначала вытащить все файлы из ветки test_deploy
, а затем вытолкнуть их? (Я не хочу вытаскивать файлы из ветки test_deploy
. Вот почему я задал этот вопрос)
Summary
В общем, я хочу перенести изменения из ветки main
в ветку test_deploy
, но не вытаскивая отдельные файлы из ветки test_deploy
, потому что они излишни в основной ветке, которую я использую локально.
TL;DR
Вы вероятно не сможете сделать это так, как вы хотите. Возможно, вы захотите использовать cherry-pick, как прокомментировал Александр.
Long
Если вы думаете о Git'е как об изменениях, файлах и/или ветках, это приведет вас по садовой дорожке. Не делайте этого: думайте о Git'е как о коммитах, потому что это так. Каждый коммит:
Is numbered. A commit has a unique hash ID. This is a (very large, random-looking but not random at all) number expressed in hexadecimal. Once Git has assigned that number to your new commit, it is now off limits for every other commit ever.1 If any other Git repository claims to have that hash ID, they must have your commit, not some other commit.
Is read-only: no part of any commit (any internal Git object, really) can ever be changd.
Stores two things: a full snapshot of every file (but compressed and Git-ified and, crucially, de-duplicated so that even if a million commits use a large file there's only one copy of that large file), and metadata. The metadata generally include the raw hash IDs of other commits (usually exactly one, the previous or parent commit).
Is found by its hash ID, which means you must provide Git with the raw hash ID, or with something Git can use to find the raw hash ID.
Имя ветви branch name предоставляет Git'у необработанный хэш ID, поэтому имя ветви позволяет Git'у найти один конкретный коммит. Затем этот коммит предоставляет хэш-идентификатор предыдущего коммита, так что Git может найти два коммита. Предыдущий коммит предоставляет свой хэш-идентификатор предыдущего коммита: это означает, что Git может найти три коммита. Третий коммит предоставляет другой хэш ID, и так далее и далее.
1 Это не-не может быть-правдой всегда, но если любой другой Git-репозиторий использует тот же номер для различного коммита, вы никогда не сможете успешно получить из вашего Git'а или отправить в этот другой Git. Большой размер хэш-идентификаторов делает это правдоподобным на миллионы er тысячи urk десятки лет (SHA-1 уже недостаточно велик, и Git переходит на SHA-256).
Использование ветвей
Я использую ветку
main
для внесения некоторых изменений локально ...
На самом деле это означает, что вы используете имя main
для поиска определённого коммита - последнего коммита "на" вашем main
. Вы заставляете Git извлечь файлы из снапшота этого коммита, чтобы вы могли прочитать их - помните, что только Git может читать Git-фицированные копии внутри снапшота - и записать их (внести свои изменения).
Если и когда вы делаете новый коммит, Git будет:
- save the updated files;2
- save some metadata saying that you made this new commit;
- include the current (latest-on-
main
) commit hash ID as the parent of the new commit; - write all of this out once, to a frozen snapshot copy that can never be changed at all; and
- thereby allocate a new unique hash ID, which Git now writes into your name
main
.
Самым важным является сам акт создания нового коммита. Именно тот факт, что Git сохраняет хэш ID нового коммита под вашим именем main
, "изменяет вашу ветку": ваш последний коммит теперь является вашим новым коммитом, родителем которого является тот, который был вашим последним коммитом минуту назад. Вот так ветвь "растёт", по одному коммиту за раз.
Последнее, что здесь нужно понять, это то, что Git является распределенным: у вас в вашем репозитории есть все коммиты, которые вы получили из какого-то другого репозитория Git, которые вы находите по именам ваших веток, или иногда по именам удаленного отслеживания, таким как origin/main
. У них в репозитории Git есть все коммиты, которые у них есть, которые они находят по именам своих ответвлений. Их имена ветвей - это их имена; ваши имена ветвей - это ваши имена. Поддерживать их синхронизацию (делать ли это вообще, и если делать, то как часто) - ваше дело: ваш Git копирует их branch имена в ваши remote-tracking names и будет обновлять их автоматически, но ваш Git не обновляет их branch имена.
Пока вы делаете новые коммиты и обновляете имена ваших веток, в другом репозитории Git ничего не происходит - пока. Вот где может пригодиться git push
:
В общем, я хочу перенести изменения из ветки
main
вtest_deploy
Отметьте здесь важный момент: существует не одна (the) main
ветка. Есть ваше имя main
(в вашем Git-репозитории), их имя main
(в их Git-репозитории), и ваше имя origin/main
(в вашем Git-репозитории, помнящем - всякий раз, когда вы соединяете свой Git с их Git и заставляете свой обновлять память вашего Git - то, что есть в их репозитории).
Что общее между вашими двумя Git-репозиториями - это не имена, а скорее коммиты. Когда вы впервые клонировали их репозиторий, вы получили все коммиты и ни одной ветки: ваш Git превратил все их имена ветвей в имена удаленного отслеживания. Затем, в конце, ваш Git создал одно новое имя ветки, используя ID хэша коммита, который ваш Git хранил под вашим remote-tracking именем: довольно долгий путь, чтобы сделать имя ветки с одинаковым именем, держа одинаковый ID хэша, но это именно то, что он сделал. Это не одна ветвь, это две: обе имеют имя main
, но они находятся в двух разных репозиториях Git. Добавление коммитов в вашу main
не добавляет никаких коммитов ни в их main
, ни в их test_branch
.
2 Вы должны сначала использовать git add
на каждом обновленном файле, по причинам, которые не рассматриваются в этом ответе. Вы можете использовать git commit -a
, чтобы отложить изучение этих причин, но это оставит вас в неведении относительно ряда других важных вещей Git, поэтому не откладывайте это надолго.
Рисование ветвей
Прежде чем мы поговорим о том, как работает git push
, давайте нарисуем несколько ветвей. Вместо гигантских шестнадцатеричных чисел давайте воспользуемся заглавными буквами для обозначения хэш-идентификаторов коммитов: они гораздо удобнее для человека. (Конечно, мы быстро исчерпаем их, поэтому в Git'е используются большие числа, но мы сохраним наши нарисованные ветви маленькими). Мы начнем с main
и test_branch
, которые нарисуем следующим образом:
...--G--H <-- main
\
I <-- test_branch
Вот как могут выглядеть вещи в репозитории their до того, как вы его клонируете. Обратите внимание, что последний коммит на main
- H
, а последний коммит на test_branch
более поздний - I
, но родитель I
- H
. Это означает, что все коммиты на main
также находятся на test_branch
. Есть только один более новый коммит на test_branch
, чем на main
.
Когда вы git clone
этот репозиторий, вы получаете только main
, а не test_branch
: ваш Git изменяет их main
на ваш origin/main
, изменяет их test_branch
на ваш origin/test_branch
, а затем создает ваш собственный main
из их main
:
...--G--H <-- main, origin/main
\
I <-- origin/test_branch
Обратите внимание, как два имени идентифицируют коммит H
. Это нормально! У вас может быть сколько угодно имен, указывающих на один коммит. Вы можете, если хотите, создать новую ветвь , указывающую на коммит test_branch
:I
...--G--H <-- main, origin/main
\
I <-- test_branch, origin/test_branch
Акт checking out или switching to main
указывает вашему Git-репозиторию извлечь снимок из коммита H
- того, на который указывает имя main
- и сделать имя main
текущей веткой , которую мы можем нарисовать, добавив слово рядом с HEAD
. Команда main
делает это с помощью цветного текста (git log
, особенно в выводе HEAD -> main
); мне нравится делать это с помощью git log --decorate --oneline --graph
в круглых скобках:main
...--G--H <-- main (HEAD), origin/main
\
I <-- test_branch, origin/test_branch
Теперь вы изменяете некоторые файлы в своём рабочем дереве - куда Git помещает пригодные для использования копии - и, возможно, выполняете некоторые тесты или что-то ещё, а затем git add
и git commit
, как обычно. Это создаст новый коммит, который заставит ваше имя main
указывать на новый коммит. Новый коммит указывает обратно на существующий коммит H
, вот так:
J <-- main (HEAD)
/
...--G--H <-- origin/main
\
I <-- test_branch, origin/test_branch
Обратите внимание, что больше ничего не произошло: другие имена не нарушены, и ни один существующий коммит не изменился: H
по-прежнему указывает назад на G
, I
по-прежнему указывает назад на H
, и новый J
указывает назад на H
тоже.
Теперь мы можем поговорить о том, как работает git push
. Помните, что git push
вызывает другой набор Git-программ, которые работают на другом Git-репозитории. Мы будем использовать короткое имя origin
, которое хранит URL, по которому ваша программа Git обращается к этой другой программе Git. Вы можете выполнить:
git push origin ...
где мы заполним часть ...
через некоторое время. Ваш Git вызовет их Git, и они поговорят о коммитах по хэш-идентификаторам. Ваш Git расскажет им о новом коммите J
, которого у них не будет, а затем расскажет им о родительском коммите J
H
, который у них есть . Таким образом, они скажут пожалуйста, пришлите J
, но мне не нужен H
, так как у меня есть этот и все предыдущие коммиты тоже . Теперь ваш Git может разработать минимальный пакет, который доставит им все необходимое для повторного создания коммита J
в полном объеме, с тем же хэш-идентификатором коммита J
в их репозитории.
Поэтому в итоге получается следующее:
J [no way to find it yet]
/
...--G--H <-- main
\
I <-- test_branch
Помните, у них нет никаких origin/
имен, только названия их ветвей.
Последним шагом вашего git push
будет обращение вашего Git'а к их Git'у с просьбой установить одно из имен их ветвей. Вы можете выбрать существующее имя -main
или test_branch
, например, или совершенно новое для них имя. Вы попросите свой Git послать им вежливый запрос: Пожалуйста, если можно, создайте или обновите ваше имя ________, чтобы оно указывало на коммит J
. Дайте мне знать, если всё в порядке.
Ваш Git заполнит хэш ID коммита J
здесь, основываясь на том, что вы передадите команде git push
. Ваш Git заполнит пробел для имени, основываясь на том, что вы указали в той же команде git push
. Это определяет, что входит в часть ...
:
git push origin main:new_branch
отправит им коммит J
, потому что в части слева от main:new_branch
написано main
, что означает коммит J
в вашем репозитории, где main
указывает на J
. Он заполнит пробел именем new_branch
, потому что оно находится справа от двоеточия в main:new_branch
.
Если вы просто выполните:
git push origin main
ваш Git будет использовать единственное имя здесь-main
, чтобы найти коммит в вашем репозитории (коммит J
), а также чтобы заполнить пустоту для имени в их репозитории (main
). Таким образом, это попросит их добавить коммит J
в их main
.
Вы хотите, чтобы они установили свои test_branch
, чтобы они указывали на фиксацию J
, однако, поэтому вы можете использовать:
git push origin main:test_branch
Это просит их установить свое имя test_branch
для указания на коммит J
.
Но что, если бы они это сделали? Теперь у них было бы:
J <-- test_branch
/
...--G--H <-- main
\
I [lost!]
У них больше нет способа найти коммит I
. Они не могут использовать свой main
:, который указывает на коммит H
. Они не могут использовать свое имя test_branch
, если они обновят его, потому что оно будет указывать на J
, которое ссылается обратно на H
, которое ссылается обратно на G
и так далее. Никто не ссылается вперед (никогда, в Git), и поэтому нет способа найти коммит I
.
Короче говоря, на вашу вежливую просьбу они ответят no. Вы получите ошибку ! rejected
и non-fast-forward
от вашего git push
, что на жаргоне Git'а означает "если я сделаю это, я потеряю несколько коммитов".
Что вы можете сделать?
Проблема здесь не в названиях ветвей как таковых, а скорее в том, что ваш новый коммит J
ссылается на существующий коммит H
, и это то, что неправильно. Вам нужен новый коммит, который ссылается обратно на существующий коммит I
- последний на их test_branch
.
Обратите внимание, что перед тем, как начать делать что-либо из этого, вам желательно выполнить:
git fetch origin
На этом шаге ваш Git вызывает их Git. Они перечисляют все имена своих ветвей и ID хэшей коммитов - то же самое, что они делали, когда вы впервые запустили git clone
, и теперь ваш Git может выяснить, есть ли у них коммиты, которых нет у вас. Например, возможно, кто-то добавил новый коммит K
на test_branch
, так что теперь у него есть:
...--G--H <-- main
\
I--K <-- test_branch
<<<Если да, то ваш Git получит от них новый коммит - ваш Git может определить, что он новый для вас, потому что у вас нет коммита с таким номером - и затем обновит ваш K
- вашу память об их origin/test_branch
, чтобы соответствовать.test_branch
Сейчас (после обновления) самое время создать свой собственный test_branch
, указывающий на тот же коммит, что и последний коммит на их test_branch
. Если они всё ещё на коммите I
, хорошо. Если они добавили коммит K
, или даже десятки коммитов, тоже хорошо. В итоге вы получите свой собственный test_branch
, указывающий на тот же самый коммит , что и их test_branch
, который ваш origin/test_branch
идентифицирует после git fetch
.
В любом случае, что бы у них ни было сейчас, вы можете копировать ваш существующий коммит J
на новый и немного другой коммит - назовем его J'
, чтобы показать, насколько он похож на J
- который добавляется туда, где заканчивается их test_branch
. Сейчас вы запустите git switch test_branch
, чтобы создать его, или git switch test_branch && git merge --ff-only origin/test_branch
, чтобы обновить его, или, возможно, даже git switch test_branch && git reset --hard origin/test_branch
, чтобы обновить ваши test_branch
:
J <-- main
/
...--G--H <-- origin/main
\
I--K <-- test_branch (HEAD), origin/test_branch
Теперь вы выполняете:
git cherry-pick main
, который направляет ваш Git найти коммит J
, найденный по вашему имени main
, и сделать всё возможное, чтобы скопировать этот коммит. Копирование коммита подразумевает выяснение того, что изменил коммит, т.е. выполнение команды git diff
, а затем применение этого изменения там, где вы сейчас находитесь (на коммите K
на рисунке выше), и создание нового коммита из результата. Технически, cherry-pick - это полное трёхстороннее слияние, но вы можете, если хотите, думать об этом как о процессе "diff and patch", и вы не будете слишком далеки от истины.
При условии, что все идет хорошо, конечный результат выглядит следующим образом:
J <-- main
/
...--G--H <-- origin/main
\
I--K <-- origin/test_branch
\
J' <-- test_branch
А git show main
, который показывает разницу между снимками в H
и J
, и git show test_branch
, который показывает разницу между снимками в K
и J'
, покажет одинаковый набор изменений (при необходимости скорректированный) для одинаковых файлов, измененных в H
-vs-J
.
Теперь вы готовы к работе:
git push origin test_branch
который использует ваше имя test_branch
для поиска коммита J'
, отправляет этот коммит на origin
- у них уже есть все, что идет перед J'
, поэтому вы просто отправляете один коммит - а затем просите их добавить коммит J'
к к их test_branch
. Поскольку это добавляет коммит к K
, они, вероятно, примут эту вежливую просьбу.