#! /usr/bin/env python
ID = "$Id: release,v 1.50 2016/01/25 04:24:21 eagle Exp $"
#
# release -- Release a software package.
#
# Copyright 2001, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2012, 2013, 2014
#     Russ Allbery <rra@stanford.edu>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.

import ConfigParser
import calendar
import getopt
import os
import re
import shutil
import stat
import string
import subprocess
import sys
import time

# The path to the configuration file specifying how to release software.
CONFIG = os.path.expanduser("~/dvl/CONFIG/release.conf")

def die(message):
    sys.stderr.write("release: %s\n" % message)
    sys.exit(1)

def usage():
    """Print usage information for this program.
    """
    return "Usage: release [-bdshuv] <program>\n"

def get_version():
    """Return the version number of this program.

    Returns the verison number of this program as a string containing the
    program name, the CVS revision number, and the last modification date in
    parentheses.  This is taken from the global ID variable.
    """
    # Make sure CVS doesn't see a variable here to substitute.
    if ID != "$" + "Id$":
        data = ID.split()
        date = data[3].replace("/", "-")
        program = data[1][:-2]
        version = data[2]
        return program + " " + version + " (" + date + ")"
    else:
        return ""

def version_deb(path):
    """Find the upstream version from a Debian changelog file.

    Given a path to a directory that contains a debian subdirectory and a
    changelog file in that subdirectory, use dpkg-parsechangelog to determine
    the version of the latest revision in that changelog and from that the
    upstream version of the package.
    """
    old = os.getcwd()
    os.chdir(path)
    info = os.popen("dpkg-parsechangelog", "r")
    version_regex = re.compile(r"^Version: (?:\d+:)?(.*?)-\d")
    line = info.readline()
    while line:
        match = version_regex.search(line)
        if match:
            info.close()
            os.chdir(old)
            return match.group(1)
        line = info.readline()
    die("cannot find version information in " + path)

def version_script(filename):
    """Find the version and last modified date of a script.

    Extracts the version and last modified date of a script given by the
    filename argument and returns them as a tuple.  The last modified date
    is taken from the RCS Id string and the version is taken either from
    that string or from a setting of the variable VERSION if one is found.
    The setting of VERSION overrides the Id string.  Missing information is
    given as empty strings.

    The version is returned as a string, and the date is returned in seconds
    since epoch.
    """
    file = open(filename, "r")
    id_regex = re.compile(r"\$Id\: (.*)\$")
    version_regex = re.compile(r"\$?VERSION\s*=\s*'?([\d.]+)")
    id = version = ""
    date = None
    for i in range(50):
        line = file.readline()
        if not line:
            break
        match = id_regex.search(line)
        if match:
            id = match.group(1)
        match = version_regex.match(line)
        if match:
            version = match.group(1)
    file.close()
    if id:
        idversion, date, hour = string.split(id)[1:4]
        if not version:
            version = idversion
        pretty = date + " " + hour + " UTC"
        try:
            date = time.strptime(pretty, "%Y-%m-%d %H:%M:%S %Z")
        except ValueError:
            date = time.strptime(pretty, "%Y/%m/%d %H:%M:%S %Z")
        date = calendar.timegm(date)
    else:
        date = int(time.time())
    return (version, date)

def find_tarball(directory, pattern):
    """Find a tarball matching a pattern and return file and version.

    Given a directory and a pattern matching files in that directory, find the
    release tarball there.  Returns a tuple consisting of the full path to
    that tarball, the version embedded in the name, and the timestamp of the
    tarball.  The file name should be the contents of the first set of
    capturing parentheses in the pattern, and the version should be the
    contents of the second.
    """
    file_regex = re.compile(pattern)
    files = os.listdir(directory)
    for file in files:
        match = file_regex.search(file)
        if match:
            path = directory + "/" + match.group(1)
            time = os.stat(directory + '/' + file).st_mtime
            return (path, match.group(2), time)
    return (None, None, None)

