“Attack of the Clones” or writing a macro to clone a Gitlab repository inside Xwiki

Finally, I “got” some free time, which means it’s time to continue the XWiki tutorial series.

After one of the articles, MaxK82 asked me if it was possible to somehow include documentation from the git repository in XWiki so that it could be versioned.
Unfortunately this article not answer his questionbut it may indicate the direction in which it is worth “digging”.

Therefore, today we are with you:

Ladies at once disclaimerI’m a techie, not a programmer.
Therefore, you should not expect good code examples and good solutions in the article.
It’s more of a material for other technical writers, or maybe docops who will have the honor of working with XWiki.

Let’s take a couple of minutes for the obvious:

  1. For the tutorial, we need a Gitlab account (it doesn’t matter if it’s commercial or on your server).

  2. You will need to get an access token to your account.

  3. You will need a repository, I use “Learn GitLab” (I don’t remember where I got it from). But any repository that has a Readme.md (or other markdown file) will do. The logic will be the same.

  4. In my article, I used the docker version of XWiki 13.10.5 with Standard Flavor, but it will also work in XWiki 12.X.

  5. We need to install the extension GitAPI.

And most importantly, we will need to install the extension Markdown syntax support.

.

In general, Xwiki has various options for integrating with git, such as importing from GitHub wiki or just connect to GitHub via its API.
You may be able to find ready-made examples and solutions.

I’ll tell you right away what we’ll do this time very clumsy macro, I needed its prototype so that my colleagues would not have to add the domain with XWiki to the CORS settings of our GitLab.

I ask you to treat this tutorial as more of a demo than a clear guide, because cloning an entire repository to an XWiki server for 1 markdown file might not be the best idea.

A few words about what a macro is.
A macro in Xwiki is a set of instructions that can be included in the content of a page. If you’ve used Conflunce, then this is familiar to you.

Creating your own macro in XWiki is very easy, all you have to do is follow instructions.

To begin with, let’s create a new page, you can right in the root.
The name of the page is not critical, the content can be left out.

Creating a Blank Page for a Macro
Creating a Blank Page for a Macro

But if you’re serious, I would recommend placing macros in one place (for example, inside the “Macros” page).

Next, go to the object editor. You can just press the latin key “O” on the page after it is created
(if the miracle did not happen, see how to enable advanced editor mode in one of the past articles).

The page will become a macro as soon as we add an object to it WikiMacroClass

This is essentially the macro itself, but before moving on to its detailed description, let’s add a few more objects with variables for the macroWikiMacroParameterClass to the page.

Create 6 objects with variables, they will allow us to control the macro from the user interface.

We will analyze the first one in more detail, and I will give the rest as screenshots below.

We set the path to the repository in git.

  • Name of the parameter (GIT_URL), how the field will be called in the user’s UI.

  • Parameter description – hint text for the parameter (links will not work).

  • Mandatory parameter – if yes, then the macro will not be created without entering a value. In the first case, the parameter is required.

  • The default value, if set, the field will be pre-filled.

  • The parameter type is a variable format, as I understand it, the main JAVA types work fine, in this case String. To be honest, I don’t remember exactly how it works, so let’s leave it like that.

I will hide screenshots with other parameters under the spoiler so as not to inflate the article.

Other parameters

If you are afraid that you forgot something, you can check yourself in the screenshot Macro UI in page editor.
You can also take the page from GitHub (you can import it through the admin panel in the “Content” section)

Back to Macro

Macro settings
Macro settings
  • Macro ID – how it will be called in source code editor mode.

  • Macro name – how it will be displayed in the user’s UI

  • Description – a hint for the user.

  • Category – category for filters by macro types.

  • Is it possible to insert in line – if not, then indentation will be needed in the source code editor.

  • Visibility of a macro – within what limits it can be used (with us within the current wiki)

  • Is it possible to insert content into a macro – in our case, no

  • Content Type – Select Wiki.

It remains to download the macro code directly.
Just in case, save your work and breathe a sigh of relief.

Let’s add cloning and removal of the repository

Let’s go directly to the code.

Full code under the spoiler.
{{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}}

In general, I do not 100% understand this code, as I borrowed some of the solutions. But I’ll try to explain what I can.

{{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;

Importing dependencies.

def CredentialsProvider getCredentialsProvider() {
  return new UsernamePasswordCredentialsProvider("UXXXX", "glXXXt-XXXXXXXXXXXXXXXXXXXXX")
}

Function for authorization in GitLab, replace UХХХХ with username and glXX-XXX with access token.

Borrowed piece under the spoiler
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;
}
 

This function, as I understand it, directly clones the repository.

def sourceRepoURL = wikimacro.parameters.GIT_URL;
def sourceRepoName = org.apache.commons.io.FilenameUtils.getName("/" + wikimacro.parameters.GIT_LOCAL_PATH);

We save and process URL parameters from macro parameters.

// 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)
  }
}

If the “Delete folder” option is enabled, then we delete it.

../xwiki/data/git/ - я уже I don’t remember well, but it’s most likely a relative path where Xwiki puts repositories by default.

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');

We get data from the repository and save it to a variable.

xcontext.put("fileContent", fileContents)

{{/groovy}}

We send the contents of the variable to the general context in order to pick it up from the macro on Velocity.
In theory, this is not necessary, since the exchange should have worked
variables between languagesbut something went wrong with me.

This is where the piece of Groovy code ends.

And a small piece of code on Velocity begins.

We need the Velocity code because we can call other XWiki macros from it, in this case the Content macro, which will allow us to display our text in the chosen format. Notice how the macro variable (OUTFORMAT) is inserted into Velocity.

By the way, it was not necessary to write macro variables in capital letters, this is my whim.

{{velocity}}
#set($fileText =$xcontext.get("fileContent"))
  {{content syntax="$wikimacro.parameters.OUTFORMAT.trim()"}}
     $fileText
  {{/content}}
{{/velocity}}

That’s it.

It remains to save and put down the settings as in the screenshot.

We enjoy the result

Let’s create another page and place our fresh macro on it.

Macro selection window
Macro selection window
Macro Options
Macro Options

We save the result and check.

GitLab
GitLab
xwiki
xwiki

Please note that all relative paths in links and images will break. Better to use absolute.

That’s it. I understand that the example turned out to be not the best, because I myself have forgotten how part of the code and fields work, but it demonstrates different methods of working with XWiki and I hope that it will be useful to someone in the absence of documentation in Russian.

PS Initially, instead of this article, I wanted to write material about my subjective experience of using XWiki and Confluence at the same time as a knowledge base for the development team.
However, in the end I decided that humanity is not yet ready for this.
But if you still want to read material on the topic Confluence vs Xwiki, write about it in the comments.

Similar Posts

Leave a Reply