This page last changed on Jul 06, 2009 by smaddox.

On this page:

Crucible SCM Plugins

Crucible SCM modules are plugins that make version control systems accessible to Crucible. An SCM plugin can be used to give Crucible the ability to work with a custom version control system that is not supported out of the box. SCM plugins are independent from FishEye's version control integrations and allow Crucible to run standalone. Crucible ships with a number of built-in SCM plugins, including Subversion and Perforce.

In this section we will implement a new Crucible SCM Plugin and explore Crucible's public SCM API. The example builds a module that exposes the underlying file system as the "repository", so that users can perform reviews of files on the server file system.

Creating a Project

To start, we use the Crucible Plugin archetype to create a new empty Maven2 project:

mvn org.apache.maven.plugins:maven-archetype-plugin:1.0-alpha-7:create \
  -DarchetypeGroupId=com.atlassian.maven.archetypes \
  -DarchetypeArtifactId=crucible-plugin-archetype \
  -DarchetypeVersion=1-SNAPSHOT \
  -DremoteRepositories=https://maven.atlassian.com/repository/public/ \
  -DgroupId=com.atlassian.crucible.example.scm \-DartifactId=example-scm-plugin

This creates a new project that has a dependency on atlassian-fisheye-api. This library contains the basic API components required by plugins. However, as we are building an SCM plugin that can be configured through a servlet, we need to add a dependency on atlassian-crucible-scmutils as well as atlassian-plugins-core by editing the generated pom.xml:

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">

    <parent>
        <groupId>com.atlassian.crucible.plugin.base</groupId>
        <artifactId>crucible-plugin-base</artifactId>
        <version>1-SNAPSHOT</version>
    </parent>

    <modelVersion>4.0.0</modelVersion>
    <groupId>com.atlassian.crucible.example.scm</groupId>
    <artifactId>example-scm-plugin</artifactId>
    <version>1.0-SNAPSHOT</version>

    <name>Example plugin that offers file system based repositories.</name>
    <packaging>atlassian-plugin</packaging>

    <properties>
        <atlassian.plugin.key>com.atlassian.crucible.example.scm.example-scm-plugin</atlassian.plugin.key>
        <atlassian.pdk.server.url>${atlassian.product.url}</atlassian.pdk.server.url>
        <atlassian.product.version>1.6.3-SNAPSHOT</atlassian.product.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.atlassian.crucible</groupId>
            <artifactId>atlassian-crucible-scmutils</artifactId>
            <version>${atlassian.product.version}</version>
        </dependency>
        <dependency>
            <groupId>com.atlassian.plugins</groupId>
            <artifactId>atlassian-plugins-core</artifactId>
            <version>2.0.4</version>
        </dependency>
    </dependencies>
</project>
IDEA Users
If you are using IntelliJ for development, be sure to run mvn idea:idea to generate the project files. Opening the pom file directly is known to miss the parent dependencies.

Crucible SCM Plugin API

Crucible's public API can be browsed online and contains the functionality needed to develop a custom SCM plugin in the package com.atlassian.crucible.scm. It consists of a set of interfaces, some of which are optional, for browsing a repository, accessing its directories, retrieving file contents and exploring changes between revisions.

At the very least, your SCM plugin should implement the com.atlassian.crucible.scm.SCMModule interface that defines the new plugin. The module is then used to create one or more repository instances:

package com.atlassian.scm;

import com.atlassian.crucible.scm.SCMModule;
import com.atlassian.crucible.scm.SCMRepository;
import com.atlassian.plugin.ModuleDescriptor;

import java.util.Collection;
import java.util.Collections;

public class ExampleSCMModule implements SCMModule {

    private ModuleDescriptor moduleDescriptor;
    private List<SCMRepository> repos = Collections.emptyList();

    public String getName() {
        return "Example File System SCM.";
    }

    public Collection<? extends SCMRepository> getRepositories() {
        return repos;
    }

    public void setModuleDescriptor(ModuleDescriptor moduleDescriptor) {
        this.moduleDescriptor = moduleDescriptor;
    }

