
Наконец-то я "раздобыл" немного свободного времени, а значит пришла пора продолжить серию туториалов по XWiki.
После публикации одной из моих статей MaxK82 спросил у меня, можно ли как-то в XWiki подключить документацию из git репозитория, так чтобы наладить её версионирование.
К сожалению эта статья не ответит на его вопрос, но возможно укажет направление, в котором стоит "копать".
Поэтому сегодня мы с вами:
Cразу дам дисклеймер - я техпис, а не программист. Поэтому не стоит ожидать в стать хороших примеров кода и удачных решений. Это скорее материал для других технических писателей или может быть docops, которым выпадет честь работать с XWiki.
Уделим пару минут очевидному:
Для туториала нам понадобится аккаунт в Gitlab (не важно коммерческий или на вашем сервере).
Нужно будет получить access token к аккаунту.
Нужен будет репозиторий, я использую "Learn GitLab" (не помню откуда он у меня взялся). Но вам подойдет любой репозиторий в котором есть Readme.md (или другой markdown файл). Логика будет похожей.
В своей статье я использовал docker версию XWiki 13.10.5 со Standart Flavor, но работать будет и в XWiki 12.X.
-
Нам понадобится установить расширение GitAPI.
Ну и самое главное, нам нужно будет установить расширение поддержки синтаксиса Markdown.

.
Вообще у Xwiki есть разные варианты интеграции с git, например импорт с GitHub wiki или вот просто подключение к GitHub через его API.
Возможно Вам удастся найти готовые примеры и решения.
Сразу скажу, что в этот раз мы сделаем очень топорный макрос, его прообраз мне был нужен для того чтобы коллегам не пришлось добавлять домен с XWiki в настройки CORS нашего GitLab.
Прошу вас рассматривайте этот туториал скорее как демонстрацию, а не как четкое руководство к действию, потому что клонировать целый репозиторий на сервер XWiki ради 1 markdown файла может быть не лучшей идеей.
Пару слов о том, что такое макрос.
Макрос в Xwiki - это набор инструкций, который может быть включен в контент страницы. Если вы использовали Conflunce, то вам это знакомо.
Создать свой макрос в XWiki очень просто, для этого надо следовать инструкции.
Для начала создадим новую страницу, можно прямо в корне. Название страницы не критично, контент можно не размещать.

Но если вы возьметесь за дело всерьез, то я бы рекомендовал размещать макросы в каком-то одном месте (например, внутри страницы "Macros").
Далее перейдите в редактор объектов. Можно просто нажать латинскую клавишу "O" на странице после её создания (если чудо не произошло, посмотрите как включить продвинутый режим редактора в одной из прошлых статей).
Страница станет макросом, как только мы добавим ей объект WikiMacroClass

Это по сути и есть сам макрос, но прежде, чем перейти к его детальному описанию, добавим еще несколько объектов с переменными для макросаWikiMacroParameterClass
на страницу.

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

Мы задаем путь к репозиторию в git.
Название параметра (GIT_URL), то как будет называться поле в UI пользователя.
Описание параметра - текст подсказки к параметру (ссылки работать не будут).
Обязательность параметра - если да, то макрос не создаться без ввода значения. В первом случае параметр обязательный.
Значение по умолчанию, если задать, то поле будет предзаполненно.
Тип параметра - формат переменной, я так понимаю основные типы JAVA работают нормально, в данном случае String. Если честно, я уже точно не помню, как это работает, поэтому оставим так.
Скриншоты с другими параметрами я спрячу под спойлер, чтобы не раздувать статью.
Остальные параметры





Если боитесь, что что-то забыли можно проверить себя на скриншоте UI макроса в редакторе страницы.
Так же страничку можно забрать из GitHub (вы можете её импортировать через панель администратора в разделе "Контент").
Вернемся к Макросу.

