Git Diff одинаковых файлов в двух каталогах всегда приводит к "переименованию"
git diff --no-index --no-prefix --summary -U4000 directory1 directory2
Это работает, как и ожидалось, так как возвращает разность всех файлов между двумя каталогами. Файлы, которые добавляются, выводятся, как ожидается, файлы, которые удаляются, также приводят к ожидаемому выводу diff.
Однако поскольку diff учитывает путь к файлу как часть имени файла, файлы с одинаковым именем в двух разных каталогах приводят к выводу diff с переименованным флагом вместо измененного.
Есть ли способ сказать git не принимать во внимание полный путь к файлу в diff и смотреть только на имя файла, как если бы файлы происходили из одного и того же каталога?
Есть ли способ для git узнать, действительно ли копия того же файла в другом каталоге была переименована? Я не вижу, как, если только у него нет способа сравнить файлы md5s так или иначе (вероятно, плохое предположение, смеется).
Будет ли использование веток вместо каталогов решить эту проблему легко, и если да, то какова версия ветки команды, перечисленной выше?
1 ответ
Здесь есть несколько вопросов, ответы на которые переплетаются. Давайте начнем с обнаружения переименования и копирования, а затем перейдем к веткам.
Переименовать обнаружение
Однако поскольку diff учитывает путь к файлу как часть имени файла, файлы с одинаковым именем в двух разных каталогах приводят к выводу diff с переименованным флагом вместо измененного.
Это не совсем верно. (Текст ниже предназначен как для пунктов 1, так и для пунктов 2).
Хотя вы используете --no-index
(предположительно, чтобы заставить Git работать с каталогами вне репозитория), код diff в Git ведет себя одинаково во всех случаях. Чтобы различать (сравнивать) два файла в двух деревьях, Git должен сначала определить идентичность файла. То есть существует два набора файлов: файлы в "левой стороне" или в дереве исходных текстов (первое имя каталога) и файлы в "правой стороне" или в дереве назначения (второе имя каталога). Некоторые файлы слева совпадают с файлами справа. Некоторые файлы слева - это разные файлы, которые не имеют соответствующего правого файла, то есть они были удалены. Наконец, некоторые файлы на правой стороне являются новыми, то есть они были созданы.
Файлы, которые являются "одним и тем же файлом", не обязательно должны иметь одинаковое имя пути. В этом случае эти файлы были переименованы.
Вот как это работает в деталях. Обратите внимание, что "полное имя пути" несколько изменяется при использовании git diff --no-index dir1 dir2
: "полное имя пути" - это то, что остается после удаления dir1
а также dir2
префиксы.
При сравнении деревьев слева и справа файлы с одинаковыми полными путями обычно автоматически считаются "одним и тем же файлом". Мы помещаем все эти файлы в очередь "файлов для проверки", и ни один из них не будет отображаться как переименованный. Обратите внимание на слово "обычно" здесь - мы вернемся к этому через минуту.
Это оставляет нам два оставшихся списка файлов:
- пути, которые существуют слева, но не справа: источник без пункта назначения
- пути, которые существуют справа, но не слева: пункт назначения без источника
Наивно, мы можем просто объявить, что все эти исходные файлы были удалены, и все эти файлы назначения были созданы. Вы можете поручить git diff
вести себя так: установите --no-renames
флаг, чтобы отключить обнаружение переименования.
Или Git может использовать более умный алгоритм: установить --find-renames
и / или -M <threshold>
флаг, чтобы сделать это. В Git версий 2.9 и новее обнаружение переименования включено по умолчанию.
Теперь, как Git решит, что исходный файл имеет такую же идентичность, что и файл назначения? У них разные пути; какой правый файл делает a/b/c.txt
слева соответствуют? Это может быть d/e/f.bin
, или же d/e/f.txt
, или же a/b/renamed.txt
, и так далее. Фактический алгоритм является относительно простым, и в прошлом не использовался окончательный компонент имени (я не уверен, что сейчас так происходит, Git постоянно развивается):
Если есть исходные и конечные файлы, содержимое которых точно совпадает, выполните их сопряжение. Поскольку Git хэширует содержимое, это сравнение очень быстрое. Мы можем сравнить левую сторону
a/b/c.txt
по хеш-идентификатору для каждого файла справа, просто просматривая все их хеш-идентификаторы. Поэтому сначала мы просматриваем все исходные файлы, находя подходящие файлы назначения, помещая новые пары в очередь сравнения и вытаскивая их из двух списков.Для всех оставшихся исходных и целевых файлов запустите эффективный, но неподходящий для
git diff
вывод, алгоритм для вычисления "сходства файлов". Исходный файл, который по крайней мере<threshold>
аналогично некоторому целевому файлу вызывает спаривание, и эта файловая пара удаляется. Пороговое значение по умолчанию составляет 50%: если вы включили обнаружение переименования без выбора определенного порогового значения, два файла, которые к этому моменту все еще находятся в списках и имеют сходство на 50%, становятся парными.Любые оставшиеся файлы либо удалены, либо созданы.
Теперь, когда мы нашли все пары, git diff
приступает к разложению парных файлов с одинаковыми идентификационными данными и сообщает нам, что удаленные файлы удаляются и создаются вновь созданные файлы. Если два пути к файлам с одинаковой идентификацией различаются, git diff
говорит, что файл переименован.
Код произвольного сопряжения файлов стоит дорого (хотя код с одинаковыми именами дает пару очень дешев), поэтому у Git есть ограничение на количество имен, попадающих в эти списки источника и места назначения. Этот предел настраивается через git config diff.renameLimit
, Значение по умолчанию поднялось за эти годы и теперь составляет несколько тысяч файлов. Вы можете установить его на 0
(ноль), чтобы Git всегда использовал свой внутренний максимум.
Ломать пары
Выше я говорил, что обычно файлы с одинаковыми именами соединяются автоматически. Обычно это правильно, так что это Git по умолчанию. В некоторых случаях, однако, левый файл с именем a/b/c.txt
на самом деле не связано с правым файлом с именем a/b/c.txt
, это действительно связано с правой стороной a/doc/c.txt
например. Мы можем сказать Git разорвать пары файлов, которые "слишком разные".
Мы увидели "индекс сходства", использованный выше для формирования пар файлов. Этот же индекс сходства можно использовать для разделения файлов: -B20%/60%
, например. Эти два числа не должны добавлять до 100%, и вы можете опустить либо одно, либо оба: есть значение по умолчанию для каждого, если вы установите -B
Режим.
Первое число - это точка, в которой файл по умолчанию, уже сопряженный, может быть добавлен в списки обнаружения переименования. С -B20%
если файлы не похожи на 20% (т.е. только на 80% похожи), файл попадает в список "источник для переименований". Если его никогда не принимают за переименование, он может выполнить повторное сопряжение с автоматическим назначением, но в этот момент вступает в силу второе число, после слэша.
Второе число устанавливает точку, в которой спаривание определенно нарушено. С -B/70%
Например, если файлы не похожи на 70% (то есть похожи только на 30%), соединение нарушается. (Конечно, если файл был удален как источник переименования, соединение уже нарушено.)
Обнаружение копирования
Помимо обычного определения пар и переименования, вы можете попросить Git найти копии исходных файлов. После запуска всего обычного кода сопряжения, включая поиск переименований и разрыв пар, если вы указали -C
Git будет искать "новые" (то есть непарные) файлы назначения, которые фактически скопированы из существующих источников. Для этого есть два режима, в зависимости от того, указали ли вы -C
дважды или добавить --find-copies-harder
: каждый рассматривает только исходные файлы, которые изменены (это единственный -C
случай), и тот, который рассматривает каждый исходный файл (это два -C
или же --find-copies-harder
дело). Обратите внимание, что это "был изменен исходный файл" означает, в этом случае, что исходный файл уже находится в парной очереди - если нет, он не "изменен" по определению - и его соответствующий целевой файл имеет другой идентификатор хеша (снова это очень дешевый тест, который помогает сохранить -C
вариант дешевый).
Филиалы не имеют значения
Будет ли использование веток вместо каталогов решить эту проблему легко, и если да, то какова версия ветки команды, перечисленной выше?
Филиалы не имеют никакого значения здесь.
В Git термин ветвь неоднозначен. Посмотрите, что именно мы подразумеваем под "ветвью"? За git diff
Тем не менее, имя ветки просто разрешается в один коммит, а именно в коммит-коммит этой ветки.
Мне нравится рисовать ветви Git следующим образом:
...--o--o--o <-- branch1
\
o--o--o <-- branch2
Маленький раунд o
каждый из них представляет коммит. Два имени веток в Git - просто указатели: они указывают на один конкретный коммит. Имя branch1
указывает на самый правый коммит в верхней строке и имя branch2
указывает на самый правый коммит в нижней строке.
Каждый коммит в Git указывает на своего родителя или родителей (большинство коммитов имеют только одного родителя, в то время как коммит слияния - это просто коммит с двумя или более родителями). Это то, что формирует цепочку коммитов, которую мы также называем "ветвью". Название ветви указывает прямо на вершину цепочки. 1
Когда вы бежите:
$ git diff branch1 branch2
все, что делает Git - это преобразовывает каждое имя в соответствующий коммит. Например, если branch1
имена совершают 1234567...
а также branch2
имена совершают 89abcde...
, это просто делает то же самое, что и:
$ git diff 1234567 89abcde
Git's diff берет два дерева
Git даже не заботится о том, что это коммиты, правда. Git просто нужно левое или исходное дерево, а также правое или целевое дерево. Эти два дерева могут происходить из коммита, потому что коммит называет дерево: дерево любого коммита является исходным снимком, сделанным, когда вы сделали этот коммит. Они могут исходить из ветви, потому что имя ветки называет коммит, который называет дерево. Одно из деревьев может быть получено из "индекса" Git (он же "промежуточная область" или "кеш"), так как индекс в основном является сплющенным деревом. 2 Одно из деревьев может быть вашим рабочим деревом. Одно или оба дерева могут даже быть вне контроля Git (следовательно, --no-index
флаг).
Конечно, Git может просто различать два файла
Если вы бежите git diff --no-index /path/to/file1 /path/to/file2
, Git будет просто различать два файла, то есть рассматривать их как пару. Это полностью обходит код обнаружения и переименования. Если нет количества возиться с --no-renames
, --find-renames
, --rename-threshold
и т. д., опции делают свое дело, вы можете явно различать пути к файлам, а не каталоги (дерево). Для большого набора файлов это, конечно, будет больно.
1 После этой точки может быть больше коммитов, но это все еще верхушка его цепи. Более того, несколько имен могут указывать на один коммит. Я рисую эту ситуацию как:
...--o--o <-- tip1
\
o--o <-- tip2, tip3
Обратите внимание, что коммиты, которые находятся "за" более чем одним именем ветви, фактически находятся во всех этих ветвях. Таким образом, оба нижних ряда коммитов находятся на обоих tip2
а также tip3
ветви, в то время как обе верхние строки находятся на всех трех ветках. Тем не менее, каждое имя ветви разрешается в один и только один коммит.
2 Фактически, чтобы сделать новый коммит, Git просто конвертирует индекс, как он есть сейчас, в дерево, используя git write-tree
, а затем делает коммит, который называет это дерево (и который использует текущий коммит в качестве своего родителя, имеет автора и коммиттера, а также сообщение о коммите). Тот факт, что Git использует существующий индекс, поэтому вы должны git add
Ваши обновленные файлы рабочего дерева в индекс перед фиксацией.
Есть несколько удобных ярлыков, которые позволяют вам сказать, git commit
добавить файлы в индекс, например, git commit -a
или же git commit <path>
, Они могут быть немного хитрыми, так как они не всегда дают индекс, который вы можете ожидать. Увидеть --include
против --only
варианты git commit <path>
, например. Они также работают путем копирования основного индекса в новый временный индекс; и это может привести к неожиданным результатам, потому что, если фиксация выполнена успешно, временный индекс копируется обратно по обычному индексу.