Live project documentation in Redmine
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!