Статьи

Как управлять подмодулями Git с помощью JGit

Для более крупного проекта с Git вы можете захотеть поделиться кодом между несколькими репозиториями. Является ли это общей библиотекой между проектами или, возможно, шаблонами и тому подобным, используемыми в различных продуктах Встроенный ответ Git на эту проблему — это субмодули. Они позволяют помещать клон другого репозитория в качестве подкаталога в родительский репозиторий (иногда его также называют суперпроектом). Подмодуль — это собственное хранилище. Вы можете зафиксировать, разветвить, перебазировать и т. Д. Из него, как и в любом другом хранилище

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

Настройка

Фрагменты кода, используемые в этой статье, написаны как учебные тесты 1 . Простые тесты могут помочь понять, как работает сторонний код и внедрить новые API. Их можно рассматривать как контролируемые эксперименты, которые позволяют точно узнать, как ведет себя сторонний код.

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

Вернуться к теме: все тесты имеют одинаковые настройки. Смотрите полный исходный код для деталей. Существует пустой репозиторий с именем parent . Рядом с ним находится хранилище библиотеки . Тесты добавят это как подмодуль к родителю. В хранилище библиотеки есть начальный коммит с файлом readme.txt. Метод setUp создает оба репозитория следующим образом:

1
Git git = Git.init().setDirectory( "/tmp/path/to/repo" ).call();

Репозитории представлены через поля parent и библиотеку типа Git. Этот класс обёртывает репозиторий и предоставляет доступ ко всем командам, доступным в JGit. Как я объяснил здесь ранее , каждый класс Command соответствует собственной команде Git pocelain. Для вызова команды используется шаблон компоновщика. Например, результат метода Git.commit () на самом деле является CommitCommand. После предоставления любых необходимых аргументов вы можете вызвать его метод call ().

Добавить подмодуль

Первый и очевидный шаг — добавить подмодуль в существующий репозиторий. Используя настройки, описанные выше, хранилище библиотеки должно быть добавлено как подмодуль в каталог modules / library родительского хранилища.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
@Test
public void testAddSubmodule() throws Exception {
  String uri
    = library.getRepository().getDirectory().getCanonicalPath();
  SubmoduleAddCommand addCommand = parent.submoduleAdd();
  addCommand.setURI( uri );
  addCommand.setPath( "modules/library" );
  Repository repository = addCommand.call();
  repository.close();
 
  F‌ile workDir = parent.getRepository().getWorkTree();
  F‌ile readme = new F‌ile( workDir, "modules/library/readme.txt" );
  F‌ile gitmodules = new F‌ile( workDir, ".gitmodules" );
  assertTrue( readme.isF‌ile() );
  assertTrue( gitmodules.isF‌ile() );
}

SubmoduleAddCommand должен знать о том, откуда следует клонировать подмодуль и где его следует хранить. Атрибут URI (не должен ли он называться URL?) Обозначает местоположение хранилища, из которого будет клонироваться, как это было бы дано команде clone. А атрибут path указывает, в каком каталоге — относительно корневого каталога рабочего каталога родительских репозиториев — должен быть размещен подмодуль. После запуска команд рабочий каталог родительского репозитория выглядит следующим образом:

подмодуль дерево Репозиторий библиотеки находится в каталоге modules / library, и его рабочее дерево извлекается. call () возвращает объект Repository, который вы можете использовать как обычный репозиторий. Это также означает, что вы должны явно закрыть возвращаемый репозиторий, чтобы избежать утечки файловых дескрипторов.

Изображение показывает, что SubmoduleAddCommand сделал еще одну вещь. Он создал файл .gitmodules в корне рабочего каталога родительского репозитория и добавил его в индекс.

1
2
3
[submodule "modules/library"]
path = modules/library
url = [email protected]:path/to/lib.git

Если вы когда-нибудь заглядывали в конфигурационный файл Git, вы узнаете синтаксис. В этом файле перечислены все подмодули, на которые ссылается этот репозиторий. Для каждого подмодуля хранится сопоставление между URL-адресом хранилища и локальным каталогом, в который он был загружен. Как только этот файл зафиксирован и передан, каждый, кто клонирует репозиторий, знает, где взять подмодули (позже об этом).

инвентарь

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

01
02
03
04
05
06
07
08
09
10
11
@Test
public void testListSubmodules() throws Exception {
  addLibrarySubmodule();
 
  Map<String,SubmoduleStatus> submodules
    = parent.submoduleStatus().call();
 
  assertEquals( 1, submodules.size() );
  SubmoduleStatus status = submodules.get( "modules/library" );
  assertEquals( INITIALIZED, status.getType() );
}

Команда SubmoduleStatus возвращает карту всех подмодулей в хранилище, где ключ — это путь к подмодулю, а значение — SubmoduleStatus. С помощью приведенного выше кода мы можем убедиться, что только что добавленный субмодуль действительно существует и ИНИЦИАЛИЗОВАН. Команда также позволяет добавить один или несколько путей для ограничения отчетов о состоянии.

Говоря о статусе, JCit StatusCommand не находится на том же уровне, что и нативный Git. Подмодули всегда обрабатываются так, как если бы команда была запущена с параметром -ignore-submodules = dirty : изменения в рабочем каталоге подмодулей игнорируются.