    public ModuleDescriptor getModuleDescriptor() {
        return moduleDescriptor;
    }
}

When your module is instantiated, Crucible passes a ModuleDescriptor instance to it containing information about the plugin. The getRepositories() method returns the repositories offered by this plugin. Currently we're returning an empty collection.

To be able to use the Crucible administration console to configure our plugin and specifiy the locations of the repositories we want to use, we will also implement the Configurable interface that allows for the injection of a custom configuration bean (by implementing SimpleConfiguration) whose properties can be manipulated through the administration interface for which we will write a small servlet. In our custom configuration bean we'll add a property for the base path or root directory of the file system based repositories we want to offer.

The plugin configuration is written to disk and fed to our SCMModule when Crucible starts up. Our plugin is responsible for generating and parsing that data, so we're free to choose the format. The ModuleConfigurationStore provides persistent storage and will automatically be injected into our plugin if we create a constructor that takes it as an argument. For the serialization, let's use simple XML serialization through XStream (using XStream is convenient as it is one of the dependencies for atlassian-crucible-scmutils):

package com.atlassian.scm;

import com.atlassian.fisheye.plugins.scm.utils.SimpleConfiguration;

public class ExampleConfiguration implements SimpleConfiguration {

    private String name;
    private String basePath;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getBasePath() {
        return basePath;
    }

    public void setBasePath(String basePath) {
        this.basePath = basePath;
    }
}

Now we make the required changes to our SCMModule to read and write the configuration:

public class ExampleSCMModule implements SCMModule, Configurable<List<ExampleConfiguration>> {

    private ModuleDescriptor moduleDescriptor;
    private ModuleConfigurationStore store;

    public ExampleSCMModule(ModuleConfigurationStore store) {
        this.store = store;
    }

    [...]

    public List<ExampleConfiguration> getConfiguration() {
        byte[] configData = store.getConfiguration(moduleDescriptor);
        if (configData != null) {
            try {
                return (List<ExampleConfiguration>)getXStream().fromXML(new String(configData, "UTF8"));
            } catch (Exception e) {
                throw new RuntimeException("Error reading configuration:" + configData, e);
            }
        }
        return new ArrayList<ExampleConfiguration>();
    }

    public void setConfiguration(List<ExampleConfiguration> config) {
        try {
            store.putConfiguration(moduleDescriptor, getXStream().toXML(config).getBytes("UTF8"));
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException("UTF8 encoding not supported", e);
        }
    }

    private XStream getXStream() {
        XStream xstream = new XStream();
        xstream.setClassLoader(moduleDescriptor.getPlugin().getClassLoader());
        return xstream;
    }
    [...]

Now that we have access to the configuration data, which describes the repositories, we can go ahead and implement our file system based repository class.

The SCMRepository interface offers basic functionality for retrieving file contents of specific file revisions. It is queried by Crucible when a user adds files to a review. Depending on the optional interfaces you implement in addition to SCMRepository, your implementation could also have the ability to browse the repository and to explore different versions of each file. Because a standard file system does not store version information, we'll only offer directory browsing in this example. As a revision key or version number we shall simply use the last modification date that is stored by the file system.

package com.atlassian.scm;

import com.atlassian.crucible.scm.SCMRepository;
import com.atlassian.crucible.scm.RevisionData;
import com.atlassian.crucible.scm.RevisionKey;
import com.atlassian.crucible.scm.DetailConstants;
import com.cenqua.crucible.model.Principal;

import java.io.OutputStream;
import java.io.IOException;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.Date;
import java.net.MalformedURLException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;

import org.apache.commons.io.IOUtils;

public class ExampleSCMRepository implements SCMRepository {

    private final ExampleConfiguration config;

    public ExampleSCMRepository(ExampleConfiguration config) {
        this.config = config;
    }

    public boolean isAvailable(Principal principal) {
        return true;
    }

    public String getName() {
        return config.getName();
    }

    public String getDescription() {
        return getName() + " file system repo at: " + config.getBasePath();
    }

    public String getStateDescription() {
        return "Available";
    }