def move_files(root, pattern, dest):
    """Find files matching pattern in root and move them to dest.
    """
    file_regex = re.compile(pattern)
    files = os.listdir(root)
    for file in files:
        match = file_regex.search(file)
        if match:
            os.rename(root + "/" + file, dest + "/" + file)

def rename_dir(root, pattern, dest):
    """Find a directory matching pattern in root and rename it to dest.
    """
    dir_regex = re.compile(pattern)
    files = os.listdir(root)
    for file in files:
        match = dir_regex.search(file)
        if match and os.path.isdir(root + "/" + file):
            os.rename(root + "/" + file, dest)
            return
    die("unable to find directory matching " + pattern + " in " + root)

def cvs_export_tree(module, dest, cvsroot = None):
    """Export the given module into a directory.

    Given a directory name and a CVS module, export the given CVS module into
    that directory.  Optionally takes a cvsroot to pass to CVS.  Dies on
    failure.
    """
    command = "cvs"
    if cvsroot:
        command = command + " -d " + cvsroot
    command = command + " export -kv -r HEAD -d " + dest + " " + module
    status = os.system(command)
    if status:
        die("cvs export status " + `status`)

def svn_export_tree(url, dest):
    """Export the given Subversion repository to a directory.

    Given a directory name and a Subversion URL, export the given Subversion
    repository into that directory.  Dies on failure.
    """
    command = "svn export " + url + " " + dest
    status = os.system(command)
    if status:
        die("svn export status " + `status`)

def bzr_export_tree(url, dest):
    """Export the given bzr repository to a directory.

    Given a directory name and a bzr URL, export the given bzr repository to
    that directory.  Dies on failure.
    """
    command = "bzr branch " + url + " " + dest
    status = os.system(command)
    if status:
        die("bzr branch status " + `status`)

def git_export_tree(url, dest, branch = "master"):
    """Export the given git repository to a directory.

    Given a directory name and a URL or path to a Git repository, export the
    repository to that directory.  Dies on failure.
    """
    command = "git archive --remote=" + url + " --prefix=" + dest + "/"
    command = command + " " + branch + " | tar xf -"
    status = os.system(command)
    if status:
        die("git archive status " + `status`)

def cvs_checkout_debian_tree(module, dest, cvsroot = None):
    """Check out the debian directory from CVS over an existing tree.

    Given a directory name and a CVS module, check out the Debian subdirectory
    of the given CVS module into that directory, removing any existing debian
    subdirectory.  Optionally takes a cvsroot to pass to CVS.  Dies on
    failure.
    """
    debian = dest + "/debian"
    module = module + "/debian"
    try:
        shutil.rmtree(debian)
    except OSError:
        pass
    command = "cvs"
    if cvsroot:
        command = command + " -d " + cvsroot
    command = command + " export -r HEAD -kv -d " + debian + " " + module
    status = os.system(command)
    if status:
        die("cvs export status " + `status`)

def svn_checkout_debian_tree(url, dest):
    """Check out the debian directory from Subversion over an existing tree.

    Given a directory name and a Subversion URL, check out the debian
    subdirectory at the given URL into that directory, removing any existing
    debian subdirectory.  Dies on failure.
    """
    debian = dest + "/debian"
    url = url + "/debian"
    try:
        shutil.rmtree(debian)
    except OSError:
        pass
    command = "svn export " + url + " " + debian
    status = os.system(command)
    if status:
        die("svn export status " + `status`)

def cvs_export_file(file, dest, cvsroot = None):
    """Export the given file from CVS.

    Given a file path in CVS and the location to put an export of that file,
    do the equivalent of an export of that file (permanently expanding all CVS
    keywords) and put it into the destination path.  If cvsroot isn't given,
    use the CVS default.
    """
    command = "cvs -Q"
    if cvsroot:
        command = command + " -d " + cvsroot
    command = command + " checkout -kv -p " + file
    output = open(dest, "w")
    input = os.popen(command)
    for line in input:
        output.write(line)
    status = input.close()
    output.close()
    if status:
        die("cvs checkout status " + `status`)

