Что оправдывает разницу между моим локальным кодом и моими ветвями? - PullRequest
1 голос
/ 17 октября 2019

У меня сегодня была странная ситуация, и я до сих пор не могу понять причину, почему Git имел такое поведение.

У меня было branch A, что у меня merged с master.

В течение merge branch A получил deleted:

git merge --no-ff branch A
git branch -D branch A

I reverted master до его состояния до branch A merge и воссоздал branch A под именем branch B исбросьте его до его последнего коммита:

git revert merge_commit_id -m 1
git checkout -b branch B
git reset --hard branch_A_commit_id

Теперь с теми шагами, которые я сделал на своем локальном:

  • master, как это было до всего и когда я тестировалв моем локальном окружении все было как прежде.

  • branch B изменились так, как branch A было раньше всего, при тестировании в моем локальном окружении все было так же, как раньше, однако ничто не отличалось отmaster для Git.

Я добавил некоторые изменения в branch B и перенес их в удаленный репозиторий, и только недавно добавленные изменения отличались от master, хотя все было иначев моей локальной среде (у меня были все изменения с branch A и branch B).

Итак, у меня было всемои изменения локально в branch B, я не смог их увидеть в git status, ни зафиксировать, ни протолкнуть их, поскольку Git ведет себя так, как будто они не отличаются от master, мои удаленные и локальные ветви были синхронизированы и обновлены.

Выполнение git pull origin branch B не изменило мой код в локальной сети. Нажатие также ничего не даст.

Я вернул свои изменения с помощью обходного пути, но я хотел бы понять, что здесь произошло с Git, и что будет лучшим способом вернуть мой старый branch A, еслитакая ситуация повторяется?

Я думаю, что изначально branch A был удален во время слияния, поэтому коммит, который я использовал для сброса branch B, фактически ссылался на старую версию master, которая моглаобъясните поведение, так как Git предполагает, что мои изменения - это старая версия master, а не фактические изменения с branch A.

1 Ответ

2 голосов
/ 18 октября 2019

В этом случае задействованы два репозитория Git. Один репозиторий Git находится в Bitbucket, а другой - на вашем локальном компьютере (git clone <em>url-of-bitbucket-repo</em> создает второй репозиторий Git). Два репозитория будут совместно использовать коммитов , что определяется по хэш-идентификаторам коммитов, но каждый репозиторий Git имеет свой собственный набор имен веток . Поэтому то, что вы увидите в ветвях, зависит от , какой репозиторий вы просматриваете .

Основы