    public RevisionData getRevisionData(Principal principal,
        RevisionKey revisionKey) {
        if (revisionKey.equals(currentKey(revisionKey.getPath()))) {
            File f = getFile(revisionKey.getPath());

            RevisionData data = new RevisionData();
            data.setDetail(DetailConstants.COMMIT_DATE, new Date(f.lastModified()));
            data.setDetail(DetailConstants.FILE_TYPE, f.isDirectory() ? "dir" : "file");
            data.setDetail(DetailConstants.ADDED, true);
            data.setDetail(DetailConstants.DELETED, false);
            try {
                data.setDetail(DetailConstants.REVISION_LINK, f.toURL().toString());
            } catch (MalformedURLException e) {
            }
            return data;
        } else {
            throw new RuntimeException("Revision " + revisionKey.getRevision() + " of file " + revisionKey.getPath() + " is no longer available.");
        }
    }

    public void streamContents(Principal principal, RevisionKey revisionKey,
        OutputStream outputStream) throws IOException {
        if (revisionKey.equals(currentKey(revisionKey.getPath()))) {
            InputStream is = new FileInputStream(getFile(revisionKey.getPath()));
            try {
                IOUtils.copy(is, outputStream);
            } finally {
                IOUtils.closeQuietly(is);
            }
        } else {
            throw new RuntimeException("Revision " + revisionKey.getRevision() + " of file " + revisionKey.getPath() + " is no longer available.");
        }
    }

    public RevisionKey getDiffRevisionKey(Principal principal,
        RevisionKey revisionKey) {
        // diffs are not supported in this example
        return null;
    }

    /**
     * Returns a {@link RevisionKey} instance for the specified file. Because we
     * do not support versioning, the revision string will be set to the file's
     * last modification date.
     *
     * @param path
     * @return
     */
    private RevisionKey currentKey(String path) {
        File f = getFile(path);
        return new RevisionKey(path, createDateFormat().format(new Date(f.lastModified())));
    }

    /**
     * Takes the name of a file in the repository and returns a file handle to the
     * file on disk.
     *
     * @param path
     * @return
     */
    private File getFile(String path) {
        return new File(config.getBasePath() + File.separator + path);
    }

    private DateFormat createDateFormat() {
        return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
    }
}

In the above code, the getRevisionData() method is used by Crucible to retrieve versioning properties for a specific revision of a file in the repository. Although the file system does not keep track of older versions, we can provide some of the properties. Most important are the predefined constants DetailConstants.FILE_TYPE, DetailConstants.ADDED, DetailConstants.DELETED (the last two indicate whether the file was newly created (ADDED), or has been removed from the repository (DELETED) as part of the revision) and DetailConstants.REVISION_LINK. In addition to the predefined constants, a repository implementation is free to add custom properties.

We are not able to implement getDiffRevisionKey() due to the lack of version information on the file system.

Before we continue to extend the functionality of the ExampleSCMRepository, we should go back to ExampleSCMModule and implement getRepositories():

[...]

    // initialize at null to trigger loading from the configuration
    private List<SCMRepository> repos = null;

    public synchronized Collection<SCMRepository> getRepositories() {
        if (repos == null) {
            repos = new ArrayList<SCMRepository>();
            for (ExampleConfiguration config : getConfiguration()) {
                repos.add(new ExampleSCMRepository(config));
            }
        }
        return repos;
    }

    public void setConfiguration(List<ExampleConfiguration> config) {
        try {
            store.putConfiguration(moduleDescriptor, xstream.toXML(config).getBytes("UTF8"));
            // we're given a new configuration, so reset our repositories:
            repos = null;
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException("UTF8 encoding not supported", e);
        }
    }

    [...]

Our SCMModule now properly creates the repository instances according to the configuration.

The above code gives us a very simple Crucible SCM plugin. However you would normally also want to implement the com.atlassian.crucible.scm.DirectoryBrowser and com.atlassian.crucible.scm.HasDirectoryBrowser interfaces. The DirectoryBrowser gives Crucible the ability to let the user interactively browse the repository and select files to review. If you do not provide a DirectoryBrowser, the only way to create a review for files in your repository is when the required files and file revisions are known up front.

In this example, we'll implement DirectoryBrowser:

public class FileSystemSCMRepository implements HasDirectoryBrowser, DirectoryBrowser {

