System Customization
====================

Critic supports some low-level, and typically very simple, customization of
various aspects of its behavior.  This customization would be done by the Critic
system administrator and is entirely optional; if a Critic system is not
customized at all, the default behavior is typically entirely reasonable.

The customization mechanism consists of conditional imports of functions from
modules in a package named "customization", which by default does not exist.  If
the system administrator creates such a package in a directory in Critic's
<code>PYTHONPATH</code>, Critic will automatically start using whatever
customization behaviours it implements.

In this Critic install, two Critic-specific directories are included in its
<code>PYTHONPATH</code>: <code>%(configuration.paths.CONFIG_DIR)s/</code> and
<code>%(configuration.paths.INSTALL_DIR)s/</code>.  To initially set up
customization, create a sub-directory named "customization" in one of these
directories, containing an empty file named <code>__init__.py</code>.
Individual customizations (described below) are then done by creating additional
<code>.py</code> files in this directory.

In all cases, the actual implementation of functions and classes below are only
examples meant to indicate how the interfaces could be implemented.  In other
words, only the function names and argument lists in the examples are in any way
"normative."

Automatic User Creation
-----------------------

This customization affects how users are automatically created when Critic is
configured to let the web server do authentication, and the authenticated user
does not exist in Critic's user database (in other words, the first time a user
logs in.)

customization/email.py
----------------------
This module should define this function:

| def getUserEmailAddress(username):
|   return username + "@example.com"

The function <code>customization.email.getUserEmailAddress()</code>, if defined,
is called to calculate a user's email address from the username.  If the
function returns None, or is not defined, the user is created with no email
address.  Either way, the user can change the email address via the /home page
when logged in.

Hyperlinks in Text
------------------

Critic's web front-end does automatic pattern-based "linkification" of text
(such as review descriptions, commit messages and user comments.)  By default it
handles plain http:// and https:// URLs, &lt;URL:...&gt;, SHA-1 commit
references (including BEGIN..END ranges), and r/NNNN review references.

This mechanism can be extended with other patterns, for instance linking issue
references to an issue tracking system.

customization/linktypes.py
--------------------------
This module should import the module <code>linkify</code> (which is part of
Critic) and then define sub-classes of the class <code>linkify.LinkType</code>,
and create one or more instances of each such sub-class
(<code>linkify.LinkType</code>'s <code>__init__()</code> registers the link type
in a global structure.)

Each sub-class should call <code>linkify.LinkType</code>'s
<code>__init__()</code> with a single argument that is a string containing a
regular expression (with no capture groups) that matches the sub-string that
should be made into a link.

The sub-class should also implement the method <code>linkify</code> which will
be called with two arguments, a string containing the sub-string that should be
made into a link and a context object.  This method should either return a
string containing a URL, or None.  If it returns a string, the word is made into
a link to that URL.

| import linkify
|
| class IssueLink(linkify.LinkType):
|   def __init__(self):
|     super(IssueLink, self).__init__("#[0-9]+")
|   def linkify(self, word, context):
|     return "https://issuetracker.example.com/showIssue?id=" + word[1:]
|
| IssueLink()

The context argument can safely be ignored.  Notable simple and potentially
useful information in it is <code>context.review.id</code>, which is the integer
id of the review, if the page loaded is connected to a review, and
<code>context.repository.name</code>, which is the string name of the repository
containing the review or commit.  Both of these sub-objects
(<code>context.review</code> and <code>context.repository</code>) can be
<code>None</code> if link conversion happens in a context where there is no
associated review or repository.

Branch Name Patterns
--------------------

Critic can handle reviews that are configured to track branches in other
repositories (not on Critic.)  In such a scenario, a review can be rebased by
switching it over to track a new (rebased) branch in the other repository.  If
branch names in the other repository follow a certain naming pattern, like
<code>&lt;basename&gt;/&lt;serial-number&gt;</code>, Critic can be customized to
find and suggest appropriate branch names based on that naming pattern when
rebasing such reviews.

customization/branches.py
-------------------------
This module should define one or both of these functions:

| import os
|
| def getRebasedBranchPattern(branch_name):
|   return os.path.dirname(branch_name) + "/*"
|
| def isRebasedBranchCandidate(current_name, other_name):
|   return os.path.dirname(current_name) == os.path.dirname(other_name)

When searching for plausible new branches for a review to track in the other
repository, the other repository is scanned using <code>git ls-remote</code>.

The <code>getRebasedBranchPattern()</code> function, if defined, is used to
calculate a pattern argument to <code>git ls-remote</code>.  If it is not
defined, or if it returns None, <code>git ls-remote</code> is called to list all
branches in the other repository.  The <code>isRebasedBranchCandidate()</code>
function, if defined, is used to filter the set of branches returned by
<code>git ls-remote</code> down further.

If neither function is defined, no scanning of the other repository is done, and
no branches are suggested to the user.  This doesn't leave the user entirely
without guidance, however; the input field on the page where the user is asked
to input the branch name will auto-complete input based on branches that exist
in the other repository (also using <code>git ls-remote</code>.)

Custom pre-receive Checks
-------------------------

It is entirely possible to install custom Git hooks in Critic's repositories,
but doing so may conflict with the hooks that Critic installs and depends on for
its interactions with the repositories.  Currently, Critic only installs a
pre-receive hook, but other hooks may be installed in the future.

If custom checks should be applied to ref updates, before Critic handles them,
the preferred way is using Critic's customization mechanism.

customization/githook.py
------------------------
This module should define an exception class, Reject, and a function, update():

| class Reject(Exception):
|   pass
|
| def update(repository_path, ref_name, old_value, new_value):
|   if ref_name.startswith("refs/heads/reserved/"):
|     raise Reject("Don't push reserved refs!")

The function is called with the arguments 'repository_path', which is the
(absolute) file-system path of the Git repository (any one of Critic's
repositories on the system); 'ref_name', which is the full name of the reference
being updated; 'old_value', which is the current SHA-1 of the reference, or
None if the reference is being created; and 'new_value', which is the new
SHA-1 of the reference, or None if the reference is being deleted.

If a Reject exception (or sub-class thereof) is raised by update(), the update
is rejected with an error message constructed by applying str() to the exception
object.  For a sub-class of Exception created with a single argument, as in the
example above, this means that argument is converted to a string and returned.

If an exception of any other type is raised, it is ignored, and the update is
processed by Critic (and either rejected or allowed, as usual.)

If the update() function writes to stdout and the update is not rejected by
either the update() function itself or Critic's normal handling of the update,
the text written is sent to the Git client updating the reference, and included
in its output, with each line prefixed by the string "remote: ".