def handle_script(program, config):
    """Release a script and return the version and time.

    Performs the necessary release steps for a single-file release, like a
    script.  Handles cvs export or copying the script, and returns the version
    and modification time taken from the script.
    """
    path = config.get(program, "script")
    (version, time) = version_script(path)
    if config.has_option(program, "copy"):
        shutil.copy2(path, config.get(program, "copy"))
    elif config.has_option(program, "export"):
        try:
            cvsroot = config.get(program, "cvsroot")
        except ConfigParser.NoOptionError:
            cvsroot = None
        path = config.get(program, "cvspath")
        cvs_export_file(path, config.get(program, "export"), cvsroot)
    return (version, time)

def archive_tarball(path, release, archive, pattern, version):
    """Copy a tarball to a release area and archive old versions.

    Given the path to the new tarball, the path to the release directory, the
    path to the archive directory, a pattern matching tarballs for the same
    program, and the current release, copy the tarball into the release area.

    If archive is not None, also scan the release directory for any files
    matching pattern.  For each of those files, if the first match group
    doesn't match version (so if the release is for some other, presumably
    older version), move it into the archive directory, which is created if
    necessary.
    """
    if not os.path.isdir(release):
        die("release path " + release + " is not a directory")
    shutil.copy2(path + '.gz', release)
    shutil.copy2(path + '.gz.asc', release)
    if os.path.isfile(path + '.xz'):
        shutil.copy2(path + '.xz', release)
        shutil.copy2(path + '.xz.asc', release)
    base = os.path.basename(path)
    link = re.sub('-' + version.replace('.', '\\.'), '', base)
    for ext in ('.gz', '.xz'):
        if os.path.islink(release + '/' + link + ext):
            os.unlink(release + '/' + link + ext)
            os.unlink(release + '/' + link + ext + '.asc')
    os.symlink(base + '.gz', release + '/' + link + '.gz')
    os.symlink(base + '.gz.asc', release + '/' + link + '.gz.asc')
    if os.path.isfile(path + '.xz'):
        os.symlink(base + '.xz', release + '/' + link + '.xz')
        os.symlink(base + '.xz.asc', release + '/' + link + '.xz.asc')
    if archive:
        file_regex = re.compile(pattern)
        files = os.listdir(release)
        move = []
        for file in files:
            match = file_regex.search(file)
            if match and match.group(2) != version:
                move.append(file)
        if len(move) > 0:
            if not os.path.isdir(archive):
                os.mkdir(archive, 0777)
            for file in move:
                shutil.move(release + "/" + file, archive + "/" + file)
    return (version, time)

def handle_tarball(program, config):
    """Release a tarball and return the version and time.

    Performs the necessary release steps for a tarball release.  Handles
    copying the tarball to the archive and archiving old versions, and returns
    the version and modification time taken from the tarball.
    """
    dir = config.get(program, "tarball")
    prefix = config.get(program, "prefix")
    pattern = '^(' + prefix + '-(\d[^-]*)\.tar)\.(xz|gz)'
    (path, version, time) = find_tarball(dir, pattern + '$')
    if not path:
        die("cannot find tarball in " + dir)
    for ext in ('gz', 'xz'):
        file = path + '.' + ext
        if (ext == 'xz') and not os.path.isfile(file):
            continue
        signature = file + '.asc'
        if os.path.isfile(signature):
            modtime = os.stat(file).st_mtime
            if os.stat(signature).st_mtime < modtime:
                os.remove(signature)
        if not os.path.isfile(signature):
            command = ['gpg2', '--detach-sign', '--armor']
            if config.has_option(program, "keyid"):
                for keyid in config.get(program, "keyid").split():
                    command.extend(['-u', keyid])
            command.append(file)
            subprocess.check_call(command)
    if config.has_option(program, "copy"):
        release = config.get(program, "copy")
        archive = None
        if config.has_option(program, "archive"):
            archive = config.get(program, "archive")
        archive_tarball(path, release, archive, pattern + '(\.[^.]*)?',
                        version)
    return (version, time)