    [...]

    public DirectoryBrowser getDirectoryBrowser() {
        return this;
    }

    public List<FileSummary> listFiles(Principal principal, String path) {
        List<FileSummary> files = new ArrayList<FileSummary>();
        for (String p : list(path, true)) {
            files.add(new FileSummary(currentKey(p)));
        }
        return files;
    }

    public List<DirectorySummary> listDirectories(Principal principal, String path) {
        List<DirectorySummary> files = new ArrayList<DirectorySummary>();
        for (String p : list(path, false)) {
            files.add(new DirectorySummary(p));
        }
        return files;
    }

    public FileHistory getFileHistory(Principal principal, String path) {
        return new FileHistory(Collections.singletonList(currentKey(path)));
    }

    private List<String> list(String path, boolean returnFiles) {
        File parent = getFile(path);
        List<String> files = new ArrayList<String>();
        if (parent.isDirectory()) {
            File[] children = parent.listFiles();
            // this may be null if we can't read the directory, for instance.
            if (children != null) {
                for (File f : children) {
                    if (f.isFile() && returnFiles || f.isDirectory() && !returnFiles) {
                        files.add(getPath(f));
                    }
                }
            }
        }
        return files;
    }

    /**
     * @return the path for a given File relative to the base configured for this
     *         repository -- the path doesn't include the base component.
     */
    private String getPath(File file) {
        String s = file.getAbsolutePath();
        if (!s.startsWith(config.getBasePath())) {
            throw new RuntimeException("Invalid file with path " + s + " is not under base " + config.getBasePath());
        }
        return s.substring(config.getBasePath().length() + 1);
    }

    [...]

This is as far as we can go with the file system. In most cases you will be integrating version control systems that keep track of all previous revisions of the resources in the repository and you would expose this to Crucible by also implemening HasChangelogBrowser and ChangelogBrowser.

Servlet Based Administration Pane

With the code for the module and the repository in place, we can focus on our servlet that provide plugin administration in Crucible's administration section. The easiest way to do this is to subclass com.atlassian.fisheye.plugins.scm.utils.SimpleConfigurationServlet and implement the three abstract methods:

package com.atlassian.crucible.example.scm;

import com.atlassian.fisheye.plugins.scm.utils.SimpleConfigurationServlet;
import com.atlassian.plugin.PluginAccessor;
import com.atlassian.crucible.spi.FisheyePluginUtilities;

public class ExampleSCMConfigServlet extends SimpleConfigurationServlet<ExampleConfiguration> {

    public ExampleSCMConfigServlet(PluginAccessor pluginAccessor,
        FisheyePluginUtilities fisheyePluginUtilities) {
        super(pluginAccessor, fisheyePluginUtilities);
    }

    protected ExampleConfiguration defaultConfig() {
        return new ExampleConfiguration();
    }

    protected String getProviderPluginModuleKey() {
        return "com.atlassian.crucible.example.scm.example-scm-plugin:scmprovider";
    }

