Статьи

Какая разница?

В этом посте я расскажу о том, как различать ревизии и создавать патчи с помощью JGit . Начиная от высокоуровневой DiffCommand и заканчивая более универсальными API, чтобы найти конкретные изменения в файле.

DiffCommand, возьми я

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

Простая форма создания diff в JGit выглядит следующим образом:

1
git.diff().setOutputStream( System.out ).call();

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

Команда выводит текстовое представление diff в указанный поток вывода:

1
2
3
4
5
6
7
8
diff --git a/file.txt b/file.txt
index 19def74..d5fcacb 100644
--- a/file.txt
+++ b/file.txt
@@ -1 +1,2 @@
 existing line
+added line
\ No newline at end of file

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

Но как можно сравнить две произвольные ревизии? При более внимательном рассмотрении DiffCommand становится очевидно, что он фактически сравнивает два дерева, а не ревизии. И это объясняет, почему рабочий каталог и индекс (которые сами являются деревьями) также можно сравнивать без дополнительных усилий.

Следовательно, команда diff ожидает, что параметры типа AbstractTreeIterator будут указывать старое и новое дерево для сравнения. Иногда старое и новое также называют источником и местом назначения или просто a и b. Чтобы узнать больше о том, что такое деревья в Git, вы можете прочитать о Git Internals с помощью JGit API .

Итераторы дерева

Но как получить определенный итератор дерева? Рассмотрение иерархии типов AbstractTreeIterator показывает, что есть три реализации, представляющие интерес.

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

1
AbstractTreeIterator treeIterator = new FileTreeIterator( git.getRepository() );

DirCacheIterator раскрывает содержимое кэша dir (он же индекс) и может быть создан аналогично FileTreeIterator. Имея репозиторий, мы можем сказать ему прочитать индекс и передать этот экземпляр DirCacheIterator следующим образом:

1
AbstractTreeIterator treeIterator = new DirCacheIterator( git.getRepository().readDirCache() );

Однако наиболее интересным, вероятно, является CanonicalTreeParser . Его можно настроить для анализа произвольного объекта дерева Git. Следовательно, его необходимо сбросить с помощью идентификатора объекта дерева из хранилища. После настройки его можно использовать для перебора содержимого этого дерева.

Это лучше всего иллюстрируется на следующем примере:

1
2
3
4
5
CanonicalTreeParser treeParser = new CanonicalTreeParser();
ObjectId treeId = repository.resolve( "my-branch^{tree}" );
try( ObjectReader reader = repository.newObjectReader() ) {
  treeParser.reset( reader, treeId );
}

Анализатор дерева настроен для итерации по дереву коммита, на который указывает my-branch .

Остерегайтесь того, что не определено, что resolv возвращает, если есть несколько совпадений. Например, resolve( "aabbccdde^{tree}" ) вызова resolve( "aabbccdde^{tree}" ) может вернуть неправильное дерево, если есть ветвь и сокращенный идентификатор фиксации с этим именем. Поэтому предпочитайте полные ссылки, такие как refs/heads/my-branch для ссылки на ветку my-branch или refs/tags/my-tag для тега my-tag.

Если идентификатор коммита уже доступен в форме ObjectId (или AnyObjectId), используйте следующий фрагмент кода, чтобы получить его идентификатор дерева:

1
2
3
4
5
6
7
try( RevWalk walk = new RevWalk( git.getRepository() ) ) {
  RevCommit commit = walk.parseCommit( commitId );
  ObjectId treeId = commit.getTree().getId();
  try( ObjectReader reader = git.getRepository().newObjectReader() ) {
    return new CanonicalTreeParser( null, reader, tree );
  }
}

Код предполагает, что данный идентификатор объекта ссылается на коммит и разрешает связанный RevCommit, который, в свою очередь, содержит идентификатор соответствующего дерева.

DiffCommand Revisited

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

1
2
3
4
git.diff()
  .setOldTree( oldTreeIterator )
  .setNewTree( newTreeIterator )
  .call();

С помощью методов setOldTree () и setNewTree () можно сравнивать деревья для сравнения.