def update_versions(versions, program, version, timestamp):
    """Update the .versions file for a new release.

    Updates the .versions file used by spin for a new release of a particular
    product.  This is done by writing the existing .versions file to
    .versions.new, replacing the existing version number and release date in
    local time.  If this is successful, .versions.new is renamed to .versions
    and then committed into CVS.
    """
    new_versions = versions + ".new"
    old = open(versions, "r")
    new = open(new_versions, "w")
    new_date = time.strftime("%Y-%m-%d", time.localtime(timestamp))
    new_time = time.strftime("%H:%M:%S", time.localtime(timestamp))
    found = 0
    for line in old:
        product, old_version, old_date, old_time = line.split()[0:4]
        if product == program:
            while len(old_version) > len(version):
                version += " "
            while len(version) > len(old_version):
                old_version += " "
            line = line.replace(old_version, version, 1)
            line = line.replace(old_date, new_date, 1)
            line = line.replace(old_time, new_time, 1)
            found = 1
        new.write(line)
    if not found:
        sys.stdout.write("Dependency page: ")
        depend = string.rstrip(sys.stdin.readline())
        full_date = new_date + " " + new_time
        line = "%-15s  %-6s  %-20s  %s\n" \
               % (program, version, full_date, depend)
        new.write(line)
    old.close()
    new.close()
    os.rename(new_versions, versions)
    path, file = os.path.split(versions)
    os.chdir(path)
    print "Added versions entry for " + program + " " + version
    command = "git commit -m '" + program + " " + version + "' " + file
    status = os.system(command)
    if status:
        die("git commit status " + `status`)

def deb_build(program, config, builddir = "/tmp", source = False):
    """Build a Debian package from an export and the current release.

    Locates the last major release of the package, unpacks it, checks out the
    current debian directory over top of that unpacked tarball, and then runs
    pdebuild in the resulting source tree to generate new Debian packages.
    Those packages will be left in builddir.

    If source is true, append -sa to the build options.
    """
    package = config.get(program, "package")
    release = config.get(program, "copy")
    prefix = config.get(program, "prefix")
    pattern = '^(' + prefix + ')-([^-]*)\.tar\.gz$'
    (orig, version, time) = find_tarball(release, pattern)
    os.chdir(builddir)
    shutil.copy2(orig, package + "_" + version + ".orig.tar.gz")
    os.system("tar xfz " + package + "_" + version + ".orig.tar.gz")
    pattern = prefix + "-(.*)"
    rename_dir('.', pattern, package + "-" + version)
    path = builddir + "/" + package + "-" + version
    if config.has_option(program, "cvspath"):
        module = config.get(program, "cvspath")
        try:
            cvsroot = config.get(program, "cvsroot")
        except ConfigParser.NoOptionError:
            cvsroot = None
        cvs_checkout_debian_tree(module, path, cvsroot)
    elif config.has_option(program, "svnurl"):
        url = config.get(program, "svnurl")
        svn_checkout_debian_tree(url, path)
    else:
        die("neither cvspath nor svnurl set for " + program)
    os.chdir(path)
    command = "pdebuild --buildresult .. --debbuildopts "
    if source:
        command += "'-i -sa'"
    else:
        command += "-i"
    status = os.system(command)
    if status:
        die("build status " + `status`)
    os.chdir("..")
    os.system("rm *_source.changes")
    shutil.rmtree(path)

def deb_build_script(program, config, builddir = "/tmp", source = False):
    """Build a Debian package around a single script.

    Checks out the Debian packaging instructions for a script and uses the
    build-orig target to retrieve the script and build the .orig tarball, and
    then runs pdebuild in the resulting source tree to generate a new Debian
    package.  That package will be left in builddir.

    If source is true, append -sa to the build options.
    """
    package = config.get(program, "package")
    os.chdir(builddir)
    try:
        cvsroot = config.get(program, "cvsroot")
    except ConfigParser.NoOptionError:
        cvsroot = None
    module = config.get(program, "cvsdebpath")
    cvs_export_tree(module, package, cvsroot)
    version = version_deb(package)
    path = package + "-" + version
    os.rename(package, path)
    os.chdir(path)
    status = os.system("debian/rules build-orig")
    if status:
        die("build-orig status " + `status`)
    command = "pdebuild --buildresult .. --debbuildopts "
    if source:
        command += "'-i -sa'"
    else:
        command += "-i"
    status = os.system(command)
    if status:
        die("build status " + `status`)
    os.chdir("..")
    os.system("rm *_source.changes")
    shutil.rmtree(path)

