Search

Running Arbitrary Scripts Under CVS

0 views

Hook Files and Their Roles

When you install CVS on a server, the repository comes with a special directory called CVSROOT. Inside that directory, five files sit quietly, waiting for you to put code in them: commitinfo, loginfo, verifymsg, rcsinfo, and taginfo. They do not look like ordinary configuration files. Instead, each of them is a dispatcher for scripts that run automatically at key moments during a CVS operation. Understanding their timing and purpose is the first step to harnessing CVS hooks effectively.

Whenever a user checks in new revisions with cvs commit, the system calls these files in a precise order. For a commit, CVS executes commitinfo before the files are written to the repository, verifymsg after the user has typed the log message but before the files become permanent, and loginfo after the repository is updated. These hooks give you three opportunities to intervene: to reject a commit that violates a rule, to modify the log message on the fly, or to publish the commit data elsewhere.

Tagging introduces a different set of hooks. When a developer runs cvs tag or cvs rtag, CVS invokes taginfo before the tags are added, moved, or deleted. The file provides a chance to validate the tag name, adjust the target revision, or log the action in a separate system.

The rcsinfo file is a bit different. It does not run a script. Instead, it points to a template file that CVS opens in the editor when a developer prepares a commit message. This template can pre-fill placeholders that the developer later edits. Because the template is read when the editor starts, you can use it to enforce a consistent structure across all log messages.

Although the names of these files suggest a linear flow, in practice they are independent dispatchers. The rules you write in commitinfo apply only to commit time; taginfo never sees a commit event, and vice versa. This separation keeps the hooks modular and easier to reason about. If you need to change the validation logic for commits, edit only commitinfo and leave the other files untouched. If you later add a new type of operation, you can create a new dispatcher file without touching the existing ones.

Because these files are read and executed on the server side, they run under the permissions of the CVS process. This has two practical implications. First, any user who has write access to CVSROOT can modify the hooks and potentially break the workflow for everyone else. Second, because the hooks run on the server, they can access repository internals that client-side scripts cannot. Knowing this, you should keep the hook files under strict version control and restrict write permissions to a small group of administrators.

In the next section we will explain the syntax that governs how CVS matches a directory to a rule inside one of these files. The pattern language is a small regular‑expression subset, and understanding it is key to writing robust scripts that trigger exactly where you intend them to.

Syntax, Patterns, and Action Templates

Each of the five hook files follows the same rule format: a regular‑expression pattern, a space, and then an action. The pattern is matched against the relative path of a directory within the repository, starting at the root of the repository. The action is a shell command that receives the matching directory and, in some cases, additional arguments supplied by CVS. For example, a rule might look like this:

Prompt
^src(/|$) /usr/local/cvs/scripts/check_style

In this line, the pattern ^src(/|$) matches any directory whose path starts with src and is either followed by a slash (indicating a subdirectory) or ends immediately (the root of src). When a commit touches that directory, CVS will invoke /usr/local/cvs/scripts/check_style. The script receives the directory name and, depending on the hook, may also get a list of files or a log message.

Two special patterns make rule sets more flexible. The pattern ALL matches every directory in the repository. It is useful for hooks that should run globally, such as a script that logs all commits to an audit trail. The pattern DEFAULT is a fallback that applies only if no other pattern (except ALL) matches a given directory. A typical rule set might include an ALL pattern that calls a generic script, and a DEFAULT pattern that performs a minimal action or nothing at all.

When CVS finds a rule that matches, it stops looking for further matches in that file. This means that the order of rules matters only if you rely on ALL or DEFAULT to catch directories that would otherwise be ignored. In most configurations, you’ll place specific rules first and the generic ALL rule at the end.

The action part of a rule is a shell command line. The command may include arguments that are constants, file names, or placeholders that CVS expands. For example, the loginfo hook uses an action of the form:

Prompt
sendlog %%

Here, the placeholder %% will be replaced by a format string that expands to a list of files and revisions affected by the commit. The final command line might become:

Prompt
sendlog main.c,1.3 main.h,1.2

Scripts can read from standard input, write to standard output, or use environment variables, but the only guaranteed input method is stdin. The command line parameters are appended after the script name by CVS. The exact parameters depend on the hook type. For example, commitinfo receives the relative directory and a list of files that will be committed; verifymsg receives the path to a temporary file that contains the log message; and taginfo receives a string that includes the tag name, the operation (add/mov/del), the repository path, and a list of file–revision pairs.

