Заставить Git применять слияния при перемещении / переименовании файлов - PullRequest
0 голосов
/ 21 сентября 2018

Я уверен, что я делаю что-то здесь не так, но я не уверен, что.

У меня есть master и branch, которые в конечном итоге будут объединены, нона данный момент разработка происходит в обоих направлениях.

Это означает, что я регулярно объединяю последние изменения из master в branch.

Проблема в том, что в branch включено многофайл перемещается и переименовывается.

Мой текущий процесс:

  • в branch
    • Переименование my-control.html в my-control.js
    • Стадияизменение и фиксация - Git обнаруживает, что это move, а не delete+add
    • Обновление my-control.js
    • Фиксация my-control.js изменений.
    • my-control.js теперь есть новые изменения и история от my-control.html
  • в master
    • Внесите изменения в my-control.html
    • Подтвердитеизменить
  • обратно в branch
    • Объединить изменения с master

И это гдевозникают проблемы - иногда я получаю ожидаемые изменения my-control.js,но примерно в половине случаев я просто получаю my-control.html обратно в branch.

Когда это происходит, my-control.js имеет всю историю, а my-control.html имеет всю историю плюс 1 или 2 коммита из master.

  • Что я делаю не так?
  • Почему это иногда случается, а иногда работает?
  • Что я могу сделать, чтобы это исправить?
  • Есть ли способ сказать Git "нет, эти изменения должны применяться к этому файлу"?

1 Ответ

0 голосов
/ 21 сентября 2018

Справочная информация: идентификатор файла

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

Когда это происходит, my-control.js имеет всю историю, а my-control.html имеет всю историю плюс 1 или 2 коммита от мастера.

У Git нет истории файлов.У Git есть только история коммитов.Точнее, коммиты являются , история и файлы к этому не имеют отношения.Коммиты содержат файлов, но это никак не контролирует историю: коммиты - это история.

У меня есть подробности об этом, например, мой ответ на Отсутствует удалениестрок в истории файлов (git) .Если вы попросите Git --follow файл переименовать, Git использует его упрощение истории, чтобы показать только коммиты, которые касаются именованного файла - и когда одно из этих «прикосновений» - «Git обнаруживает переименование», Git начинает искать новое имя в этот момент и перестает искать старое.(Или, поскольку Git движется назад, лучше сказать, что он начинает искать старое имя и перестает искать новое.)

Этот метод, очевидно, может потерпеть неудачу при слияниях, поскольку одна ногаиз слияния может иметь «неправильное» имя.Тем не менее, в любом случае упрощение истории обычно сводится только к одному этапу слияния!

Если вы не используете --follow, но используете git log -- <em>path(s)</em> или эквивалентный, Git просто не обнаруживает переименование: онопросто упрощает историю, используя заданный путь или пути.

Немного замученная аналогия

Что я делаю не так?

Ничего или, может быть, все,Проблема в том, что Git иногда может, а иногда и не может определить, что файл с именем Bob в одном месте и файл с именем Robert в другом месте относятся к одному и тому же парню.Он может или не может правильно идентифицировать пару файлов.Боб и Роберт - один и тот же парень или нет?

Почему это иногда случается, а иногда работает?

Это, по крайней мере, дает твердый ответ: Git может идентифицировать два файла, если они достаточно похожи, и другие условия также выполняются.То есть, вы показываете Git два снимка с некоторыми файлами («людьми») в них и позволяете угадать, кто есть кто, а кто перемещался.Если есть только один файл с надписью «Bob» на более раннем рисунке, и один файл с надписью «Robert» на более позднем рисунке, Git может быть в состоянии обнаружить, что это один и тот же парень,до тех пор, пока он не потерял конечность или не получил дополнительную голову или что-то подобное.Однако, если на обеих фотографиях есть парни с именами «Боб» и «Роберт», Git будет предполагать, что два «Боба» - один и тот же парень, а два «Роберта» - один и тот же, и что ранееБоб никогда не будет последним Робертом, и даже наоборот.

Технический: git merge, граф фиксации и git diff --find-renames

Давайте посмотрим, как на самом деле работает git merge.Чтобы попасть туда, нам нужно начать с двух вещей: граф фиксации и git diff --find-renames.