def make_dist(program, config, distdir):
    """Generate a distribution tarball of a package in distdir.

    Exports the current source of a package into an empty directory and then
    performs whatever actions are required to generate a distribution
    tarball.  The resulting tarball is left in distdir.  If there is no
    xz-compressed version of the tarball, create one.
    """
    os.chdir(distdir)
    if config.has_option(program, "cvspath"):
        module = config.get(program, "cvspath")
        try:
            cvsroot = config.get(program, "cvsroot")
        except ConfigParser.NoOptionError:
            cvsroot = None
        cvs_export_tree(module, program, cvsroot)
    elif config.has_option(program, "svnurl"):
        url = config.get(program, "svnurl")
        svn_export_tree(url, program)
    elif config.has_option(program, "bzrurl"):
        url = config.get(program, "bzrurl")
        bzr_export_tree(url, program)
    elif config.has_option(program, "giturl"):
        url = config.get(program, "giturl")
        if config.has_option(program, "gitbranch"):
            branch = config.get(program, "gitbranch")
            git_export_tree(url, program, branch)
        else:
            git_export_tree(url, program)
    else:
        die("no source repository set for " + program)
    os.chdir(program)
    if os.path.isfile("Build.PL"):
        status = os.system("perl Build.PL")
        if status:
            die("perl Build.PL status " + `status`)
        status = os.system("./Build disttest")
        if status:
            die("./Build disttest status " + `status`)
        status = os.system("./Build dist")
        if status:
            die("./Build dist status " + `status`)
    elif os.path.isfile("Makefile.PL"):
        status = os.system("perl Makefile.PL")
        if status:
            die("perl Makefile.PL status " + `status`)
        status = os.system("make dist")
        if status:
            die("make dist status " + `status`)
    elif os.path.isfile("autogen") or os.path.isfile("bootstrap"):
        if os.path.isfile("autogen"):
            status = os.system("./autogen")
            if status:
                die("autogen status " + `status`)
        else:
            status = os.system("./bootstrap")
            if status:
                die("bootstrap status " + `status`)
        status = os.system("./configure")
        if status:
            die("configure status " + `status`)
        if config.has_option(program, "maketarget"):
            status = os.system("make " + config.get(program, "maketarget"))
        elif os.path.isfile("Makefile.am"):
            status = os.system("make distcheck")
        else:
            status = os.system("make dist")
        if status:
            die("make dist status " + `status`)
    elif os.path.isfile("Makefile"):
        if config.has_option(program, "maketarget"):
            status = os.system("make " + config.get(program, "maketarget"))
        else:
            status = os.system("make dist")
        if status:
            die("make dist status " + `status`)
    else:
        die("unknown package type for " + program)
    prefix = config.get(program, "prefix")
    pattern = '^(' + prefix + '-(.*)\.tar)\.(xz|gz)'
    (path, version, time) = find_tarball('.', pattern + '$')
    if not os.path.isfile(path + '.xz'):
        gzip_file = path + '.gz'
        subprocess.check_call(['gzip', '-dk', gzip_file])
        subprocess.check_call(['xz', path])
    move_files('.', pattern, "..")
    os.chdir("..")
    shutil.rmtree(program)

def find_unreleased(config, versions):
    """Find and report on unreleased scripts.

    Walks through the .versions file and, for each program listed there for
    which we can easily determine the current released version, check to be
    sure the released version matches the version in the .versions file.
    Report on any cases where it doesn't.
    """
    data = open(versions, "r")
    for line in data:
        product, version = line.split()[0:2]
        if config.has_section(product):
            try:
                path = config.get(product, "script")
                current = version_script(path)[0]
                if current != version:
                    print "%s (%s != %s)" % (product, version, current)
            except ConfigParser.NoOptionError:
                continue
    data.close()