Прежде чем мы перейдем к процессу, или "что случилось", давайте начнемс этими пунктами маркированного списка (многие из которых могут быть вам знакомы, если они есть, не стесняйтесь переходить к следующему разделу):

  • Коммиты фактически идентифицируются по их уникальному хэшуидентификаторы. В то время как каждый коммит получает свой собственный уникальный хэш-идентификатор, отличный от любого другого коммита, каждый Git также вычисляет хэш-идентификатор таким же образом. Таким образом, каждый отдельный репозиторий Git использует одинаковые хэш-идентификаторы для одинаковых коммитов. Это означает, что два Git могут легко определить, имеют ли они коммиты друг у друга, сравнивая хеш-идентификаторы.

    (Чтобы это работало, никакая часть какого-либо коммита никогда не может быть изменена после его создания. Хеш-идентификатор основан на всехданных и метаданных в фиксации, и если хотя бы один бит в одном файле или одна буква в сообщении журнала будут изменены, это приведет к аннулированию хеш-идентификатора. Если вы попытаетесь изменитькоммит, в итоге вы делаете новый коммит с новым и другим хеш-идентификатором. Старый коммит все еще существует!)

  • Каждый коммит хранитполный снимок всех файлов. Вы можете просмотреть снимок только как «изменения в некоторых файлах» путем , сравнивая этот снимок с другим снимком.

  • Каждый коммит также хранит некоторые метаданные: имя и адрес электронной почты автора коммита, а также, например, отметку даты и времени. (На самом деле их два, один для «автора» и один для «коммиттера».) Ваше сообщение журнала фиксации также является частью этого.

  • Каждый коммит также хранит хэшID (ы) его непосредственных предшествующих коммитов, которые Git называет родителей этого коммита. (Это также часть метаданных.)

  • Имена ветвей просто содержат хэш-идентификатор одного отдельного коммита. Этот коммит должен рассматриваться как последний коммит в ветви.

  • В общем, чтобы сделать новый коммит, вы или Git пришлисоздайте новый снимок файлов, запишите этот снимок, добавьте соответствующие метаданные и создайте новый коммит. Это никак не влияет на любые существующие коммиты: новый коммит получает новый уникальный идентификатор хеша. родительский коммит нового коммита должен быть хеш-идентификатором некоторых существующих, действительных коммитов в этом хранилище.

    Так как имя ветки является последним коммитом вветвь, добавляющая новый коммит, заставляет Git обновлять текущее имя ветки - ветку, которую вы извлекли с git checkout, чтобы теперь она идентифицировала новый коммит. Родитель нового коммита - это коммит, который вы извлекли только минуту назад.

    Все это работает, потому что новый коммит ссылается на существующий коммит, который ссылается на его родителя, и так далее. Мы добавляем коммитов в ветку, убедившись, что новый коммит запоминает своего родителя. Если новый коммит не помнит своего родителя, и нам нужно было установить имя ветви, чтобы он запомнил новый коммит, предыдущий коммит, в лучшем случае, будет очень трудно найти, потому что хэш-идентификаторы выглядят очень случайно.

  • Если у вас есть два репозитория, коммиты передаются через git fetch и git push. Обратите внимание, что git pull запускает git fetch, за которым следует вторая команда Git, обычно git merge, согласно теории, что если вы только что получили несколько новых коммитов из их Git, вы, вероятно, захотите интегрировать ихкаким-то образом фиксирует ваши имена филиалов. Ваши имена ветвей остаются вашими, а git fetch сам по себе их не касается. Это команда second , которая касается одной из них.

  • Чтобы ваш Git запомнил хэш-идентификаторы, которые их Git вызывает "«Последний коммит в ветви», шаг git fetch сначала получает любые новые коммиты из какого-либо другого репозитория, затем создает или обновляет имена удаленного отслеживания для этого репозитория. Например, если ваш Git вызывает Git origin и у него есть ветвь master, ваш Git будет использовать ваш origin/master, чтобы запомнить, что их Git говорит, что их master.

    Другими словами, выборка заканчивается настройкой ваших Git's remote-tracking имен: например, ваш origin/master может измениться, но ваш собственный master никогда не меняется. Ваш origin/branch-A может быть создан, изменен или даже удален , но ваш branch-A никогда не изменится.

  • Помимо направления передачи, git push сильно отличается от git fetch одним очень важным способом. В конце git push, ваших запросов Git или даже команд (git push --force), чтобы их Git установил одно из их ответвлений . Вам не нужно, чтобы они устанавливали свои имена для удаленного слежения! Они могут даже не иметь имен для удаленного слежения: для серверных серверных репозиториев довольно редко иметь их.

Рисование фрагментов графика фиксации и слияние

Каккак мы отмечали выше, каждый коммит хранит хеш-код (ы) своих непосредственных предшественников. Мы, или Git, можем использовать это для группировки наших коммитов в граф. (Более конкретно это D irected A циклический G raph или DAG, хотя здесь мы не будем вдаваться в подробности.) Мы находим последний идентификатор хеша коммита - назовем его H - читая имя ветки как master. Имя содержит идентификатор хеша, поэтому мы говорим, что master указывает на commit H:

              H   <-- master

Но H также содержит идентификатор фиксации: идентификатор хешародителя H. Давайте назовем это G. H затем указывает на G. Конечно, G также указывает на что-то:

        <-G <-H   <-- master

Давайте назовем коммит, на который G указывает, F:

... <-F <-G <-H   <-- master

