Git объединяет ветки с разными структурами каталогов

Я немного новичок в git, я использую его в течение нескольких месяцев, и Im удобнее выполнять большинство основных задач. Итак... Я думаю, что пришло время заняться еще более сложными задачами. На моей работе у нас есть несколько человек, которые работают над старым кодом, чтобы обновить его, это связано с фактической работой кода и обновлением структуры каталогов, чтобы быть более модульным. Мой вопрос в том, могут ли эти две вещи выполняться в параллельных ветвях, а затем сливаться или пересоединяться. Моя интуиция говорит "нет", потому что реорганизация dir - это переименование, а git переименовывается, добавляя новый файл и удаляя старый (по крайней мере, так я понимаю). Но я хотел быть уверенным.
Здесь сценарий: родительская ветвь выглядит следующим образом:

├── a.txt
├── b.txt
├── c.txt

то мы разветким два, скажем, branchA и branchB. В ветке B мы модифицируем структуру:

├── lib
│   ├── a.txt
│   └── b.txt
└── test
    └── c.txt

Затем в ветки А мы обновляем a, b и c.

Есть ли способ слияния изменений, выполненных в ветке А с новой структурой в branchB? но мне кажется, что lib/a.txt фактически связан с a.txt после git mv...

Джеймсон

Ответы

Ответ 1

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

$ git checkout master
Switched to branch 'master'
$ git status

(убедитесь, что он очищает резервную копию неудачного слияния, когда изменения не являются забавными)

$ git merge feature

Если слияние не выполняется:

$ git merge --abort

Если автоматическое слияние выполнено успешно, но вы еще не хотите его сохранять:

$ git reset --hard HEAD^

(Помните, что HEAD^ является первым родителем текущего коммита, а первым родителем слияния является "то, что было до слияния". Таким образом, если слияние сработало, HEAD^ является фиксацией непосредственно перед слияние.)


Здесь простейший рецепт для определения того, что переименовывает git merge будет автоматически обнаружен.

  • Убедитесь, что diff.renamelimit 1 - 0, а diff.renames - true:

    $ git config --get diff.renamelimit
    0
    $ git config --get diff.renames
    true
    

    Если они еще не установлены таким образом, установите их. (Это влияет на шаг diff ниже.)

  • Выберите, с какой ветвью вы сливаетесь, и из которой вы сходите. То есть, вы скоро сделаете что-то вроде git checkout master; git merge feature; нам нужно знать эти два имени. Найдите базу слияния между ними:

    $ into=master from=feature
    $ base=$(git merge-base $into $from); echo $base
    

    Вы должны увидеть около 40 символов SHA-1, например ae47361... или что-то еще. (Не стесняйтесь вводить master и feature вместо $into и $from всюду здесь. Я использую переменные так, что это "рецепт" вместо "пример".)

  • Сравните базу слияния как с $into, так и с $from, чтобы определить, какие файлы обнаружены как "переименовать":

    $ git diff --name-status $base $into
    R100    fileB   fileB.renamed
    $ git diff --name-status $base $from
    R100    fileC   fileD
    

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

Два выходных раздела имеют список файлов A dded, D eleted, M odified, R enamed и т.д. (в этом примере есть только два переименования со 100% совпадениями).

Так как $into есть master, первый список - это то, что git думает, что уже произошло в master. (Это изменения git "хочет сохранить", когда вы объединяетесь feature.)

Между тем $from есть feature, поэтому второй список - это то, что git думает, что произошло в feature. (Это изменения git хочет "теперь добавить к master", когда вы выполните слияние.)