График фиксации является важнейшим ключом к слиянию.Каждый коммит записывает необработанный хэш-идентификатор своего родительского коммита, если это обычный коммит, или, если это коммит слияния , оба (или все 1 )его родителей.Как правило, есть только два родителя слияния.Давайте нарисуем немного графика коммитов в качестве примера и выберем несколько конкретных коммитов для обсуждения.Вместо того, чтобы использовать полные большие уродливые хеш-идентификаторы, давайте использовать заглавную букву для указания конкретных коммитов (и круглые точки для менее интересных).У нас будут ветви branch и main, которые разделяются при коммите B, но были объединены хотя бы один раз в прошлом:

          o--o---D--o--o--E   <-- branch
         /        \
...--o--B--o----C--M--o--o--F   <-- main

Когда мы произвели слияние при коммите M (для слияния), слияния branch в main, база слияния была ясной и очевидной: последний общий коммит был B.Коммит B был и есть в обеих ветвях.Таким образом, способ, которым Git произвел слияние, был следующим:

git diff --find-renames <hash-of-B> <hash-of-C>   # what we did, on main
git diff --find-renames <hash-of-B> <hash-of-D>   # what they did, on branch

Затем Git объединил два набора изменений, применил объединенные изменения к снимку, сохраненному в B, и сделалрезультирующий коммит слияния M.

Поскольку M является коммитом слияния , он запоминает и C и D,Когда Git просматривает историю, которая, помните, состоит из коммитов, он должен посещать обоих родителей, когда он движется назад от M.

Теперь мы собираемсязапустить git checkout main; git merge branch.То есть мы выберем коммит F в качестве нашего текущего коммита и попросим Git объединить коммит E в F.Git теперь должен найти базу слияния: последний коммит, который был на обеих ветвях.

Можете ли вы угадать, какой коммит является базой слияния?Это не B на этот раз!

Поиск базы слияния - это всего лишь достижимость , и я передам более полное обсуждение Think Like (a) Git , но ответ здесь заключается в том, что, пройдя назад от F до M, мы можем достичь D, а пройдя назад от E, мы можем достичь D прямо вдоль верхней линии.Таким образом, D является базой для слияния на этот раз.Git еще раз запускает две команды git diff:

git diff --find-renames <hash-of-D> <hash-of-F>   # what we did on main
git diff --find-renames <hash-of-D> <hash-of-E>   # what they did on branch

Каждый diff имеет левую сторону, коммит D и правую сторону, коммит tip конкретной ветви.Git находит оба набора изменений, включая обнаружение переименований.Поэтому, если в базовом и подсказочном коммите есть какой-то файл с другим именем, и Git решил, что этот является тем же файлом под другим именем - например, Боб на левой фотографии стал помечен как Роберт вправильный - тогда Git объявит, что файл был переименован.

Git теперь объединит два набора изменений, используя базовый (D) снимок в качестве основы, к которой применяются изменения.Если изменения включают «переименовать файл», Git также сделает переименование.Если файл помечен Бобом в базе и Робертом в обоих подсказках, то оба diff имеют одинаковое переименование, и все хорошо.Если только одно изменение переименовывает файл, то имя, которое вы получите, зависит от того, в какой ветке вы находитесь при слиянии: мы переименовали Боба в Роберта или они сделали это?

Куда все идет очень плохо, если Git не может обнаружить переименование.Что, если Боб потерял руку, и Гит не узнает, что парень, помеченный Робертом на одной из фотографий, - это тот же самый парень?


1 Объединяется с тремя или более родителяминазываются слияниями осьминога в Git.У Linux есть одно слияние в 66 направлений, из которых Линус Торвальдс заметил: это не осьминог, это слияние Ктулху .


Что вы можете сделать сэто: индекс сходства

Что я могу сделать, чтобы это исправить?

Самым простым, безусловно, является избегание переименования.Git сначала считает, что метки на файлах - имена путей.Если базовый коммит и оба подсказки имеют файлы с именем bob.txt, почему, это должен быть тот же самый парень, Боб.Тут нечего путать.

Однако переименование уже произошло.Один из способов исправить это - организовать для всех будущих слияний использование нового имени: если файл должен называться robert, убедитесь, что каждое future base merge base и в будущем branch-tip вызовите файл robert, и путаницы не будет.