ID макроса - то как он будет называться в режиме редактора исходного кода.
Название макроса - то как будет отображаться в UI пользователя.
Описание - подсказка для пользователя.
Категория - категория для фильтров по типам макросов.
Можно ли вставить в линию - если нет, то нужен будет отступ в редакторе исходного кода.
Видимость макроса - в каких границах можно использовать (у нас в рамках текущей вики).
Можно ли вставить контент в макрос - в нашем случае нет.
Тип контента - выберите Wiki.
Осталось загрузить непосредственно код макроса.
На всякий случай сохраните ваш труд и вздохните с облегчением.
Добавим клонирование и удаление репозитория
Перейдем непосредственно к коду.
Полный код под спойлером.
{{groovy}}
import org.apache.commons.io.*
import org.eclipse.jgit.api.*
import org.eclipse.jgit.lib.*
import org.eclipse.jgit.revwalk.*
import org.eclipse.jgit.storage.file.*
import org.eclipse.jgit.transport.*;
import org.xwiki.environment.*;
import org.apache.commons.io.FilenameUtils;
import groovy.io.*;
import org.apache.commons.io.FileUtils;
Git git;
def CredentialsProvider getCredentialsProvider() {
return new UsernamePasswordCredentialsProvider("UXXXX", "glXXXt-XXXXXXXXXXXXXXXXXXXXX")
}
def service = services.get("git");
def Repository getRepository(String repositoryURI, String localDirectoryName) {
Repository repository;
Environment environment = services.component.getInstance(Environment);
File permDir = environment.getPermanentDirectory();
File localGitDirectory = new File(permDir, "git")
File localDirectory = new File(localGitDirectory, localDirectoryName);
File gitDirectory = new File(localDirectory, ".git");
// println "Local Git repository is at [${gitDirectory}]"
FileRepositoryBuilder builder = new FileRepositoryBuilder();
try {
// Step 1: Initialize Git environment
repository = builder.setGitDir(gitDirectory)
.readEnvironment()
.findGitDir()
.build();
Git git = new Git(repository);
// Step 2: Verify if the directory exists and isn't empty.
if (!gitDirectory.exists()) {
// Step 2.1: Need to clone the remote repository since it doesn't exist
git.cloneRepository()
.setCredentialsProvider(getCredentialsProvider())
.setDirectory(localDirectory)
.setURI(repositoryURI).setCloneAllBranches(false).setBranch(wikimacro.parameters.GIT_TREE)
.call();
}
} catch (Exception e) {
throw new RuntimeException(String.format("Failed to execute Git command in [%s]", gitDirectory), e);
}
return repository;
}
def sourceRepoURL = wikimacro.parameters.GIT_URL;
def sourceRepoName = org.apache.commons.io.FilenameUtils.getName("/" + wikimacro.parameters.GIT_LOCAL_PATH);
// delete folder before cloning, for clean pull.
if (wikimacro.parameters.DELETE_FOLDER)
{
def file = new File("../xwiki/data/git/"+sourceRepoName)
if (file.exists()){
FileUtils.deleteDirectory(file)
}
}
def textFilePath = wikimacro.parameters.TEXT_FILE_PATH;
def shortFileName = org.apache.commons.io.FilenameUtils.getName(textFilePath)
def repo = getRepository(sourceRepoURL, sourceRepoName)
result = new Git(repo).pull().setCredentialsProvider(getCredentialsProvider()).call()
def baseUrl = repo.getWorkTree().toString();
def fileName =org.apache.commons.io.FilenameUtils.separatorsToSystem(baseUrl + "/" + textFilePath);
def String fileContents = new File(fileName).getText('UTF-8');
xcontext.put("fileContent", fileContents)
{{/groovy}}
{{velocity}}
#set($fileText =$xcontext.get("fileContent"))
{{content syntax="$wikimacro.parameters.OUTFORMAT.trim()"}}
$fileText
{{/content}}
{{/velocity}}
В целом я не на 100% понимаю этот код, так как часть решений заимствовал. Но постараюсь объяснить что смогу.
{{groovy}}
import org.apache.commons.io.*
import org.eclipse.jgit.api.*
import org.eclipse.jgit.lib.*
import org.eclipse.jgit.revwalk.*
import org.eclipse.jgit.storage.file.*
import org.eclipse.jgit.transport.*;
import org.xwiki.environment.*;
import org.apache.commons.io.FilenameUtils;
import groovy.io.*;
import org.apache.commons.io.FileUtils;
Git git;
Импортируем зависимости.
def CredentialsProvider getCredentialsProvider() {
return new UsernamePasswordCredentialsProvider("UXXXX", "glXXXt-XXXXXXXXXXXXXXXXXXXXX")
}
Функция для авторизации в GitLab, замените UХХХХ на имя пользователя, а glXX-XXX на access token.
Позаимствованный кусок под спойлером
def service = services.get("git");
def Repository getRepository(String repositoryURI, String localDirectoryName) {
Repository repository;
Environment environment = services.component.getInstance(Environment);
File permDir = environment.getPermanentDirectory();
File localGitDirectory = new File(permDir, "git")
File localDirectory = new File(localGitDirectory, localDirectoryName);
File gitDirectory = new File(localDirectory, ".git");
// println "Local Git repository is at [${gitDirectory}]"
FileRepositoryBuilder builder = new FileRepositoryBuilder();
try {
// Step 1: Initialize Git environment
repository = builder.setGitDir(gitDirectory)
.readEnvironment()
.findGitDir()
.build();
Git git = new Git(repository);
// Step 2: Verify if the directory exists and isn't empty.
if (!gitDirectory.exists()) {
// Step 2.1: Need to clone the remote repository since it doesn't exist
git.cloneRepository()
.setCredentialsProvider(getCredentialsProvider())
.setDirectory(localDirectory)
.setURI(repositoryURI).setCloneAllBranches(false).setBranch(wikimacro.parameters.GIT_TREE)
.call();
}
} catch (Exception e) {
throw new RuntimeException(String.format("Failed to execute Git command in [%s]", gitDirectory), e);
}
return repository;
}
Эта функция, как я понимаю непосредственно клонирует репозиторий.
def sourceRepoURL = wikimacro.parameters.GIT_URL;
def sourceRepoName = org.apache.commons.io.FilenameUtils.getName("/" + wikimacro.parameters.GIT_LOCAL_PATH);
Сохраняем и обрабатываем параметры с URL из параметров макроса.
// delete folder before cloning, for clean pull.
if (wikimacro.parameters.DELETE_FOLDER)
{
def file = new File("../xwiki/data/git/"+sourceRepoName)
if (file.exists()){
FileUtils.deleteDirectory(file)
}
}
Если включен параметр "Удалять папку", то мы ее удаляем.
../xwiki/data/git/ - я уже
плохо помню, но скорее всего это относительный путь, куда Xwiki по умолчанию складывает репозитории.
def textFilePath = wikimacro.parameters.TEXT_FILE_PATH;
def shortFileName = org.apache.commons.io.FilenameUtils.getName(textFilePath)
def repo = getRepository(sourceRepoURL, sourceRepoName)
result = new Git(repo).pull().setCredentialsProvider(getCredentialsProvider()).call()
def baseUrl = repo.getWorkTree().toString();
def fileName =org.apache.commons.io.FilenameUtils.separatorsToSystem(baseUrl + "/" + textFilePath);
def String fileContents = new File(fileName).getText('UTF-8');
Получаем данные из репозитория и сохраняем их в переменную.
xcontext.put("fileContent", fileContents)
{{/groovy}}
Отправляем содержимое переменной в общий контекст, чтобы забрать из макроса на Velocity.
По идее это не обязательно, так как должен был сработать обмен переменными между языками, но у меня что-то пошло не так.
На этом кусок Groovy кода заканчивается.
И начинается небольшой кусочек кода на Velocity.
Код на Velocity нам нужен потому, что из него можно вызывать другие макросы XWiki, в данном случае макрос Content, который позволит нам отобразить наш текст в выбранном формате. Обратите внимание, как вставляется переменная макроса (OUTFORMAT) в Velocity.
Кстати переменные макроса было не обязательно писать с больших букв, это моя прихоть.
{{velocity}}
#set($fileText =$xcontext.get("fileContent"))
{{content syntax="$wikimacro.parameters.OUTFORMAT.trim()"}}
$fileText
{{/content}}
{{/velocity}}
Ну вот и всё.
Остается сохранить и проставить настройки как на скриншоте.

Наслаждаемся результатом
Создадим еще страницу и разместим на ней наш свеженький макрос.


Сохраняем результат и проверяем.


Обратите внимание, что все относительные пути в ссылках и у картинок, сломаются. Лучше использовать абсолютные.
Ну вот и всё. Я понимаю, что пример получился не самым лучшим, потому что я уже сам подзабыл как работает часть кода и полей, но он демонстрирует разные приемы работы с XWiki и надеюсь, что будет кому-то полезен в условиях недостатка документации на русском языке.
P.S. Изначально вместо этой статьи я хотел написать материал о моём субъективном опыте одновременного использования XWiki и Confluence в качестве базы знаний для команды разработки.
Однако, в итоге решил, что человечество к такому пока еще не готово.
Но если вы все же хотите почитать материал на тему Confluence vs Xwiki напишите об этом в комментариях.