На этом этапе вам нужно выполнить кучу работы:

  • Файлы, отмеченные R, git, будут обнаружены как переименованные.
  • Если два списка R одинаковы в обеих ветвях, вы, возможно, все хорошо (но читайте в любом случае). Если в первом списке есть R, которые не находятся во втором... ну, см. Ниже.
  • Когда вы запустите git checkout master; git merge feature (или git checkout $into; git merge $from), git выполнит переименования, показанные во втором списке, чтобы "добавить эти изменения" в master.
  • В любом случае сравните это с файлами, которые вы хотите git обнаружить как переименованные. Найдите записи D и A, которые вы хотите отобразить как записи R: они возникают, когда в одной из ветвей вы не только переименовали этот файл, но также изменили содержимое настолько, что git больше не обнаруживает переименование.

Если во втором списке не отображается все, что вы хотите увидеть, вам придется помочь git выйти. См. Еще более подробное описание ниже.

Если первый список имеет переименование, а не второе, это может быть совершенно безобидным или может вызвать "ненужный" конфликт слияния и пропущенный шанс для реального слияния. git предполагает, что вы намереваетесь сохранить это переименование, а также посмотрите, что произошло в слиянии - из ветки ($from или feature в этом случае). Если исходный файл был изменен там, git попытается внести изменения оттуда в переименованный файл. Это, вероятно, то, что вы хотите. Если исходный файл там не был изменен, git не имеет ничего, чтобы вставить его и оставит файл в покое. Это также, вероятно, то, что вы хотите. "Плохой" случай - это снова необнаруженное переименование: git считает, что исходный файл был удален в ветке feature, и был создан новый файл с другим именем.

В этом "плохом" случае git даст вам конфликт слияния. Например, он может сказать:

CONFLICT (rename/delete): newname deleted in feature and renamed in HEAD.
Version HEAD of newname left in tree.
Automatic merge failed; fix conflicts and then commit the result.

Проблема здесь не в том, что git сохранил файл под своим новым именем в master (мы, вероятно, хотим этого); что git, возможно, упустил шанс объединить изменения, сделанные в ветке feature.

Хуже - и это может быть классифицировано как ошибка - если новое имя встречается в слиянии - из ветки feature, но git считает его новым файлом, git оставляет нас только слиянием - в версию файла в дереве работ. Выпущенное сообщение одинаково. Здесь я сделал еще несколько изменений в master, чтобы переименовать fileB в fileE, а на feature убедиться, что git не обнаружит изменения как переименование:

$ git diff --name-status $base master
R100    fileB   fileE
$ git diff --name-status $base feature
D       fileB
R100    fileC   fileD
A       fileE
$ git checkout master; git merge feature
CONFLICT (rename/delete): fileE deleted in feature and renamed in HEAD.
Version HEAD of fileE left in tree.
Automatic merge failed; fix conflicts and then commit the result.

Обратите внимание на потенциально вводящее в заблуждение сообщение, fileE deleted in feature. git печатает новое имя (версия имени master); что имя, которое, по его мнению, "хочет" увидеть. Но это файл fileB, который был "удален" в feature, заменен на совершенно новый fileE.

(git-imerge, упомянутый ниже, может справиться с этим конкретным случаем.)


1 Также существует merge.renameLimit (записанный в нижнем регистре limit в источнике, но эти переменные конфигурации не чувствительны к регистру), которые вы можете установить отдельно. Установка этих значений в 0 сообщает git использовать "подходящее значение по умолчанию", которое с годами изменилось по мере того, как процессоры стали быстрее. Если отдельный предел переименования слияния не установлен, git использует ограничение переименования diff и снова подходящее значение по умолчанию, если оно не установлено или равно 0. Если вы установите их по-другому, то слияние и diff будут определять переименования в разных случаях.

Вы также можете установить "порог переименования" в рекурсивном слиянии с -Xrename-threshold=, например, -Xrename-threshold=50%. Использование здесь такое же, как для опции git diff -M. Эта опция впервые появилась в git 1.7.4.


