Continuing this series of posts on Gitolite hooks, this time I will be adding live project documentation to Redmine, using Doxygen.

images/2012-11-18/redmine-documentation.png

Background.

Documentation is often as important as code itself, especially when working in a team or introducing new programmers.

But keeping generated documentation up to date is a pain, and it really shouldn't be.

At (REDACTED) we use Redmine as our project/task tracker. From here our developers have access to relevant projects with the ability to directly view the repository for that project, so why not add a "Documentation Tab" too?

Choosing a generator.

After evaluating a couple of doc generators we settled on the tried and true classic, Doxygen.

We use several different languages across our projects (most extensively Objective-C, Java, and PHP), so we needed something that is comfortable with all of them. Thankfully Doxygen works well with all of them and more. It is also quite flexible in it's output including support for HTML and PDF (via LaTeX), as well as compiled help files for Eclipse and Xcode.

A Doxygen hook for Gitolite.

So first things first, we need a hook to generate this output before we can use it in Redmine.

generate-documentation

#!/bin/bash
read oldrev newrev refname

# Get project name from directory name
PROJECT=$(basename "$PWD")

# Remove .git from the end of the name
PROJECT=${PROJECT%.git}

# Checkout directory
CHECKOUT_DIR=/tmp/git/${PROJECT}

# Where the docs should reside
DOCS_DIR=/srv/documentation/${PROJECT}

# Create the checkout directory
mkdir -p ${CHECKOUT_DIR}
chmod -R 775 ${CHECKOUT_DIR}

# Remove and recreate docs directory
rm -Rf ${DOCS_DIR}
mkdir -p ${DOCS_DIR}
chmod -R 775 ${DOCS_DIR}

# Checkout this website
GIT_WORK_TREE=${CHECKOUT_DIR} git checkout -q -f ${newrev}

# At this stage we can access the project at $CHECKOUT_DIR
# Docs should be built to $DOCS_DIR

doxygen - 2>&1 >/dev/null <<EOF
#
# You will likely want to customise these values
# a bit to suit your shop.
#

QUIET = YES
RECURSIVE = YES

# Exclude third party code from generated docs
EXCLUDE_PATTERNS = */libs/* */thirdparty/* */third-party/*

# Extract all code, including undocumented stuff
EXTRACT_ALL = YES

JAVADOC_AUTOBRIEF = YES

INPUT = ${CHECKOUT_DIR}
OUTPUT_DIRECTORY = ${DOCS_DIR}

# Use a short SHA-1 has as project number
PROJECT_NUMBER = "$(git rev-parse --short ${newrev})"
PROJECT_NAME = "${PROJECT}"

# Use a relative path
FULL_PATH_NAMES = YES
STRIP_FROM_PATH = ${CHECKOUT_DIR}

GENERATE_HTML = YES
GENERATE_LATEX = NO
GENERATE_MAN = NO
HTML_OUTPUT = ${DOCS_DIR}

# Customise output style a bit
HTML_COLORSTYLE_HUE = 120
HTML_COLORSTYLE_SAT = 80
HTML_DYNAMIC_SECTIONS = YES
HTML_TIMESTAMP = NO
HAVE_DOT = YES
GENERATE_TREEVIEW = YES
DISABLE_INDEX = YES

SEARCHENGINE = NO
SOURCE_BROWSER = YES
INLINE_SOURCES = YES
TAB_SIZE = 2
EOF

# Print success
echo -e "\e[1;32mDone.\e[00m"

Remember that this goes into local-code/hooks/common/hooks.d/, and make sure it's executable.

Next up we need to give Redmine the ability to view/serve it.

The Redmine Documentation plugin.

In order to serve the documentation I needed to write a Redmine plugin. Redmine is written using Rails, which actually makes this pretty straight forward.

DISCLAIMER: This implementation is directly serving files from the filesystem based on URL which could be considered dangerous. In my tests I was unable to perform any directory traversal.

The plugin consists of four main parts.

init.rb

  # Regular plugin code is here.
  ...

  # We need to add this stuff below to add our "Documentation" tab per project.
  permission :documentation, { :documentation => [:index, :serve] }, :public => true
  menu :project_menu, :documentation, { :controller => 'documentation', :action => 'index' }, { :caption => 'Documentation', :after => :repository }
end

Next up are the routes that tell Rails how to route a URL to our controller.

config/routes.rb

RedmineApp::Application.routes.draw do
  get '/projects/:id/documentation', :to => 'documentation#index'

  # Use route globbing here to serve documentation directly.
  get '/projects/:id/documentation/*file', { :to => 'documentation#serve', :format => false }
end

Now for the controller itself.

app/controllers/documentation_controller.rb

class DocumentationController < ApplicationController
  unloadable

  def index
    @project = Project.find(params[:id])
  end

  def serve
    # Find the first repository for this project.
    @repository = Project.find(params[:id]).repositories.first

    # Get the repository name from this.
    # Originally it will look like /path/to/repo.git/ and
    # we want to turn it into "repo".
    repoName = File.basename(@repository.root_url, File.extname(@repository.root_url))

    # Serve documentation straight from this path.
    path = "/srv/documentation/#{ repoName }/#{ params[:file] }"

    # Use send_file for slightly better performance.
    send_file path, :disposition => 'inline'
  end
end

And now finally the index view.

app/views/documentation/index.html.erb

<% html_title("Documentation") %>
<h2>Documentation</h2>
<iframe src="/projects/<%= @project.id %>/documentation/index.html" style="width: 100%; height: 1024px; border: none"></iframe>

As mentioned before we are using send_file which optionally uses the X-Accel-Redirect or X-Sendfile headers to accelerate downloads so you will want to make sure that your web server is set up for this. See wiki.nginx.org/XSendfile for more information.

Wrapping up.

With both of these installed and Redmine restarted you should be ready to serve up live generated documentation for your Redmine projects!