    protected String getTemplatePackage() {
        return "/examplescm-templates";
    }
}

The getTemplatePackage() method returns the name of the resource directory that contains the velocity templates that determine how the configuration pane will be rendered. The template directory must be in src/main/resources so Crucible can find them. We'll create three different pages: one that lists the current configuration list.vm, one to edit a repository's configuration edit.vm and one that is displayed when the user tries to manipulate a non-existing repository instance (nosuchrepo.vm):

src/main/resource/examplescm-templates/list.vm
<html>
<head>
    <link rel="stylesheet" href="$request.contextPath/$STATICDIR/main.css" type="text/css" />
</head>
<body class="plugin">
<div class="box formPane">
<table class="adminTable">
#if ($configs.empty)
        <tr><td>No File System repositories are configured.</td></tr>
#else
    <tr>
        <th>Name</th>
        <th>Base Path</th>
        <th><!-- for edit link --></th>
        <th><!-- for delete link --></th>
    </tr>
    #foreach ($config in $configs)
    <tr>
        <td>$config.name</td>
        <td>$config.basePath</td>
        <td><a href="./examplescm?name=$config.name">Edit</a></td>
        <td><a href="./examplescm?name=$config.name&amp;delete=true">Delete</a></td>
    </tr>
    #end
#end
    <tr>
        <td class="verb"><a href="./examplescm?name=_new">Add a repository.</a></td>
    </tr>
</table>
</div>
</body>
</html>
src/main/resource/examplescm-templates/edit.vm
<html>
<head>
    <link rel="stylesheet" href="$request.contextPath/$STATICDIR/main.css" type="text/css" />
</head>
<body class="plugin">
<div class="box formPane">
<form action="./examplescm" method="POST">
    #if ($config.name)
    <input type="hidden" name="name" value="$!config.name"/>
    #end
    <table class="adminTable">
         #if ($errorMessage)
        <tr><td colspan="2"><span class="errorMessage">$errorMessage</span></td></tr>
        #end
        <tr>
            <td class="tdLabel"><label class="label">Name:</label></td> <td><input
            #if ($config.name)
                disabled="true"
            #else
                name="name"
            #end
            type="text"  value="$!config.name"/> </td>
        </tr>
        <tr>
            <td class="tdLabel"><label class="label">Base Path:</label></td> <td><input type="text" name="basePath" value="$!config.basePath"/> </td>
        </tr>
        <tr>
            <td colspan="2">
                <input type="submit" value="Save"/>
            </td>
        </tr>
    </table>
</form>
</div>
</body>
</html>
src/main/resource/examplescm-templates/nosuchrepo.vm
<html>
<head>
    <link rel="stylesheet" href="$request.contextPath/$STATICDIR/main.css" type="text/css" />
</head>
<body class="plugin">
<p>
There is no repository named '$name'.
</p>
</body>
</html>

Finally we tie everything together in the mandatory atlassian-plugin.xml file that describes the new plugin, contains its name, location of the servlet and the classnames Crucible uses to instantiate the components. Because this is an SCM plugin, we must add the <scm/> element:

src/main/resources/atlassian-plugin.xml
<atlassian-plugin key="${atlassian.plugin.key}" name="example-scm-plugin" plugins-version="2">
    <plugin-info>
        <description>An example SCM provider for the local file system</description>
        <vendor name="Atlassian" url="http://www.atlassian.com"/>
        <version>1.0-SNAPSHOT</version>
        <param name="configure.url">/plugins/servlet/examplescm</param>
    </plugin-info>

    <scm name="Example File System SCM" key="scmprovider" class="com.atlassian.crucible.example.scm.ExampleSCMModule">
        <description>Example SCM implementation for local file system</description>
    </scm>

    <servlet name="Example File System SCM Configuration Servlet" key="configservlet" class="com.atlassian.crucible.example.scm.ExampleSCMConfigServlet" adminLevel="system">
        <description>Allows Configuration of File System example SCM Plugin</description>
        <url-pattern>/examplescm</url-pattern>
    </servlet>
</atlassian-plugin>

Packaging, Deploying and Running

Now we can package everything up using mvn package and you should end up with target/example-scm-plugin-1.0-SNAPSHOT.jar that can be deployed in Crucible by copying the jar file to the CRUCIBLE_HOME/var/plugins/user directory. Then login to the administration section, go to Plugins and click the link "Check for new plugins in...". This should detect your plugin and add it to the list in "disabled" state as illustrated below:

Screenshot: Detecting Your Plugin

Next, click "Configure" to create a file system based repository:

Screenshot: Creating a File-System Based Repository

When the repository is created, navigate to "Repository List". Our custom Crucible SCM Plugin will now show up in the list and is ready to use:

Screenshot: The Custom SCM Plugin in Crucible

When reviewing files from the plugin repository, click on the "Manage Files" tab in a new or existing review and then select the repository from the pull down list and select the files and revisions you want to review:

Screenshot: Selecting Files and Revisions for Review


Document generated by Confluence on Jul 09, 2009 19:51