Это повторяется вплоть досамый первый коммит, который - будучи первым коммитом - нигде не указывает. Git называет это root commit , и именно здесь, когда последующие коммиты в обратном направлении, как это, мы должны остановиться. (Обычно мы останавливаемся раньше, когда устаем. :-))

Эта обратная цепочка, построенная из коммитов, образует граф фиксации. Это также означает, что мы можем определить тест «является предком» для любой данной пары коммитов. Например, G является родителем H, поэтому G является предком H. Между тем F является родителем G, поэтому F также является предком H.

Как только мы начнем добавлять new commits, хотя ... хорошо,посмотрите на этот частичный график:

          I--J   <-- branch1
         /
...--G--H
         \
          K--L   <-- branch2

Здесь имя branch1 идентифицирует коммит J, чей родитель I, чей родитель H. Так что H является предком J. Имя branch2 идентифицирует коммит L, чей родитель K, чей родитель H. Так что H также является предком L. Но I является не предком L. I также не является потомком L.

Чтобы выполнить слияние, мы выбираем одну из этих двух ветвей - например, git checkout branch1 - и затем выполняем слияние на другой: git merge branch2,Git будет:

  • найти тлучший общий предок (H), который Git называет базой слияния;
  • выяснит, что изменилось в branch1, сравнив базу слияния с коммитом tip branch1то есть J;
  • выяснить, что изменилось в branch2, сравнив базу слияния с верхушкой branch2, то есть L
  • объединить изменения, применяя объединенные изменения к снимку в H, чтобы создать новый коммит слияния M, который имеет оба коммитовкак его два родителя.

Это дает нам:

          I--J
         /    \
...--G--H      M   <-- branch1 (HEAD)
         \    /
          K--L   <-- branch2

как наш результат. (Имя branch1 - это то, которое было обновлено, потому что мы проверили его. Чтобы показать, что это ветвь, которую мы извлекли, я добавил (HEAD) к метке ветки.) Работая в обратном направлении от merge коммит как M - это вопрос о обоих его родителях, поэтому git log из branch1 покажет коммит M, затем все четыре коммита I + Jи K + L (в некотором порядке), а также фиксирует H и более ранние.

Обратите внимание, что в некоторых случаях объединение может быть выполнено тривиально:

...--G--H   <-- branch1 (HEAD)
         \
          I--J   <-- branch2

Здесь git merge branch2 не нужно совершать реальное слияние. Фиксация базы слияния H равна вершине branch1, и сравнение H с H всегда не покажет никаких изменений. Результат объединения ничего с чем-либо всегда является «чем-то», поэтому конечный результат всегда будет соответствовать commit J. Git может быть ленивым и просто переместить имя branch1 вперед и проверить фиксацию J:

...--G--H
         \
          I--J   <-- branch1 (HEAD), branch2

Git называет это ускоренным слиянием , даже если нет фактическогослияние: это действительно просто git checkout, который перетаскивает название ветви вместо переключения на другую ветвь.

Вы можете вызвать реальное слияние, даже в ситуации быстрой перемотки вперед, используя git merge --no-ff,Результат выглядит следующим образом:

...--G--H------K   <-- branch1 (HEAD)
         \    /
          I--J   <-- branch2

, где commit K - это новый коммит слияния.

Во всех трех случаях мы могли бы удалить имя branch2 безопасно после этого, потому что мы можем найти, какой коммит был вершиной branch2, взглянув на вершину branch1 и - при необходимости - работать в обратном направлении.

Это, вероятно, то, что произошло во время вашего слияния

Предположительно, веб-интерфейс Bitbucket для выполнения слияния аналогичен GitHub (я не использовал Bitbuckets). Вы выбираете одну ветвь, такую ​​как master, как ту, которая «получит» слияние, а затем выбираете другую ветку, например, branch-A, как ту, которая должна объединиться. Под обложкой веб-страницы Bitbucket делает эквивалент git merge или git merge --no-ff. Но тогда, как вы сказали, Bitbucket также удалил имя branch-A.