Lines that start with a hash (#) are comments and are ignored by CVS. Comments are useful for documenting the purpose of a rule or the meaning of its arguments. It is a good practice to add a comment above every rule you write, especially in shared repositories where multiple administrators may touch the files.

Because the pattern language is a subset of regular expressions, you can use anchors, character classes, and quantifiers. However, CVS does not support backreferences or advanced features like lookaheads. For most use cases, the simple patterns shown above suffice. If you need more elaborate matching, you can call an external script that performs its own pattern matching and decide whether to proceed.

Once you grasp how patterns map directories to actions, you can begin to assemble hooks that enforce coding standards, audit changes, or integrate CVS with other tools. The next sections will walk through how to use each hook type for common tasks.

Commit‑Time Hooks: commitinfo and verifymsg

The commitinfo hook fires before CVS writes any new revisions to the repository. Its purpose is to allow you to validate the set of files about to be committed or to impose a lock on the repository for exclusive development. Because the hook runs in a context where the repository is still writable, you can examine the exact set of files that the user has requested to commit.

When the hook runs, CVS passes two arguments: the relative path of the directory that contains the files, and a space‑separated list of all files that will be committed in that directory. The script may inspect each file, run a linter, or check that the file names match a particular convention. If the script exits with a non‑zero status code, CVS aborts the entire commit operation. The user receives an error message and can adjust the commit accordingly.

Because commitinfo operates on a per‑directory basis, a single repository can have multiple directories with different rules. For example, you might allow anyone to commit to docs/ but restrict src/ to members of the core team. A rule for src/ would run a script that verifies the user’s group membership before allowing the commit. In practice, the script might look up the user’s UID in a database and compare it against a list of permitted IDs.

One nuance to keep in mind is that commitinfo receives a temporary copy of each file that will be committed. The script sees the file content in a directory called $TMPDIR (or a similar temporary location). If the script tries to modify a file, those changes are discarded once the hook finishes. This behavior protects the repository from accidental or malicious modifications, but it also means that commitinfo is read‑only with respect to the actual files. If you need to write a file during a commit, you must do so via a separate process that runs after commitinfo has finished, such as loginfo

The verifymsg hook gives you a second chance to scrutinize or alter the commit log message. After a user enters a message and before the commit proceeds, CVS creates a temporary file containing that message and passes its path as an argument to verifymsg. The script can then read the message from standard input or from the file path. If the script exits with a non‑zero status, the commit aborts.

Because log messages can contain sensitive information or must follow a certain format, verifymsg is ideal for enforcing standards. A common pattern is to require a project identifier or a task ID at the beginning of the log message. The script would search the first line for a pattern like PROJECT-1234. If the line is missing or malformed, the script returns 1, and CVS cancels the commit. Users then see an error and are prompted to correct the message before trying again.

Scripts that modify the log message can rewrite the file that verifymsg points to. Whether CVS uses the original message or the modified one depends on the RereadLogAfterVerify setting in the config file inside CVSROOT. Setting this option to true forces CVS to read the updated file after the script runs, so the new message becomes the commit’s final log entry. If the option is false, CVS continues with the original message and ignores any changes made by the script.

Because verifymsg runs on the server, you can also use it to trigger side effects, such as sending an email or creating a ticket in a bug‑tracking system. The script receives the temporary file path, so you can read the message and then construct an email that includes the log text and the file list. Keep in mind, however, that the script runs with the privileges of the CVS process, so any external calls it makes should be safe and sandboxed.

When writing commit‑time hooks, you must be mindful of the potential for deadlocks. If your script executes a CVS command that requires a lock on the same directory, it could block while waiting for the lock that the original commit holds. To avoid this, run any internal CVS commands in the background or design the script so that it does not need to acquire the same lock. Alternatively, structure your hook to perform quick checks and exit early, leaving heavier tasks for post‑commit hooks.

In practice, the combination of commitinfo and verifymsg offers a powerful way to maintain code quality, enforce naming conventions, and keep commit logs clean. The next section explores hooks that act after the commit is finalized, letting you broadcast changes or create reports.

Post‑Commit Hooks: loginfo and rcsinfo

The loginfo hook runs after the repository has recorded a new revision. It is the perfect place to publish commit data to external systems, notify stakeholders, or generate release notes. When loginfo is called, CVS passes the log message that the user typed, the full path of the repository being committed, and a format string that the hook will use to generate a list of affected files and their old and new revisions.

The format string is a sequence of tokens that begin with a percent sign. The most common tokens are %s for the filename, %V for the revision before the commit, and %v for the revision after the commit. You can also group tokens with braces, such as %(sV), to produce a single comma‑separated value per file. CVS expands the string into one line per file, so a commit that touches three files might produce:

Prompt
main.c,1.3,1.4</p> <p>utils.c,2.1,2.2</p> <p>README,1.0,1.1

When you write a loginfo rule, you might set the action to a script that parses this output and posts it to a message queue. For instance:

Prompt
# In loginfo</p> <p>^lib(/|$) /usr/local/cvs/scripts/publish_log

Inside publish_log, the script can read the log message from standard input, read the format string from the next argument, and then parse the file list. Once parsed, it can transform the data into JSON and send it to a REST endpoint that aggregates commits.

Because the hook runs after the commit is complete, it can also perform expensive operations without holding locks. For example, you could generate a changelog file that aggregates all changes in the last week, or trigger a nightly build that picks up the newly committed files. The key advantage of loginfo is that it runs only once per directory, even if multiple files are changed within that directory.

The rcsinfo file is not a hook in the same sense as the others. It points to a template file that CVS opens in the editor when a developer is about to type a commit message. The template can pre‑populate fields that the developer will later edit, which helps enforce consistency and reduce human error. A typical template might look like this:

Prompt
CVS: Add your bug number here</p> <p>@bugid</p> <p>CVS: Resolution (e.g., Fixed, WontFix, Duplicate)</p> <p>@resolution</p> <p>CVS: Write a brief description of your changes</p> <p>@message

When a user runs cvs commit, CVS launches the editor with the template. The user fills in the @bugid and other fields. After the editor exits, verifymsg can validate the placeholders or modify the message further.

One important detail is that the rcsinfo file is read when the editor starts. If you change the template file, you must delete and recreate the client’s working copy for the change to take effect. CVS copies the template into a Template file in the CVS subdirectory of each sandbox. Since that copy is not updated automatically, users who have been working with an old template may still see stale content. In practice, the safest way to update the template is to run cvs update -C in each working copy or to instruct users to check out a fresh copy.

In combination with verifymsg, rcsinfo provides a lightweight, client‑side enforcement mechanism that keeps commit messages structured. When you pair this with a post‑commit hook like loginfo, you can extract the fields you need and post them to a bug‑tracking system or a project management tool. The next section will walk through a concrete example of integrating CVS with Bugzilla.

Integrating CVS with Bugzilla: A Practical Example

Bug tracking and version control often coexist in the same workflow. Many teams use Bugzilla to manage tasks, while CVS stores the code. By combining hooks, you can keep the two systems in sync without manual steps. The example below assumes that Bugzilla is configured to accept email updates. That configuration requires Bugzilla to parse the email subject for a bug ID and the body for the update text.

First, create a template for the commit message and point rcsinfo to it. The template contains three placeholders that the developer will fill: the bug ID, the resolution status, and a brief description. The file might be named bugzilla-template and placed under the repository’s CVSROOT/scripts directory:

Prompt
CVS: Add the Bugzilla ID</p> <p>@bugid</p> <p>CVS: Set the resolution (Fixed, WontFix, Duplicate)</p> <p>@resolution</p> <p>CVS: Describe your changes</p> <p>@message

Now, add a rule in rcsinfo to load this template for the relevant project:

Prompt
# rcsinfo</p> <p>^project(/|$) /usr/local/cvsroot/CVSROOT/scripts/bugzilla-template

When a developer commits to the project directory, the editor opens with the template. The user enters the bug ID on the first line, the resolution on the next, and then types a description. Once the editor closes, verifymsg can enforce that the bug ID is present and correct.

Write a script named bugzilla-mail and place it in the same directory. The script reads the temporary log file created by verifymsg, extracts the first line, and sends an email to Bugzilla:

Prompt
#! /bin/bash</p> <p>read prompt bugid <p>if [ "$prompt" != "@bugid" ]; then</p> <p> echo "Error: first line must start with @bugid" >&2</p> <p> exit 1</p> <p>fi</p><h1>Assemble the email subject and body</h1> subject="[Bug $bugid]" <h1>Skip the first two lines (@bugid and @resolution) for the body</h1> tail -n +3 "$1" | mail -s "$subject" bugzilla@example.com</p> <p>exit 0

Notice that the script reads from standard input (<&0) because CVS passes the log file that way. The script also checks that the first line is exactly @bugid before extracting the bug ID. If the check fails, the script exits with 1 and CVS aborts the commit. If it passes, the script sends the email. Bugzilla will parse the subject for [Bug 1234] and treat the rest of the email as the update text.

Finally, add a rule in verifymsg to run this script for the same project:

Prompt
# verifymsg</p> <p>^project(/|$) /usr/local/cvsroot/CVSROOT/scripts/bugzilla-mail

With these two rules in place, the entire process is automatic. A developer commits code, fills in a template that includes the bug ID, and the commit proceeds only if the script validates the format. After the commit, Bugzilla receives the update email and attaches the commit log to the bug. The developer sees the commit succeed, and the bug shows the new changes without any manual steps.

Beyond Bugzilla, the same pattern works for other systems such as JIRA or GitHub issues. You just need to adapt the script to format the email or API request the way the target system expects. The hook mechanism remains the same, making it a flexible tool for many integrations.

When deploying hooks, test them thoroughly in a staging repository. Because hooks run on the server, a mistake can lock out the whole team or break the workflow. Use a small group of trusted administrators to maintain the hook files, and keep a backup of each rule set before making changes. Logging inside your scripts is also essential; redirect stderr to a file so you can debug failures after the fact.

In summary, CVS hooks give you fine‑grained control over every step of the commit and tag lifecycle. By understanding the syntax, writing robust scripts, and integrating with external tools, you can transform CVS from a simple version‑control system into a central hub that coordinates code, tasks, and notifications.

Suggest a Correction

Found an error or have a suggestion? Let us know and we'll review it.

Share this article

Comments (0)

Please sign in to leave a comment.

No comments yet. Be the first to comment!

Related Articles