Помимо этих основных свойств, можно управлять несколькими другими аспектами команды:

  • setPathFilter позволяет ограничить проверенные файлы определенными путями в хранилище
  • setSourcePrefix и setDetinationPrefix изменяют префикс исходного (старого) и целевого (нового) пути. Значениями по умолчанию являются a/ и b/ .
  • setContextLines изменяет количество строк контекста, то есть количество строк, напечатанных до и после измененной строки. Значение по умолчанию три.
  • setProgressMonitor позволяет отслеживать прогресс во время вычисления различий. Вы можете реализовать свой собственный монитор прогресса или использовать один из предопределенных, которые идут с JGit
  • setShowNameAndStatusOnly пропускает генерацию текстового вывода и просто возвращает вычисленный список DiffEntries. (как следует из названия)

Помимо свойств, описанных до сих пор, DiffCommand считывает эти параметры конфигурации из раздел.

  • noPrefix: если установлено значение true, префиксы источника и назначения по умолчанию пусты вместо a/ и b/ .
  • переименовывает: если задано значение true, команда пытается обнаружить переименованные файлы на основе аналогичного содержимого. Подробнее о переименовании контента позже.
  • алгоритм: алгоритм сравнения, который следует использовать. JGit в настоящее время поддерживает myers или histogram .

DiffEntry, возьми я

Как упоминалось ранее, мы более подробно рассмотрим основной вывод команды diff: DiffEntry. Для каждого файла, который был добавлен, удален или изменен, возвращается отдельный DiffEntry. GetChangeType () указывает тип изменения: ДОБАВИТЬ, УДАЛИТЬ или ИЗМЕНИТЬ. Если при сканировании изменений использовался детектор переименования, типом изменения также может быть RENAME или COPY.

Кроме того, DiffEntry содержит информацию о старом и новом состоянии, включая путь, режим и идентификатор файла. Методы названы соответственно getOldPath / Mode / Id и getNewPath / Mode / Id. В зависимости от того, представляет ли запись добавление или удаление, методы getNew или getOld могут возвращать «пустые» значения. JavaDoc подробно объясняет, какие значения возвращаются. Обратите внимание, что идентификатор ссылается на объект BLOB-объекта в базе данных репозитория, которая содержит содержимое файла.

Под прикрытием DiffCommand

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

DiffCommand в основном использует DiffFormatter, к которому также можно обращаться напрямую для поиска изменений и создания исправлений.

Его метод scan () ожидает итераторы для старого и нового дерева и возвращает список DiffEntries. Существуют также перегруженные версии, которые принимают идентификаторы объектов дерева для предоставления.

Простой пример поиска изменений выглядит так:

1
2
3
4
5
OutputStream outputStream = DisabledOutputStream.INSTANCE;
try( DiffFormatter formatter = new DiffFormatter( outputStream ) ) {
  formatter.setRepository( git.getRepository() );
  List<DiffEntry> entries = formatter.scan( oldTreeIterator, newTreeIterator );
}

Выходной поток, который будет использоваться format (), указывается в конструкторе. Поскольку сейчас мы не заинтересованы в выводе, предоставляется нулевой поток вывода. С setRepository указывается хранилище, которое должно быть проверено. И, наконец, парсеры дерева передаются методу scan (), который возвращает список изменений между ними.

Обратите внимание, что DiffFormatter необходимо явно закрывать или использовать в операторе try-with-resources, как показано в примере кода.

Для создания патчей можно использовать один из методов format (). Патч выражается в виде инструкций по изменению старого дерева, чтобы сделать его новым деревом.

Как и методы scan (), методы format () принимают указатели или итераторы для старого и нового дерева. Любая сторона может быть нулевой, чтобы указать, что дерево было добавлено или удалено. В этом случае разница будет вычислена против ничего.

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

1
2
3
4
5
OutputStream outputStream = new ByteArrayOutputStream();
try( DiffFormatter formatter = new DiffFormatter( outputStream ) ) {
  formatter.setRepository( git.getRepository() );
  formatter.format( oldTreeIterator, newTreeIterator );
}

Существуют также перегруженные методы format () для печати одного DiffEntry или списка DiffEntries, которые, возможно, были получены предыдущим вызовом scan ().

