Как переместить файлы во время рефакторинга, не вызывая конфликтов? - PullRequest
2 голосов
/ 02 апреля 2020

Я работаю в репозитории со следующей структурой каталогов:

my-project/
  a/
  b/
  c/

Мне нужно реорганизовать его и переместить a, b и c в подпапку, например так :

my-project/
  subfolder/
    a/
    b/
    c/

Однако есть много активных ветвей, в которых люди работают с файлами внутри a, b и c, и они не в подпапке. Как я могу выполнить этот рефакторинг, не вызывая конфликтов всех ветвей моих товарищей по команде при попытке слияния?

1 Ответ

3 голосов
/ 02 апреля 2020

Как прокомментировал Evert , в идеале вам не нужно ничего делать.

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

Подробности

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

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

Каждый коммит имеет свой уникальный идентификатор ha sh. Этот идентификатор ha sh на самом деле представляет собой криптографическую c контрольную сумму содержимого фиксации как из его основных данных - его снимка - так и из его метаданных. Это означает, что после выполнения коммита его нельзя изменить ни на один бит: изменение любого отдельного бита или любой группы бит просто делает новый коммит с новым уникальным идентификатором ha sh, в то время как существующий коммит все еще там.

Git в целом работает в обратном направлении. имя ветви содержит необработанный идентификатор ha sh последнего коммита . Оттуда Git находит второй-последний коммит, который находит третий-последний и так далее:

... <-F <-G <-H   <--branch

Имя branch содержит фактический идентификатор ha sh о каком-то коммите мы просто звоним H. Когда Git читает H из своей большой базы данных, используя этот идентификатор ha sh, H содержит идентификатор ha sh предыдущего коммита G. Git теперь может читать G из своей Git -objects-database, которая получает идентификатор ha sh предыдущего коммита F. Это позволяет git log и другим командам работать через коммиты в обратном направлении.

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

Если коммит G README.md и коммит H README.md идентичны, Git даже не скажет вам что H имеет README.md. Однако, если они отличаются, Git сравнит два README.md файла содержимого и покажет, что изменило . Вот как вы видите изменения.

Вы можете сравнить любой двух коммитов, не только родитель и потомок, но сравнение родителей и потомков чрезвычайно распространено: многие команды Git просто сделай это автоматически. Некоторые, такие как git diff, позволяют выбрать два коммита, а некоторые, например git merge, выбирают коммит самостоятельно, как мы увидим через мгновение.

Обнаружение переименования

Если вы переименовываете некоторые файлы, то на самом деле происходит следующее: ранний коммит G имеет файл, скажем, README.txt, а позже коммит H имеет файл с именем README.md. Git замечает, что G не имеет README.md и H не имеет README.txt, и догадывается, что может быть, просто возможно, вы переименованный эти два файла в этих двух коммитах.

Если вы переименуете целую коллекцию файлов - в глазах Git нет каталогов; файлы просто имеют длинные имена: a/b/c.ext - это имя файла, это не папка с именем a, содержащая папку с именем b и т. д., это просто длинное имя с косой чертой - Git попытается сопоставить каждая файловая пара, которая может. (Совсем недавно были предприняты некоторые попытки улучшить сопоставление имен, чтобы принять во внимание типичное «изложение папок». Несколько раз оно ошибалось, но я думаю, что оно вернулось сейчас.)

Это Обнаружение переименования - опционально во внутреннем механизме различия Git. При запуске git diff он включен по умолчанию в современном Git, но выключен по умолчанию в очень старых версиях Git. Вы можете включить его с помощью git diff --find-renames (для краткости git diff -M) или установить diff.renames в true в вашей конфигурации.

Обнаружение слияния и переименования

При запуске git checkout somebranch; git merge otherbranch, Git опирается на граф фиксации , чтобы найти базу слияния . Я собираюсь опустить все детали об этом здесь; подробнее см. другие ответы.

Рассмотрим граф фиксации, который выглядит следующим образом:

          I--J   <-- somebranch (HEAD)
         /
...--G--H
         \
          K--L   <-- otherbranch

То есть names somebranch и otherbranch выбирают коммиты J и L соответственно. Коммиты через H находятся в обеих ветвях , в то время как коммиты I-J находятся только в somebranch, а в данный момент коммиты K-L находятся только в otherbranch. Вы запустили git checkout somebranch, на что указывает HEAD, связанный с именем somebranch, и git merge otherbranch теперь запускается.

Git теперь найдет базовый коммит слияния H на свой. Найдя базу слияния, Git теперь необходимо преобразовать снимки в H, J и L в , что вы изменили на somebranch и , на что они изменились otherbranch соответственно.

Поскольку git diff может находить переименования, слияние выполняется только git diff с опцией find-renames, которая наверняка включена:

git diff --find-renames <hash-of-H> <hash-of-J>   # what we changed
git diff --find-renames <hash-of-H> <hash-of-L>   # what they changed