Скажем, вы находитесь на ветке master, и вы делаете git merge 12345467 или git merge otherbranch. Здесь git делает:

  • Найдите базу слияния: git merge-base master 1234567 или git merge-base master otherbranch.

    Это дает идентификатор commit. Позвольте называть этот ID B для "Base". git теперь имеет три конкретных идентификатора фиксации: B, база слияния; идентификатор фиксации вершины текущей ветки master; и идентификатор фиксации, который вы ему дали, 1234567 или кончик ветки otherbranch. Позвольте просто нарисовать их в терминах графа фиксации, для полноты; скажем, это выглядит так:

    A - B - C - D - E       <-- master
          \
            F - G - H - I   <-- otherbranch
    

    Если все пойдет хорошо, git создаст коммандную фиксацию с E и I в качестве двух родителей, но мы хотим сконцентрироваться здесь на полученном дереве работы, а не на графике фиксации.

  • Учитывая эти три коммиты (B E и I), git вычисляет два diff, a la git diff:

    git diff B E
    git diff B I
    

    Первый - это набор изменений, сделанных на branch, а второй - набор изменений, сделанных на otherbranch, в этом случае.

    Если вы запустите git diff вручную, вы можете установить "порог подобия" для обнаружения переименования с помощью -M (см. выше, чтобы установить его во время слияния). git default merge устанавливает автоматическое определение переименования на 50%, это то, что вы получаете без опции -M, а diff.renames - true.

Если файлы "достаточно схожи" (и "точно такое же" всегда достаточно), git обнаружит переименования:

    $ git diff B otherbranch  # I tagged the merge-base `B`
    diff --git a/fileB b/fileB.txt
    similarity index 71%
    rename from fileB
    rename to fileB.txt
    index cfe0655..478b6c5 100644
    --- a/fileB
    +++ b/fileB.txt
    @@ -1,3 +1,4 @@
     file B contains
     several lines of
     stuff.
    +changeandrename

(В этом случае я просто переименован из fileB в fileB.txt, но обнаружение также работает в каталогах.) Обратите внимание, что это удобно представить в виде git diff --name-status output:

    $ git diff --name-status B otherbranch
    R071    fileB   fileB.txt

(Я должен также отметить здесь, что у меня diff.renames установлено значение true и diff.renamelimit = 0 в моей глобальной конфигурации git.)

  1. Git теперь пытается объединить изменения от B до I (на otherbranch) в изменения от B до E (на branch).

Если git может обнаружить, что lib/a.txt переименован из a.txt, он будет их подключать. (И вы можете просмотреть, будет ли это делать, выполнив git diff.) В этом случае результат автоматического слияния, вероятно, будет тем, что вы хотите, или достаточно близко.

Если нет, это не будет.

Когда автоматическое обнаружение переименования выходит из строя, существует способ разбить коммиты (или, может быть, они уже достаточно разбиты) поэтапно. Предположим, например, что в последовательности F G H I один шаг (возможно, G) просто переименовывает a.txt в lib/a.txt, а другие шаги (F, H и/или I) делают так много других изменений в a.txt (под любым именем), чтобы обмануть git, не осознавая, что файл был переименован. Здесь вы можете увеличить количество слияний, чтобы git мог "видеть" переименование. Предположим для простоты, что F не изменяет a.txt и G переименовывает его, так что diff от B до G показывает переименование. Мы можем сделать первое слияние commit G:

git checkout master; git merge otherbranch~2

Как только это слияние завершено и git переименовал из a.txt в lib/a.txt в дереве для нового коммита слияния в ветке branch, мы делаем второе слияние для ввода commits H и I:

git merge otherbranch

Это двухступенчатое слияние заставляет git "делать правильную вещь".

В самом крайнем случае инкрементная последовательность слияния с фиксацией по фиксации (что было бы крайне болезненно для выполнения вручную) поднимет все, что можно было бы подобрать. К счастью, кто-то уже написал для вас эту программу "постепенного слияния": git-imerge. Я не пробовал это, но это Очевидный ответ на трудные случаи.