Давайте нарисуем, что:

          I--J   <-- master (HEAD)
         /
...--G--H
         \
          K--L   <-- branch-A

становится:

          I--J
         /    \
...--G--H      M   <-- master (HEAD)
         \    /
          K--L

или, может быть:

          I--J   <-- branch-A
         /
...--G--H   <-- master (HEAD)

становится:

          I--J
         /    \
...--G--H------M   <-- master (HEAD)

Revert

A git revert работает путем добавления нового коммита, который «отменяет» все, что произошло ввзять на себя обязательствоКак для cherry-pick, так и для возврата Git сравнивает один коммит со своим родителем (в единственном числе), чтобы увидеть, что произошло в этом коммите. Затем Git пытается повторно выполнить (cherry-pick) или un-do (вернуть) изменения, внесенные против текущего коммита. 1 Если это удается, Git создает нового одного из родителейcommit.

Например, рассмотрите возможность выбора коммита C, в то время как вы зафиксировали коммит H:

...--B--C--...   <-- other-branch
  \
   o--H   <-- current-branch (HEAD)

Git будет сравнивать снимки в B(родитель) и C (ребенок), чтобы увидеть, что изменилось в C. Применение этих же изменений к снимку в H дает нам новый коммит I:

...--B--C--...   <-- other-branch
  \
   o--H--I   <-- current-branch (HEAD)

Разница между снимками в H и I будет одинаковой (за исключением, может быть, дляномера строк) как разность между снимками в B и C.

Восстановление просто пытается отменить изменения, а не копировать их. В обоих случаях, если вы используете коммит слияния, он имеет двух родителей, поэтому вы должны выбрать одного из двух, используя дополнительный аргумент -m для git cherry-pick или git revert.


1 Технически, Git фактически выполняет полное трехстороннее слияние, используя родителя выбранного коммита в качестве базы слияния, текущий коммит в качестве левой части слияния и выбранный коммит в качестве правогосторона слияния. Результирующий коммит является обычным коммитом, а не коммитом слияния, поэтому он записывает только текущий коммит как родитель (новый) коммита.


Выполнение возврата

ВВ этом случае вы сделали откат, поэтому давайте нарисуем его. Но подождите секунду, вы сделали возврат в своем собственном репозитории, а не в хранилище Bitbucket! Давайте нарисуем ваш репозиторий, после того как вы запустите git fetch и git merge, илиgit pull чтобы сделать оба за один выстрел.

Вы начинаете с:

          I--J   <-- master (HEAD)
         /
...--G--H
         \
          K--L   <-- branch-A

Теперь вы запускаете git fetch и получаете коммит M:

Вот чтоВы сделали в этом случае, поэтому давайте нарисуем это:

          I--J   <-- master
         /    \
...--G--H      M    <-- origin/master
         \    /
          K--L   <-- branch-A

Тот факт, что branch-A пропал в Git over Bitbucket не влияет на ваш branch-A. Ни одно из названий вашей ветки не изменится! Самое большее, если бы у вас был origin/branch-A, ваш git fetch удалил бы origin/branch-A.

Если вы удалите свой собственный branch-A сейчас, это ваша ответственность. Но давайте скажем, что вы делаете, и что у вас также есть быстрый перемотка вперед master, чтобы указать M. Возможно, вы git checkout master, а затем git pull, чтобы получить M, обновите свой origin/master, а затем перемотайте вперед master все в одну команду, затем вы удалите свой собственный branch-A, увидев origin/branch-A уйти:

          I--J
         /    \
...--G--H      M    <-- master (HEAD), origin/master
         \    /
          K--L

Теперь вы запускаете git revert -m 1 <hash>, поэтому давайте нарисуем график. Мы прекратим рисовать origin/master (он по-прежнему указывает на M):

          I--J
         /    \
...--G--H      M--N   <-- master (HEAD)
         \    /
          K--L