В объединить там, где не было переименований, детектор переименования ничего не находит, а Git просто объединяет, например, README.md в H на основе изменений, найденных при сравнении H README.md с J README.md и затем снова на основе L README.md. Однако, когда переименовывает , Git должен соединить каждую пару файлов. Например, если:

  • README.txt в H равно README.md в J, а
  • README.txt в H равно README.txt в L

затем вы переименовали файл, а они не сделали. Комбинацией этих операций является переименование файла .

Итак, в вашем случае вы собираетесь сделать коммит, в котором имена файлов теперь subfolder/a/file.ext и так далее, когда они были просто a/file.ext и так далее. Если повезет, Git будет правильно сопоставлять базу слияния a/file.ext с a/file.ext другого парня, в этом дифференциале и правильно совпадут a/file.ext с вашим subfolder/a/file.ext. Разница покажет, что одна сторона переименовала файл , а другая - нет, а комбинация этих двух изменений включает в себя «переименование файла».

Когда это происходит go не так?

Git Детектор переименования, управляемый его механизмом сравнения, зависит от трех вещей:

  • Файл с его старым именем должен быть там -на-слева и не-там-справа, а под новым именем он должен быть там-справа-а не-там-слева.

    То есть предположим, что у нас было path/to/file.ext, а теперь new/path/to/file.ext. Мы поместим старый коммит слева, а новый справа. Но что, если мы создали новое и отличное path/to/file.ext справа?

    Git даже не попытаются сравнить path/to/file.ext слева с new/path/to/file.ext справа, потому что он сопоставит левую сторону path/to/file.ext с новым, но не связанным path/to/file.ext справа. Git просто вызовет new/path/to/file.ext справа новый файл .

    Следовательно, первоначальное сравнение left-vs-right должно показать некоторые «удаленные» файлы слева и некоторые "новые" файлы справа. Детектор переименования попытается сопоставить левые удаленные файлы с правыми добавленными файлами и преобразовать такие пары файлов в (обнаруженные) переименования.

  • Даже если у вас есть такая пара, Git не будет вызывать файл , переименованный , если содержимое не похоже на . То есть, предположим, вы не только переименовали файл, но и что-то изменили. Git сделает быстрый тест на сходство , выраженное в процентах. Если левый и правый файлы по крайней мере «похожи на 50%», Git сочтет это кандидатом на переименование.

    Git должен сделать это для каждые слева и непарный файл с правой стороны. То есть для каждой пары файлов Git должен вычислять индекс сходства. left/file1.a похоже на right/file2.b? Насколько похоже left/file1.a на right/file3.c? Повторите для каждого файла с обеих сторон.

    Чтобы сделать это go быстрее, Git может легко найти 100% идентичные файлы. Поэтому вы можете сначала зафиксировать операцию переименования, затем зафиксировать изменения в переименованных файлах и улучшить положение при выполнении фиксации за коммитом, как это делает git log.

    Это менее полезно во время слияний, поскольку git merge никогда не идет коммит за коммитом. (Я думаю, у него должна быть возможность сделать это, просто чтобы найти переименования, но это не так.)

    Порог сходства по умолчанию, равный 50%, является только значением по умолчанию. При запуске git diff вы можете повысить или понизить минимальное требуемое сходство, используя параметр -M или --find-renames с числом, как в -M30, чтобы уменьшить его до 30% или -M75, чтобы повысить его до 75%. 1 При использовании git merge вы можете установить его с помощью -X find-renames=<number>, чтобы выбрать предел. (В старых версиях Git вместо этого вам нужно -X rename-threshold=<number>: посмотрите документацию вашей конкретной версии Git, например, git help merge.)

  • Last, Git накладывает пределы переименования из-за сложности поиска похожих файлов.

    Предел по умолчанию в современном Git (Git 2.26) составляет 400, т. е. может быть обнаружено 400 переименований. Если вы переименовали 402 файла, 400 из них будут обнаружены, а два - нет. Вы можете повысить или понизить этот лимит, используя diff.renameLimit. Установка его в ноль говорит Git, что искусственно не ограничивать обнаружение переименования.

    Команда git merge и имеет свои собственные отдельные ручки настройки, но не имеет значения по умолчанию для включения обнаружения переименования даже в старых версиях Git, он будет подчиняться вашему diff.renameLimit, если вы не установите отдельное merge.renameLimit.

Я установил diff.renameLimit на ноль, чтобы отменить ограничение переименования. Это приводит к тому, что некоторые команды git diff и git merge иногда запускаются очень медленно, но мне не нужно беспокоиться об этом (и я знаю, что при необходимости включите его).


1 Осторожно, --find-renames=4 означает 40%, а не 4%. Вы можете добавить символ %, --find-renames=4% или просто написать его как --find-renames=04. Вероятно, не стоит снижать порог переименования слишком далеко, так как Git начнет искать переименования, которые не имеют смысла.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...