Хотя результат вышеприведенного примера также может быть достигнут с помощью простого DiffCommand, давайте посмотрим, что еще может предложить DiffFormater.

Как упоминалось ранее, переименованные файлы могут быть связаны при вычислении различий. Чтобы включить обнаружение переименования, необходимо рекомендовать DiffFormatter сделать это с помощью setDetectRenames (). После этого для точной настройки можно получить RenameDetector с помощью getRenameDetector ().

Помните, что Git является трекером контента и не отслеживает переименования . Вместо этого переименования выводятся из аналогичного содержимого во время операции diff.

Кроме того, DiffFormatter имеет несколько дополнительных свойств для точной настройки его поведения, которые перечислены ниже:

  • setAbbreviationLength: количество цифр для печати идентификатора объекта.
  • setDiffAlgorithm: алгоритм, который следует использовать для построения вывода diff.
  • setBinaryFileThreshold: файлы, размер которых превышает этот размер, будут обрабатываться так, как если бы они были двоичными, а не текстовыми. По умолчанию 50 МБ.
  • setDiffComparator: компаратор, используемый для определения идентичности двух строк текста. Компаратор может быть настроен на игнорирование различных типов пробелов. Однако я не смог позволить DiffFormatter игнорировать все пробелы .

DiffEntry Revisited

Если вас интересуют изменения, которые произошли в определенном файле, вы можете еще раз взглянуть на DiffEntry и DiffFormatter.

С diffFormatter.toFileHeader (), так называемый FileHeader может быть получен из данного DiffEntry. А через метод toEditList () можно получить список правок.

В следующем примере кода показано, как получить список редактирования для первой записи diff, полученной в результате сканирования:

1
2
3
4
5
6
7
OutputStream outputStream = DisabledOutputStream.INSTANCE;
try( DiffFormatter formatter = new DiffFormatter( outputStream ) ) {
  formatter.setRepository( git.getRepository() );
  List<DiffEntry> entries = formatter.scan( oldTreeIterator, newTreeIterator );
  FileHeader fileHeader = formatter.toFileHeader( entries.get( 0 ) );
  return fileHeader.toEditList();
}

Этот список можно интерпретировать как модификации, которые необходимо применить к старому контенту, чтобы преобразовать его в новый контент.

Каждое редактирование описывает регион, который был вставлен, удален или заменен, и строки, на которые влияют
Строки считаются, начиная с нуля, и могут быть запрошены с помощью getBeginA (), getEndA () getBeginB () и getEndB ().

Например, предоставлен файл с этими двумя строками содержимого:

1
2
line 1
line 3

Вставка строки 2 между двумя строками приведет к редактированию типа INSERT с A (1-1) и B (1-2). Другими словами, замените строку 1 на строки 1 и 2. Повторное удаление строки 2 приводит к инверсии вставки Edit: DELETE с A (1-2) и B (1-1). И изменение текста строки 2 приведет к редактированию типа REPLACE с теми же областями A и B 1-2.

Завершение создания различий с JGit

Хотя DiffCommand довольно прост в использовании, DiffFormatter имеет страшный API. Но прежде чем использовать JGit в своем проекте, вы наверняка изолируете себя от библиотеки , не так ли?!? … и тем самым выбрать более подходящий API.

Но помимо этого, JGit предоставляет средства для выполнения большинства, если не всех задач, связанных с различий и исправлений в Git.

Фрагменты, показанные на протяжении всей статьи, являются выдержками из набора учебных тестов. Полный исходный код можно найти здесь:
https://gist.github.com/rherrmann/5341e735ce197f306949fc58e9aed141

Если вы хотите поэкспериментировать с примерами, перечисленными здесь, я рекомендую настроить JGit с доступом к источникам и JavaDoc так, чтобы у вас была содержательная контекстная информация, справка по содержимому, источники отладки и т. Д.

Если у вас возникли трудности или возникли дополнительные вопросы, оставьте комментарий или обратитесь за помощью к дружественному и полезному сообществу JGit .

Ссылка: Какая разница? Создание Diffs с JGit от нашего партнера JCG Рудигера Херрманна в блоге Code Affine .