def main():
    longopts = ["build", "dist", "help", "source", "unreleased", "version"]
    options, arguments = getopt.getopt(sys.argv[1:], "bdhsuv", longopts)
    build = False
    dist = False
    source = False
    unreleased = False
    for opt, arg in options:
        if opt in ("-b", "--build"):
            build = True
        elif opt in ("-d", "--dist"):
            dist = True
        elif opt in ("-s", "--source"):
            source = True
        elif opt in ("-u", "--unreleased"):
            unreleased = True
        elif opt in ("-h", "--help"):
            print usage()
            sys.exit()
        elif opt in ("-v", "--version"):
            print get_version()
            sys.exit()
    if not unreleased and len(arguments) != 1:
        sys.stderr.write(usage())
        sys.exit(1)
    config = ConfigParser.ConfigParser()
    config.readfp(open(CONFIG))
    versions = config.get("PATHS", "versions")
    if unreleased:
        find_unreleased(config, versions)
        sys.exit(0)
    program = arguments[0]
    if not config.has_section(program):
        die("unknown program " + program)
    if build:
        if config.has_option("PATHS", "builddir"):
            builddir = config.get("PATHS", "builddir")
        else:
            builddir = None
        if config.has_option(program, "cvsdebpath"):
            deb_build_script(program, config, builddir, source)
        else:
            deb_build(program, config, builddir, source)
    elif dist:
        make_dist(program, config, config.get("PATHS", "distdir"))
    else:
        if config.has_option(program, "script"):
            (version, time) = handle_script(program, config)
        elif config.has_option(program, "tarball"):
            (version, time) = handle_tarball(program, config)
        else:
            die(program + " has neither a script nor tarball configuration")
        update_versions(versions, program, version, time)

if __name__ == "__main__":
    main()
    sys.exit()