Если это невозможно, есть еще одна надежда на автоматизацию: Дайте Git больше (или другую) информацию.Фактически, сделайте Git умнее: скажите Git, что он должен совпадать с Бобом и Робертом, даже если он потерял всех своих конечностей.

FlКроме того, Git отличается от git diff против git merge, но оба используют одну и ту же идею - установить индекс сходства .Когда Git сравнивает два снимка (два коммита), если какой-то файл пропал слева, а новый файл появился справа, Git сравнивает содержимое этих файлов .

Используя git diff --find-renames (или короче, git diff -M), вы можете добавить порог индекса подобия : например,

git diff -M10

.Число после M (или --find-renames=) представляет собой минимальный требуемый индекс подобия для двух файлов, которые следует считать "одним и тем же" файлом, т. Е. Для Git решить, какой корабль (или был)Корабль Тесея или тот, кто носит имя Боба, тот же самый, что и парень, носящий имя Роберта.

Внутреннее вычисление Git схожести двух файлов не меняется, но порог в Git, точка, в которой он объявляет, что эти два разных файла действительно один и тот же файл , делает.Снижение порога делает Git очень счастливым для идентификации файлов с разными именами.Повышение этого делает Git более неохотным.

Порог сходства по умолчанию составляет 50%, -M50.Точно одинаковые байтовые файлы идентичны на 100%.Другие менее похожи / более непохожи.Фактическая формула в моем ответе на Попытка понять `git diff` и` git mv` механизм обнаружения переименования , но в целом способ найти пригодное число - использовать git diff на базе слияния идве подсказки ветви.Установите очень низкий порог, запустите git diff, и Git скажет вам, какие файлы были сопоставлены, и каково их фактическое сходство.

(Чтобы найти базу слияния, запустите git merge-base --all <em>commit1 commit2</em>, где два идентификатора фиксацииназовите ветку tip commit коммиты. Вы можете использовать здесь имена веток, или необработанные хеш-идентификаторы, или что-нибудь подходящее для Git в соответствии с документацией gitrevisions . После этого у вас будет хеш-код базы, которую выможно использовать в качестве одного из аргументов git diff.)

Вы можете указать тот же порог для git merge, используя -X find-renames=<em>number</em>.Вы можете просто использовать очень низкое число, но это может найти слишком много переименований.Чтобы узнать, что Git будет думать, что переименовано, используйте git diff.

Если все остальное терпит неудачу

Если ничего из вышеперечисленного недостаточно (что может ) случается), у вас нет полностью альтернативы:

Есть ли способ сказать Git "нет, эти изменения должны применяться к этому файлу"?

Существует полностью ручной способ слияния файлов:

  • Запустите слияние, используя --no-commit, чтобы сказать Git, что Git не долженпредположим, что слияние прошло успешно.

  • Разрешите все, что можете, используя более простые методы.

  • Если в Git неправильно определены файлы, распакуйтебазовая версия файла слияния из некоторой известной или выбранной вручную фиксации базы слияния.Если нет, то он уже находится в index в слоте стадии 1, так что вы можете извлечь его оттуда.В любом случае, извлеките файл в рабочее дерево под именем, которое вы можете использовать.Например:

    git show $hash:$basepath > file.base
    

    Аналогично, извлеките «нашу» и «их» версию файла в рабочее дерево:

    git show HEAD:file > file.ours
    git show MERGE_HEAD:$theirpath > file.theirs
    

    Теперь, когда у вас есть все три версиифайл, используйте git merge-file, чтобы выполнить трехстороннее объединение файла.Как только у вас будет правильный результат слияния в вашем рабочем дереве, поместите его под правильным именем и используйте git add, чтобы скопировать его в индекс, готовый для фиксации.Обязательно удалите из индекса любую неправильную (--theirs) версию, которая осталась позади - git status сообщит вам о таких файлах, если они существуют.

Когда слияние завершитсяиспользуйте git commit (или в достаточно новых версиях Git, git merge --continue), чтобы завершить слияние.

Это - мы вручную выбираем три файла и используем для них какую-то программу слияния - вот как мысделал это в старые добрые времена до Git.Добро пожаловать в 1990-е!: -)

...