Здесь N отменяет изменения, внесенные цепочкой K-L (ну, предположительно - вымог отменить изменения, внесенные цепочкой I-J, используя git revert -m 2). В любом случае имя master теперь указывает на фиксацию N.

Повторное создание ветки-A

Давайте немного повторим:

Я вернулсяmaster до его состояния перед ветвью A, слияние и воссоздание ветки A под именем B и сброс его до последнего коммита:

git revert merge_commit_id -m 1
git checkout -b branch B
git reset --hard branch_A_commit_id

График до коммита N представляет ваше состояние послеgit revert. Шаг git checkout -b branch-B просто создает новое имя branch-B, указывающее на текущий коммит (N), и присоединяет HEAD к этому новому имени:

          I--J
         /    \
...--G--H      M--N   <-- master, branch-B (HEAD)
         \    /
          K--L

Ваш git reset --hard сбрасываетсяиндекс и рабочее дерево (которое мы не рисуем!) и перемещает имя , к которому прикреплено HEAD , т. е. branch-B, к указанному вами хеш-идентификатору. Предположительно, это хеш-идентификатор commit L:

          I--J
         /    \
...--G--H      M--N   <-- master
         \    /
          K--L   <-- branch-B (HEAD)

Я добавил некоторые изменения в ветку B и перенес их в удаленный репозиторий ...

Изменениефайлы в рабочем дереве, добавление их в индекс и фиксация дадут вам новый коммит:

          I--J
         /    \
...--G--H      M--N   <-- master
         \    /
          K--L--O   <-- branch-B (HEAD)

Затем вы можете git push origin branch-B (возможно, также с -u) отправить новый коммитO другому Git и заставьте их создать их имя ветви branch-B, указывающее на новый коммит O.

, и только недавно добавленные изменения отличались отмастер, даже если все было по-другому в моей локальной среде (у меня были все изменения от ветви A и ветви B).

Здесь все идет не так, как надо: посмотреть изменения , вы должны сравнить коммит O с чем-то. Само по себе это просто снимок. С чем вы сравниваете коммит O с?

Выполнение git pull origin branch-B не изменило мой код в локальном ...

Запуск git pull первый запуск git fetch, затем запускается git merge. В этом случае вам нужно, чтобы ваш Git вызывал их (Bitbucket's Git) и спрашивал об их branch-B, который идентифицирует коммит O. У вас уже есть коммит O, поэтому коммиты не приходят. Затем ваш Git обновляет ваш origin/branch-B, чтобы он указывал на фиксацию O, если он этого еще не сделал - вероятно, это произошло, - и тогда ваш Git запускается git merge <hash-of-O>.

Текущий коммит это commit O, поэтому объединять нечего. Поэтому неудивительно, что это ничего не делает.

Заключение

Вышеприведенное может быть несколько ошибочным: возможно, слияние, которое произвела Bitbucket, могло бы быть слиянием с ускорением (но явно не было), и в этом случае график рисуется в точке, где у вас есть только master и сделали git revert -m 1 должно быть:

          I--J
         /    \
...--G--H------M--N   <-- master (HEAD)

и мне совершенно не ясно, какой коммит вы использовали, когда воссоздали имя branch-A (возможно, у вас было намерение совершить коммитH). Остальное, однако, должно быть примерно таким же: вы получите:

          I--J
         /    \
...--G--H------M--N   <-- master
         \
          O   <-- branch-B

Когда вы просматриваете коммит O, вы должны сделать это в некотором родеконтекст. Команда git show просматривает его, сравнивая с его родителем H. Просмотрщик снимков будет смотреть на его сохраненный снимок.

В подобных головоломках всегда полезно остановиться и нарисовать график . Помните о том, что два разных репозитория Git могут иметь разные имена веток ... и, если вы не запустили git fetch или git push, один репозиторий может иметь некоторые коммиты, которых нет у другого. Однако, как только коммиты передаются, так что каждый репозиторий имеет каждый коммит, все дело в том, чтобы посмотреть на график: найти коммиты подсказок по именам веток и работать в обратном направлении.

...