documentation = """

=head1 NAME

release - Release a software package

=head1 SYNOPSIS

B<release> [B<-hsv>] [B<-b> | B<-d>] I<package>

B<release> B<-u>

=head1 REQUIREMENTS

Python 2.5 or later, GnuPG for signing releases, and the revision control
program for whatever revision control software is used for the source.
Building Debian packages additionally requires pbuilder be installed and
properly configured.  Git is used to commit changes to the F<.verisons>
file.

This program is very likely to require customization if used by anyone
other than its author.

=head1 DESCRIPTION

B<release> automates portions of the process of releasing a new version of
a software package or script.  It's actions and all relevant paths are
controlled by a configuration file (see L</CONFIG FILE> below).  It
understands a variety of revision control systems and build systems and
attempts to determine the correct thing to do automatically with a minimum
of configuration.  It updates a F<.versions> file for use by B<spin> for
generating web pages.

The default action, if no arguments but I<package> are given, is to create
a detached GnuPG signature of the package if it is a tarball release (not
a script), copy it to its copy location, create or update symlinks without
the embedded version to point to the latest version, archive old versions
if appropriate, and update F<.versions>.  In this mode, it expects the
tarball or script to have already been prepared for a release, although
for a script it may export the final version from CVS.

If the B<-d> option is given, B<release> instead generates a tarball
release by running the appropriate build actions.  It currently
understands how to do exports from CVS, Subversion, bzr, and Git and how
to generate tarball releases for packages using Automake, Perl's build
system, or a dist Makefile target, with or without a configure script.

If the B<-b> option is given, B<release> instead builds Debian packages.
It knows how to combine a tarball release with a F<debian> directory
stored in CVS or Subversion to build a package.  All package builds are
done with B<pdebuild>.  This mode is no longer tested.

If the B<-u> option is given, B<release> does not expect a package on the
command line.  Instead, it scans the F<.versions> file and checks the
release version against the current version information, as best as it is
able to determine, for each package listed there.  For any that appear to
have a newer release than is recorded in the F<.versions> file, it prints
out the package and its current version.  Currently, it only checks
scripts.

=head1 OPTIONS

=over 4

=item B<-b>, B<--build>

Build a Debian package as described above.  This option is no longer
tested and is not used by the author, who has switched to
B<git-buildpackage> and separate repository branches for Debian package
builds.

=item B<-d>, B<--dist>

Rather than release software, generate the release tarball.  B<release>
first exports the package from its revision control system and then runs
the appropriate build system commands to generate a release tarball.  It
understands CVS, Subversion, bzr, and Git for revision control systems and
Perl, Automake, Autoconf, and simple makefiles with a C<dist> target for
generating the release tarball.

=item B<-h>, B<--help>

Prints basic usage information for B<release>.

=item B<-s>, B<--source>

When building a Debian package with B<-b>, force inclusion of the full
source in the *.changes file by passing B<-sa> to B<pbuilder> in the
B<--debuildopts> argument.

=item B<-u>, B<--unreleased>

Rather than release software, scan the F<.versions> file and check all
scripts listed there to see if they have a different version than the one
recorded in F<.versions>.

=item B<-v>, B<--version>

Print out the version of B<release> and exit.

=back

=head1 CONFIG FILE

B<release> uses a configuration file for most of the information it
needs.  This configuration file is in the format parsed by the standard
Python ConfigFile class (similar to a Windows INI file) and should have
one section per package, as used for command-line arguments.  It also must
have a section entitled C<PATHS> that configures some global variables.
Blank lines and lines beginning with C<#> are ignored.

Sections start with the section title between C<[]>.  A variable setting
is the variable name, a colon, whitespace, and then the value.  For
example:

    [PATHS]
    distdir: /home/eagle/data/dist
    versions: /home/eagle/web/eagle/.versions

    [kstart]
    tarball: /home/eagle/data/dist
    prefix: kstart
    copy: /afs/ir/users/r/r/rra/public/software/kerberos
    archive: /afs/ir/users/r/r/rra/public/software/ARCHIVE/kstart
    package: kstart
    giturl: /home/eagle/dvl/kstart

Variables for the C<PATHS> section:

=over 4

=item builddir

Specifies where to put Debian packages built using the B<-b> option.
Required only if B<-b> is used.

=item distdir

Specifies where to put tarballs built using the B<-d> option.  This
location will then generally be named in the tarball parameter of the
relevant package configuration section.  Required only if B<-d> is used.

=item versions

Specifies the location of the F<.versions> file for B<spin> that
B<release> should update.  Required.

=back

Variables for each package section:

=over 4

=item archive

When copying a new tarball release, look for any older files from previous
versions and move them to this directory, which is created if it doesn't
already exist.  Older files are recognized by looking for files starting
with C<prefix> and matching the file name pattern for a tarball with a
version or its MD5 checksum or PGP signature.  Only used if C<tarball> is
set; there is no way to archive old versions of scripts.

=item bzrurl

The URL to the bzr repository for the package.  When B<-d> is given, it is
used with the C<bzr branch> command to generate an exported copy of the
repository, which is then used to run the appropriate command to generate
a distribution tarball.

=item copy

The location into which to copy the release, either a script or a tarball.
For tarballs, files from previous versions in that location will be left
there unless C<archive> is also set.  For scripts, any existing version
will be overwritten.

=item cvsdebpath

The path to the CVS module for the Debian build rules within a CVS
repository.  This is only used when building a Debian package for a script
using the B<-b> option and is no longer tested.

=item cvspath

The path to the CVS module for the package within a CVS repository.  When
B<-d> is given, it is used with the C<cvs export> command to generate an
exported copy of the repository, which is then used to run the appropriate
command to generate a distribution tarball.  It is also used to locate the
script in the CVS repository to generate a CVS export when the C<export>
variable is set instead of C<copy>.  If C<cvsroot> is not also set, the
default CVS root is used, which may be unpredictable.

=item cvsroot

Specifies the root of the CVS repository for use with C<cvspath>.  Setting
this variable is strongly recommended, even if you have CVS configured to
use a default repository and all packages are in that repository, since it
removes potential future ambiguity.

=item export

Normally, scripts are copied from the location specified by C<script> to
the location specified by C<copy>.  If this variable is set instead of
C<copy>, the script is instead exported from its CVS repository, expanding
CVS keywords (the B<-kv> option).  C<cvspath> must be set.  This is only
used when C<script> is set and should not be set for the same package as
C<copy>.

=item gitbranch

The branch to export when generating a distribution tarball, instead of
C<master>.  Only used with B<-d> and if C<giturl> is set.

=item giturl

The URL, which may be a file system path, to the Git repository for the
package.  When B<-d> is given, it is used with the C<git archive> command
to generate an exported copy of the repository, which is then used to run
the appropriate command to generate a distribution tarball.  By default,
the C<master> branch is exported, but a different branch may be specified
with C<gitbranch>.  Only used if C<tarball> is set.

=item keyid

If set, names PGP key IDs to use when signing tarball releases.  Setting
this causes the B<-u> option and this value to be passed to B<gpg> when
creating signatures.  The value will be split on whitespace and each word
will be taken to be a separate key.  If more than one key is listed, the
resulting detached signature file will contain multiple signatures.  Only
used if C<tarball> is set.

=item maketarget

The make target to use to generate a distribution tarball.  This overrides
the default of C<distcheck> for Automake package and C<dist> for any other
package with a F<Makefile> or F<configure> script.  This is only used if
C<tarball> is set and when the B<-d> option is given.

=item package

The name of the Debian source package.  Only used when building Debian
packages with B<-b>.  This need not match the package name in the tarball,
which should be given with C<prefix>.  It should patch the package name in
F<debian/changelog>.

=item prefix

The prefix for tarballs of this package.  Release tarballs are expected to
be named I<prefix>-I<version>.tar.gz.  Only used if C<tarball> is also
set.

=item script

Specifies the location of a script to release.  B<release> will determine
the version of the script by searching the file for either a CVS Id string
or a variable VERSION or $VERSION within the first 50 lines of the file.
The latter overrides the former.  The presence of this variable indicates
that the package is a script, as opposed to C<tarball> which indicates
tarball releases.

=item svnurl

The URL to the Subversion repository for the package.  When B<-d> is
given, it is used with the C<svn export> command to generate an exported
copy of the repository, which is then used to run the appropriate command
to generate a distribution tarball.

=item tarball

Specifies the location searched for the tarball to release.  The resulting
directory will be searched for tarballs starting with C<prefix> and the
version number of the release extracted from the file name.  The presence
of this variable indicates that this package has tarball releases (as
opposed to C<script>, which indicates a script.

=back

Only one of C<bzrurl>, C<cvspath>, C<giturl>, or C<svnurl> should be
given.

=head1 FILES

=over 4

=item F<~/dvl/CONFIG/release.conf>

The default configuration file location, set at the top of this script.

=back

=head1 BUGS

B<release> hard-codes all sorts of details about the package layout,
export preferences, and version control methods of its author.  It will
likely require extensive customization to work for someone else.  This is
particularly true of the Debian package building (B<-b>), which relies on
a particular way of storing Debian package build information alongside a
package in a CVS or Subversion repository.

Debian package builds (B<-b>) are no longer used by the author and no
longer tested.  The same is true for all tarball support for revision
control systems other than Git, although it's simple enough that it's
unlikely to break significantly.

=head1 SEE ALSO

bzr(1), cvs(1), git(1), pdebuild(1), spin(1), svn(1)

The current version of this script is available from Russ Allbery's script
page at L<http://www.eyrie.org/~eagle/software/scripts/>.

=head1 AUTHOR

Russ Allbery <rra@stanford.edu>

=head1 COPYRIGHT AND LICENSE

Copyright 2001, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2012 Russ Allbery
<rra@stanford.edu>.

Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.

=cut

"""