This short guide explains the main report concepts by using illustrative examples scripts. The API reference is available here.

Language

pepper's report scripts are written in Lua. There's extensive online information about the programming language, including a detailed introduction and a reference manual.

Script layout

Report scripts should encapsulate all of their code, except from module imports, in functions. The function which will be used to run the report is mandatory and is called run. A single argument will be passed to it - the current report context. Meta-data like the report title, description or accepted options will be queried by calling describe().

A skeleton script might look light the following:

-- Meta-data
function describe()
    local r = {}
    r.title = "Skeleton"
    r.description = "Just a skeleton"
    return r
endf

-- Entry point with report context "self"
function run(self)
    -- Nothing here
end

Using repository information

Report scripts can use the their context to access the current environment The repository object provides numerous functions to access the source code repository selected by the user. The following code will print the repository's type, its location and the current head revision.

function run(self)
    local repository = self:repository()
    print(repository:type() .. " repository at " .. repository:url())
    print("HEAD is " .. repository:head())
end

Examining a revision range

A revision range can be accessed using a revision iterator obtained by calling the repository's iterator() function. The iterator's revisions() function can be used as a standard Lua iterator as shown in the code below. Furthermore, a map() function is provided that maps a callback function to every revision.

The following code will gather information about the number of commits and the number of authors on a branch.

-- Entry point
function run(self)
    local repository = self:repository()
    local branch = repository:default_branch()

    local authors = {} -- Dictionary to mark authors
    local num_authors = 0
    local num_commits = 0

    -- Initialize iterator and loop over all revisions
    local iterator = repository:iterator(branch)
    for revision in iterator:revisions() do
        -- Check if the author has already been counted
        if authors[revision:author()] == nil then
            num_authors = num_authors + 1
            authors[revision:author()] = true
        end

        num_commits = num_commits + 1
    end

    -- Print result
    print("Commits on branch " .. branch .. ":")
    print(num_commits .. " commits from " .. num_authors .. " author(s)")
end

Here, default_branch() will return the branch that is currently checked out for local repositories. The equivalent revision iteration using map() would look like the following, using a closure containing the previous the loop body. As opposed to iteration by using revisions(), progress information will be written to the console.

 -- Initialize iterator and loop over all revisions
    local iterator = repository:iterator(branch)
    iterator:map(function (revision)
        -- Check if the author has already been counted
        if authors[revision:author()] == nil then
            num_authors = num_authors + 1
            authors[revision:author()] = true
        end

        num_commits = num_commits + 1
    end)

Technical information regarding revision iterators

Internally, revision iterators are designed in a asynchronous manner. The actual revisions on the branch and range selected don't need to be known at the time of construction since fetching this information from the repository may be time-consuming. Thus, the revision iterator will probably block during iteration when waiting for further revision identifiers from the backend. Currently, this kind of background log fetching is implemented for the Subversion backend only as it is the the only repository backend that operates on remote repositories.

Once a bunch of revision identifiers is known to be part of an iteration, meta-data and diffstats can be prefetched by the respective backend implementation in background threads. Most of the backends are using this kind of prefetching. Currently, the only exception is the Mercurial backend.

Option handling

Users can specify report options at the command line after defining the path to the report script. Option handling in scripts consists of two parts:

  • Declaration of available options in the table returned by describe(). These options will be presented to the user if help is requested at the command line.
  • Retrieval of option values using report:getopt(). Default option values can be specified, too.

Here's a short script that prints the head revision of a given branch for illustrative purposes:

function describe()
    local r = {}
    r.title = "Branch HEAD"
    r.options = {{"-bARG, --branch=ARG", "Select branch"}}
    return r
end

function run(self)
    local repository = self:repository()
    local branch = self:getopt("b,branch", repository:default_branch())
    print("Branch " .. branch .. " is at " .. repository:head(branch))
end

Graphical reports

Reports can use the built-in GNUPlot interface to produce graphical output. Furthermore, the pepper.plotutils Lua module provides support for common command-line options and plot setup.

The following report plots a histogram of commits by week days.

-- Include the plotutils module
require "pepper.plotutils"

-- Meta-data
function describe()
    local r = {}
    r.title = "Commits by Week Days"

    -- Add common plotting options
    pepper.plotutils.add_plot_options(r)
    return r
end

-- This function will be called for every revision
function callback(revision)
    -- Determine week day of commit
    local day = os.date("*t", revision:date())["wday"]

    -- Update commit count
    commits[day] = commits[day] + 1
end

-- Entry point
function run(self)
    local repository = self:repository()
    local days = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}
    -- Global table of commit counts with an entry for each day
    commits = {0, 0, 0, 0, 0, 0, 0}

    -- Gather data by iterating over the default branch
    local branch = repository:default_branch()
    repository:iterator(branch):map(callback)

    -- Create a new plot with a title including the current branch
    local plot = pepper.gnuplot:new()
    plot:set_title("Commits by Week Days (on " .. branch .. ")")

    -- This will setup the plot output using to the command-line
    -- options added in describe()
    pepper.plotutils.setup_output(plot)

    -- Make sure the Y axis starts at zero
    plot:cmd("set yrange [0:*]")

    -- Run the plot
    plot:plot_histogram(days, commits)
end

This time, the iterator's map() function has been used with a corresponding callback function. The commits variable is global, so it can be accessed from within the callback function. However, we could have used a closure as well and kept the variable local.

If you pass --help as a main option to the program when you run this report, common plot options like output file name and image size will be shown. Those have been added by calling add_plot_options(). setup_output(plot) makes sure that these options are actually applied to the plot.