Сначала немного расслабьтесь: git branch --set-upstream-to=<em>target</em> <em>branch</em>
просто устанавливает в восходящем направлении ветви с именем branch
на target
. Восходящий поток ветки на самом деле ничего не меняет в ветке. Он изменяет некоторые отчеты и включает некоторые ярлыки.
Если вы действовали в отчетах , это имеет значение. Если нет, то это не так.
Если вы использовали ярлыки , это имеет значение. Если нет, то это не так.
К удалить восходящий канал ветви B:
git branch --unset-upstream B
Чтобы изменить восходящий канал ветви B на origin/B
:
git branch --set-upstream-to=origin/B B
Я думал, что это синхронизирует c мою ветку с мастером, но я каким-то образом поместил все изменения, которые у меня были в моей ветке, в ветку master.
Опять же, это зависит от любых других команд, которые вы выполнили. Опция --set-upstream-to
в git branch
не сделала ничего, кроме установки восходящего потока. Для обсуждения того, что такое восходящий поток - для чего он хорош - см. мой ответ на Почему я должен "git pu sh - set-upstream origin "?
Итак, мои вопросы: как мне вернуть все файлы на мастере в состояние, в котором они были при предыдущем коммите? Как правильно синхронизировать c, чтобы я мог получать обновленные изменения из master в свою ветку?
Сложно ответить на этот вопрос, потому что я думаю, что вы исходите из неверных предположений. Это очень легко сделать, потому что система ветвления Git, ну, в общем, прямолинейная странная . Если вы никогда ранее не пользовались какой-либо другой системой управления версиями, вы можете не найти это странным, но если это так, Git может стать большим шоком.
Людям нравится думать о ветвях как о чем-то реально , что-то solid: полезно, может быть неподвижно, как огромный валун, может укрыться как здание, и в любом случае solid и надежно. В других системах контроля версий они имеют тенденцию быть таким образом. В Git они не такие: они легкие, эфемерные, текучие и во многом почти бесполезные во многих отношениях. В основном они делают одну вещь для нас, и они делают это довольно хорошо, но они не делают большинство других вещей, которые мы ожидаем, что ветви сделают. Но здесь я говорю о ветвях имен , таких как master
и develop
и так далее. Слово branch в Git на самом деле неоднозначно: оно не всегда означает название ветви . Другое дело, что это довольно solid.
(Если вы никогда не использовали другие системы контроля версий, ни одна из вышеперечисленных не имеет большого значения, потому что у вас не будет предыдущие ожидания.)
Git - это все о коммитах
Немного забудь о ветвях здесь. Git не столько о ветвях. Мы используем их, конечно - они делают что-то полезное - но Git на самом деле все о коммитов . Коммит - это базовая c единица в Git, и это то, о чем вам нужно знать больше всего. Итак, давайте посмотрим, что такое коммит и что он делает.
Git коммит содержит данные - снимок всех ваших файлов - и метаданные: информация о коммит. Вот что в коммите: данные и метаданные; снимок файлов и некоторая информация о снимке, например, кто его сделал и когда. Передает , не удерживает изменения. Они просто хранят моментальные снимки и метаданные.
После того, как сделанный коммит не может быть изменен, он не может быть изменен. Ни одна часть любого коммита не может измениться. Все его файлы заморожены на все времена. Это имеет кучу полезных свойств. Например, некоторые люди возражают против того факта, что Git делает новый снимок каждого файла для каждого коммита. Не приведет ли это к тому, что хранилище станет ужасно толстым, ужасно быстрым? Что ж, если бы Git сделал это глупо, это было бы; но Git нет, поэтому нет.
Допустим, у вас есть сто файлов. Вчера вы сделали коммит со всеми 100 файлами. Вы только изменили два и сделали новый коммит только сейчас. 98 файлов в new соответствуют тем, которые были в предыдущем коммите. Что ж, мы только что сказали, что каждый коммит полностью заморожен, поэтому ваш новый коммит может просто поделиться всеми 98 неизмененными файлами. На самом деле нужно только сделать снимок двух разных файлов.
Замороженные файлы в каждом коммите дополнительно сжимаются, потому что они находятся в специальном, только для чтения, Git -только сформироваться. Они буквально неизменны в этой форме: их не могут изменить не только вы . Ни один не может Git сам. Это отлично подходит для архивирования, и это означает, что один ответ на один из ваших вопросов тривиален:
как я могу вернуть все файлы ... в состояние, в котором они были в предыдущем коммите
Они уже в этом состоянии, в этом коммите. Все, что вам нужно сделать, это использовать этот предыдущий коммит. Но прежде чем мы перейдем к этому, давайте закончим sh, чтобы разобраться с тем, что в них есть коммитов.
Каждый коммит получает уникальный номер. A Git коммит имеет ha sh ID , и когда вы делаете коммит, этот ha sh ID всегда зарезервирован для обозначения , что коммит. В некотором смысле, это было зарезервировано еще до того, как вы сделали коммит. Тем не менее, идентификатор ha sh для фиксации создается с помощью криптографии c ha sh для всех данных и метаданных фиксации, а метаданные включают точную секунду, в которую вы создаете фиксацию, поэтому мы я должен знать заранее когда вы собираетесь его создать, а также все остальное, что вы собираетесь в него вставить, чтобы предсказать его га sh ID.
Every Git везде соглашается, что , что commit получает , что ha sh ID. Это означает, что если вы подключите два Gits друг к другу, они могут просто посмотреть на идентификаторы ha sh друг друга: если у вашего Git есть свой ha sh ID H1 , у вашего Git есть свои совершить H1 . Если у них есть ваш га sh ID H2 , у них есть ваш коммит H2 . Везде, где у вас есть идентификатор sh, которого у них нет, или наоборот, у вас есть коммит, которого нет, или наоборот. 1 Это делает обмен коммитами довольно эффективным: ваш Git знает который совершает они имеют, и наоборот, просто посмотрев на некоторые га sh идентификаторы.
Последнее, но очень важное, что нужно знать, это то, что каждый commit хранит ha sh ID (или несколько ha sh ID) своего непосредственного предка : его parent (или родителей, в случае коммитов слияния) , Обратите внимание, что эта связь идет только в одну сторону, от дочерней к родительской. Это потому, что когда ребенок «рождается» - когда мы делаем новый коммит - мы знаем, каких родителей мы хотим использовать. Но когда мы делаем коммит, мы еще не знаем, какие у него будут sh идентификаторы в детстве. И каждая часть каждого коммита полностью, полностью, на 100% доступна только для чтения, поэтому мы не можем добавить потомок с sh идентификаторами позже.
1 Git использует идентификаторы ha sh для всех четырех своих внутренних типов объектов, а не только для фиксации, так что с технической точки зрения это немного не так. Сравнение просто га sh ID , а не коммит га sh ID ; у вас есть объект или нет, если у вас есть га sh ID или нет.
Итак, Git сохраняет коммитов , которые:
- представляют собой снимки всех ваших файлов, навсегда замороженные,
- плюс некоторые метаданные например, кто сделал коммит, когда, почему и т. д. c.,
- , и каждый из них имеет уникальный «номер» (га sh ID); и
- каждый указывает назад на своего непосредственного родителя, сохраняя ха sh ID родителя.
Эти обратные ссылки означают, что мы можем рисовать совершает. Давайте начнем с небольшого репозитория, содержащего всего три коммита:
A <-B <-C
Commit C
- это последний коммит, т. Е. Тот, который мы сделали совсем недавно. Он содержит идентификатор ha sh предыдущего коммита B
, поэтому B
является родителем C
: C
указывает на B
. Между тем B
содержит идентификатор ha sh предыдущего коммита A
: B
указывает на A
. Коммит A
был нашим самым первым коммитом. Нет более раннего коммита, на который можно было бы указать, так что это просто не так.
Здесь мы используем отдельные заглавные буквы, чтобы заменить коммиты. Но фактические идентификаторы ha sh большие и некрасивые и невозможны для людей , с которыми можно работать. Они должны быть большими, чтобы вы могли получить уникальный, отличающийся от любого другого коммита с sh ID, всегда, каждый раз, когда вы делаете новый коммит. Итак, как мы будем помнить, что коммит C
является последним коммитом?
Именно здесь имена ветвей входят в картину. В Git имя ветви типа master
просто содержит фактический га sh идентификатор последнего коммита:
A--B--C <-- master
(я перестал рисовать внутренние стрелки между коммитами в виде стрелок, потому что это становится слишком трудным. Просто помните, что все стрелки указывают назад ; Git работает в обратном направлении.)
Если у нас более одного имени ветви, каждое просто держит один га sh ID. Несколько имен могут содержать одинаковые га sh ID:
A--B--C <-- master, develop
Теперь нам нужен способ узнать, какое имя ветви нам нужно Git чтобы использовать, поэтому мы прикрепляем специальное имя HEAD
, в таком верхнем регистре, как это, к одному из названий ветвей (только по одному за раз):
A--B--C <-- master (HEAD), develop
Здесь мы используя commit C
, но on master
. Если мы теперь git checkout develop
, мы переключаемся на коммит C
, то есть мы ничего не переключаем, но мы также переключаемся на , используя имя develop
, так что мы develop
:
A--B--C <-- master, develop (HEAD)
Если мы сейчас сделаем новый коммит обычным способом - который я здесь не буду описывать - мы получим новый коммит с новый большой уродливый ха sh ID. Давайте просто назовем это D
. Commit D
получает новый снимок всех наших файлов, даже если он просто использует большинство из них из C
(и учтите, что если мы изменим файл back мы будем повторно использовать копию из более старого коммита, поэтому, возможно, D
повторно использует большую часть C
и повторно использует A
или B
для последних нескольких файлов). Он получает свои собственные метаданные, в том числе наше имя как человека, который его создал (коммиттер) и записал его (автора), 2 и сохраняет идентификатор C
ha sh в виде коммита. родитель. Это легко сделать, потому что имя ветки develop
содержит C
ha sh ID прямо сейчас.
Сделав коммит D
, теперь мы имеем :
A--B--C <-- master, develop (HEAD)
\
D
и теперь мы на последнем шаге git commit
, то есть: он просто записывает, какой у него есть sh ID, который он только что получил, для нового коммита в имя текущей ветви , т. е. ту, к которой прикреплено HEAD
. Это приводит к тому, что имя ветки move :
A--B--C <-- master
\
D <-- develop (HEAD)
Если мы сейчас git checkout master
и сделаем новый коммит, мы получим новый коммит E
, который будет указывать на C
, и имя master
переместится:
E <-- master (HEAD)
/
A--B--C
\
D <-- develop
Обратите внимание, что мы можем, если захотим, нарисовать коммит D
или E
на той же строке, что и первые три коммита. Важно то, что мы подключаем D
обратно к C
, E
обратно к C
и C
к B
к A
.
График и коммиты являются реальными и достаточно солидными - граф гибок, пока мы сгибаем его, не прерывая коммитов, - но имена ветвей являются просто метками. Мы можем в любое время сказать Git: снять метку develop
с коммита D
. Мы можем указать любой коммит, который у нас есть. Мы можем даже полностью удалить его, хотя, если мы сделаем это, у нас будет адское время поиск коммит D
- это га sh ID выглядит совершенно случайным, в конце концов.
Если мы не сможем найти D
, Git в конечном итоге удалит его. (Git имеет некоторые меры безопасности, чтобы найти временно заброшенные коммиты, обычно в течение как минимум 30 дней, в случае, если вы потерпите неудачу. ) Так что коммиты не обязательно навсегда . Но однажды сделанные, они не могут быть изменены . Пока у вас есть D
и вы можете найти его ha sh ID, он у вас есть, включая все его файлы снимков.
Обратите внимание, что мы можем найти коммиты A-B-C
в два способа на данный момент: мы можем начать с master
и найти E
и использовать это, чтобы найти C
, а затем B
и затем A
; или мы можем начать с develop
, найти D
и использовать это, чтобы найти C
, а затем B
и затем A
. Это означает, что коммиты A-B-C
находятся на обеих ветвях. (Это Git сильно отличается от большинства систем контроля версий.) Если мы удаляем имя develop
и не можем найти D
, мы все равно можем найти A-B-C
и теперь они ' только на 1358 * одна ветка. Или мы можем добавить новое имя:
E <-- master (HEAD)
/
A--B--C <-- three
\
D <-- develop
, и теперь коммиты A-B-C
находятся на трех ветвях, включая ветку с именем three
. Коммит C
является последним коммитом three
и является частью, но не последним из master
и develop
.
Это то, что он означает когда я говорю, что ветви не очень много значат. Они позволяют нам находить последний последний коммит, и оттуда мы находим все более ранние коммиты. Если у нас есть другой способ найти эти коммиты, единственное, что делает имя ветки, - это помнит, что какой-то коммит - например, C
- является подсказкой этой ветви.
2 Это разделение между автором и коммиттером позволяет Git работать с электронной почтой, когда кто-то просто отправляет вам по электронной почте исправления, которые вы применяете. Тогда вы коммиттер , а другой человек - автор . Linux люди использовали Git, как это было в первые дни Git, а иногда все еще делают.
Как вы все это используете
Проверка ветки имя означает выбрать коммит этой ветви как текущий коммит и выбрать эту ветку как текущую ветку . Git делает это следующим образом:
- присоединяя специальное имя
HEAD
к имени ветви; и - извлечение замороженных файлов только для чтения из выбранного коммита в область, где вы можете их просматривать и работать с ними.
Итак, если у вас есть:
...--F--J--K <-- master
\
G--H--I <-- branchname (HEAD)
и вы случайно сделали что-то для добавления new коммитов к branchname
, которые вам не нужны, например добавив коммит I
, вы можете просто принудительно Git сделать укажите имя branchname
обратно на существующий коммит H
:
...--F--J--K <-- master
\
G--H <-- branchname (HEAD)
\
I [abandoned]
Коммит I
задержится на некоторое время - вероятно, не менее 30 дней - но будет невидимым; ты этого не увидишь. Как только он слишком долго зависает, сборщик мусора для обслуживания, git gc
, в конечном итоге удалит его по-настоящему, и он действительно исчезнет. 3
Для этого вам нужно Git сказать принудительно переустановить имя branchname
, чтобы оно указывало на фиксацию H
вместо фиксации I
. Есть несколько Git команд, которые могут это сделать, но основной, которую вы обычно используете для этого, является git reset
. Это также уничтожает любую незафиксированную работу - ну, в зависимости от того, как вы ее используете - и незафиксированная работа не может быть восстановлена в течение 30+ дней или в течение любых дней В общем, так что очень осторожно с git reset --hard
:
git reset --hard <hash-of-H>
сделает то, что мы нарисовали выше, сделав коммит H
текущим коммитом и имя branchname
point совершить H
. Коммит I
все еще будет существовать, но его будет очень трудно найти снова. 4
3 Предполагается, что вы не позволили другим Git скопировать коммит I
по его sh ID в их хранилище, и таким образом захватить его и оставить под каким-либо именем по своему выбору. Если вы это сделаете, они могут позже ввести commit I
обратно в ваш репозиторий.
4 Если вы решите, что хотите вернуть его, обычный способ найти его га sh ID - это через Git s reflogs , которые отслеживают, что га sh идентификаторы были в каждом ref . Но это для другого вопроса StackOverflow.
Получение изменений в другую ветку
А также как правильно синхронизировать c, чтобы я мог получать обновленные изменения из master в мою ветку?
Помните, ветви не так уж важны. Это совершает , что имеет значение. И, коммиты являются снимками. Они не изменения! Однако вы можете превратить коммит в изменения. Выберите любые два соседних снимка - родительский и дочерний - и спросите Git: В чем разница с родительским снимком от дочернего снимка? То есть:
...--G--H <-- master (HEAD)
Если мы имеем Git извлекает снимки G
и H
в две временные рабочие области, затем сравнивает каждый файл в двух временных областях, и мы увидим, что изменилось . Вот что делают git log -p
, git show
и git diff
. Случай git log -p
берет каждый коммит, который он показывает, так как он возвращается назад, показывая один коммит за раз, и сравнивает своего родителя с ним - затем продолжает показывать родителя и так далее. Команда git show
берет один коммит для показа, показывает его, сравнивая с ним своего родителя, и останавливается. С git diff
вы можете присвоить ему любой два коммита га sh ID; он извлекает оба коммита и сравнивает их. 5 Он не смотрит на какие-либо коммиты между ними: вы просто выбираете левую и правую стороны и сравниваете. Вы можете сравнить ваш самый первый коммит с вашим последним, если хотите.
Две очень полезные команды Git:
git merge
, который находит three фиксирует и делает два git diff
с, а затем объединяет изменения; и git cherry-pick
, который фактически копирует коммит, сравнивая его с его родителем, чтобы сделать из него ряд изменений. 6
Когда использовать git merge
, когда использовать git cherry-pick
и когда и нужно ли использовать git rebase
- который, по сути, запускает для вас целый ряд git cherry-pick
команд - это еще одна топика c и довольно большой и часто самоуверенный. Я не буду go в этих деталях здесь. Давайте просто покажем настоящее слияние. Это происходит, когда у вас есть две ветви - именно здесь возникает проблема с неопределенностью Git со словом branch , которая выглядит следующим образом:
I--J <-- branch1, merge-me (HEAD)
/
...--G--H
\
K--L <-- branch2
Обратите внимание, что наша текущая ветвь имя merge-me
указывает на фиксацию J
, а branch2
указывает на фиксацию L
. Теперь мы можем запустить git merge branch2
. Git легко найдет коммит J
- это наш текущий коммит - и легко найдет L
, потому что имя branch2
указывает на L
. Затем Git найдет лучший общий коммит , который Git вызывает базу слияния . В некоторых случаях это неясно, но здесь лучший общий коммит - это, очевидно, commit H
(независимо от того, какой у него действительный идентификатор ha sh): commit H
включен оба ветки, и это самый последний такой, позже, чем любой другой коммит, который также есть в обеих ветвях.
Так что git merge
теперь будет git diff
коммит H
против J
, чтобы увидеть что мы изменились на нашу ветку. Также будет git diff
commit H
- база слияния, опять же - против commit L
, чтобы увидеть, что они изменили в их ветви. Тогда Git объединит эти изменения. Если мы изменили файл F в снимке H
на новую версию F в J
, Git примет наши изменения. Если они также изменили файл F в снимке H
на новую версию L
, Git примет , то их также изменится. Код слияния объединяет изменения, если это возможно, и применяет объединенные изменения к файлу F из снимка H
.
Это повторяется для каждого измененного файла. Для файлов, в которых никто ничего не менял - файл F2 в H
совпадает с файлом в J
и , файл в L
- Git может принимать любую из трех версий, поскольку все они совпадают. Для файлов, в которых только одна сторона изменила что-либо, Git может быть сокращен: он может просто взять наш или просто взять их.
Два различия запускаются с --find-renames
, чтобы искать файлы переименованы в , и файлы diff автоматически ищут файлы, которые были добавлены или удалены. Код слияния объединяет их тоже или, по крайней мере, так, как может.
Если наши изменения и их изменения перекрываются, но не совпадают, код слияния объявит слияние конфликт . В этом случае Git оставит все три копии входного коммита файла вокруг. 7 Git также напишет все возможное для объединения, с маркерами конфликта, где он имел неприятности, в вашу рабочую зону. Ваша работа становится разрешать эти конфликты путем получения правильного результата слияния. (Затем вы должны продолжить / завершить sh слияние самостоятельно.)
Если Git не обнаружит никаких конфликтов тем не менее, Git включит go, чтобы сделать новый коммит из результата. Помните, что это результат объединения всех изменений и применения комбинированных изменений к тому, что было в базе слияния - снимке H
в нашем пример:
I--J <-- branch1
/ \
...--G--H M <-- merge-me (HEAD)
\ /
K--L <-- branch2
Новый коммит M
- это коммит слияния: указывает на J
, как любой коммит, но также на L
, другой коммит это слилось. (Тот факт, что слияние использовало H
в качестве базы слияния, нигде не записано. Если вы спросите Git , что было базой слияния? Git, то просто придется снова разобраться, так же, как он понял это до того, как M
существовал.) Новый коммит M
заставил текущую ветвь продвинуться, чтобы указать на нее, так же, как всегда делают новые коммиты. Коммит M
имеет снимок, а не изменения, как и любой другой коммит. Особенностью в M
является то, что у него два родителя вместо одного. Когда Git работает в обратном направлении по коммитам, по одному, он должен go с M
до и J
и L
. 8
5 Из-за внутреннего формата хранения Git фактически не нужно ничего извлекать, пока не дойдет до точки, где два файла действительно не соответствует. В общем, ему не нужна временная рабочая область, он просто делает все это правильно в памяти.
6 Внутренне Git фактически реализует git cherry-pick
, используя те же три способ слияния, выполняемый git merge
, но с родительским элементом коммита-копии, как base merge . После этого вместо коммит слияния - коммит с двумя родителями - git cherry-pick
делает обычный коммит с одним родителем. Но это объясняет, почему перебазировка сложнее, чем слияние: если вы перебазируете цепочку из пяти коммитов, вы фактически делаете пять слияний вместо одного.
7 Они находятся в Git index aka области подготовки , в которую я go не вошел.
8 Это создает некоторые интересные проблемы, так как Git собирается по одному. Но опять же, это для других сообщений.