Обновление субмодуля

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

Чтобы изменить ревизию, вы должны явно обновить подмодуль, как описано здесь:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
@Test
public void testUpdateSubmodule() throws Exception {
  addLibrarySubmodule();
  ObjectId newHead = library.commit().setMessage( "msg" ).call();
 
  File workDir = parent.getRepository().getWorkTree();
  Git libModule = Git.open( new F‌ile( workDir, "modules/library" ) );
  libModule.pull().call();
  libModule.close();
  parent.add().addF‌ilepattern( "modules/library" ).call();
  parent.commit().setMessage( "Update submodule" ).call();
 
  assertEquals( newHead, getSubmoduleHead( "modules/library" ) );
}

Этот довольно длинный фрагмент сначала фиксирует что-то в репозитории библиотеки (строка 4), а затем обновляет субмодуль библиотеки до последней фиксации (строки 7–9).

Чтобы сделать обновление постоянным, субмодуль должен быть зафиксирован (строки 10 и 11). В коммите хранится обновленный идентификатор коммита подмодуля под его именем (модули / библиотека в этом примере). Наконец, вы обычно хотите нажать на изменения, чтобы сделать их доступными для других.

Обновление изменений в подмодулях в родительском репозитории

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

Это то, что решает SubmoduleUpdateCommand . Использование команды без дальнейшей параметризации обновит все зарегистрированные подмодули. Команда клонирует отсутствующие подмодули и извлекает коммит, указанный в конфигурации. Как и в случае с другими командами подмодулей, существует метод addPath () для обновления только подмодулей в пределах указанных путей.

Клонирование репозитория с подмодулями

Тем временем вы, вероятно, получили образец, все, что связано с подмодулями, — это ручной труд. Клонирование репозитория с конфигурацией подмодулей не клонирует подмодули по умолчанию. Но у CloneCommand есть атрибут cloneSubmodules, и установка его в значение true также клонирует настроенные подмодули. Внутренне SubmoduleInitCommand и SubmoduleUpdateCommand выполняются рекурсивно после клонирования (родительского) хранилища и извлечения его рабочего каталога.

Удаление подмодуля

Чтобы удалить подмодуль, вы должны написать что-то вроде

1
git.submoduleRm().setPath( ... ).call();

К сожалению, ни в Git, ни в JGit нет встроенной команды для удаления подмодулей. Надеюсь, это будет решено в будущем. До этого мы должны вручную удалять подмодули. Если вы прокрутите вниз до метода removeSubmodule (), вы увидите, что это не ракетостроение.

Сначала соответствующий раздел субмодулей удаляется из файлов .gitsubmodules и .git / config. Затем подмодульная запись в индексе также удаляется. Наконец, изменения — .gitsubmodules и удаленный подмодуль в индексе — фиксируются, и содержимое подмодуля удаляется из рабочего каталога.

Для каждого субмодуля

Native Git предлагает команду git submodule foreach для выполнения команды оболочки для каждого подмодуля. Хотя JGit точно не поддерживает такую ​​команду, она предлагает SubmoduleWalk . Этот класс можно использовать для перебора подмодулей в хранилище. В следующем примере извлекаются восходящие коммиты для всех подмодулей.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
@Test
public void testSubmoduleWalk() throws Exception {
  addLibrarySubmodule();
 
  int submoduleCount = 0;
  Repository parentRepository = parent.getRepository();
  SubmoduleWalk walk = SubmoduleWalk.forIndex( parentRepository );
  while( walk.next() ) {
    Repository submoduleRepository = walk.getRepository();
    Git.wrap( submoduleRepository ).fetch().call();
    submoduleRepository.close();
    submoduleCount++;
  }
  walk.release();
 
  assertEquals( 1, submoduleCount );
}

С помощью next () можно перейти к следующему подмодулю. Метод возвращает false, если подмодулей больше нет. Когда это сделано с SubmoduleWalk, его выделенные ресурсы должны быть освобождены с помощью вызова release (). Опять же, если вы получаете экземпляр Repository для подмодуля, не забудьте закрыть его.

SubmoduleWalk также можно использовать для сбора подробной информации о подмодулях. Большинство его методов получения относятся к свойствам текущего подмодуля, таким как путь, заголовок, удаленный URL и т. Д.

Синхронизировать удаленные URL

Ранее мы видели, что конфигурации субмодулей хранятся в файле .gitsubmodules в корне рабочего каталога репозитория. Ну, по крайней мере, удаленный URL может быть переопределен в .git / config. И затем есть файл конфигурации самого подмодуля. Это, в свою очередь, может иметь еще один удаленный URL. SubmoduleSyncCommand может использоваться для сброса всех удаленных URL к настройкам в .gitmodules

Как видите, поддержка подмодулей в JGit почти на уровне родного Git. Большинство его команд реализованы или могут быть эмулированы без особых усилий. И если вы обнаружите, что что-то не работает или отсутствует, вы всегда можете обратиться за помощью к дружелюбному и полезному сообществу JGit .

  1. Термин взят из раздела «Изучение и изучение границ» в «Чистом коде» Роберта С. Мартина.