#! /usr/bin/env python3
ID = '$Id: cvs2xhtml,v 1.15 2021/03/28 02:20:34 eagle Exp $'
#
# cvs2xhtml -- Convert cvs log output to XHTML Strict.
#
# Copyright 2002, 2004, 2006-2008, 2021 Russ Allbery <eagle@eyrie.org>
# See the documentation at the end of this file for the license.

import getopt, string, sys, time

# This page header is common to all generated pages.  Variables are in all
# caps surrounded by %%, and are substituted when the header is printed.
page_header = '''<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html
    PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
  <title>%NAME% Change History</title>
  <link rel="stylesheet" href="%STYLE%" type="text/css" />
  <meta http-equiv="content-type" content="text/html; charset=utf-8" />
</head>

<!-- Generated by %VERSION% on %DATE% -->

<body>

<h1>%NAME% Change History</h1>

<dl>
'''

# Used to report parsing errors when parsing the cvs log input.
class FormatException(Exception):
    def __init__(self, value):
        self.value = value
    def __str__(self):
        return repr(self.value)

def 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 usage():
    """Print usage information for this program.
    """
    print("Usage: cvs log <program> | cvs2xhtml [-hv] -n <program> -s <style>")

def skip_header(file):
    """Skip the header of CVS log output.

    Skips over the uninteresting header information of CVS log output read
    from the provided file up to the first line of 28 dashes.  We can't
    currently do anything useful with that information.
    """
    line = file.readline()
    while line != "-" * 28 + "\n":
        line = file.readline()

def parse_change_header(file):
    """Parse the header of a CVS log entry.

    Each CVS log entry starts with two lines giving the revision, date,
    author, and line deltas of the change.  Parse those two lines, reading
    them from the provided file, and print the XHTML for the header.  Return
    the modified lines value as a tuple of the positive and negative line
    counts.
    """
    line = file.readline()
    (revheader, revision) = line.split()
    if revheader != "revision":
        raise FormatException("revision number expected")
    line = file.readline()
    data = line.split()
    linesheader = None
    plus = None
    minus = None
    if len(data) == 10:
        (dateheader, date, time, authorheader, author, stateheader, state,
         linesheader, plus, minus) = data
    elif len(data) == 11:
        (dateheader, date, time, zone, authorheader, author, stateheader,
         state, linesheader, plus, minus) = line.split()
    elif len(data) == 13:
        (dateheader, date, time, zone, authorheader, author, stateheader,
         state, linesheader, plus, minus, commitheader,
         commitid) = line.split()
    elif len(data) == 7:
        (dateheader, date, time, authorheader, author, stateheader,
         state) = line.split()
    elif len(data) == 8:
        (dateheader, date, time, zone, authorheader, author, stateheader,
         state) = line.split()
    else:
        raise FormatException("invalid date header: " + line)
    if linesheader and linesheader != "lines:":
        if len(data) == 10:
            (dateheader, date, time, zone, authorheader, author, stateheader,
             state, commitheader, commitid) = line.split()
            plus = None
            minus = None
        else:
            raise FormatException("lines header expected")
    if dateheader != "date:":
        raise FormatException("date header expected")
    if authorheader != "author:":
        raise FormatException("author header expected")
    if stateheader != "state:":
        raise FormatException("state header expected")
    date = date.replace("/", "-")
    if minus:
        minus = minus.rstrip(';')
    print('<dt>')
    print('  <span class="revision">' + revision + '</span>')
    print('  (<span class="date">' + date + '</span>)')
    print('</dt>')
    print('<dd>')
    return (plus, minus)

def process_log_entry(file):
    """Read a CVS log entry and print it out in XHTML.

    Read a CVS log entry from the provided file and output that log entry in
    XHTML.  Returns the line following the log entry so that the caller can
    decide whether there are any additional log entries remaining in the
    file.
    """
    (plus, minus) = parse_change_header(file)
    print("<p>")
    line = file.readline()
    while line:
        line = line.strip()
        if line == "=" * 77 or line == "-" * 28:
            print("</p>\n")
            break
        elif line == "":
            print("</p>\n\n<p>")
        else:
            line = line.replace("&", "&amp;")
            line = line.replace("<", "&lt;")
            line = line.replace(">", "&gt;");
            print(line)
        line = file.readline()
    if plus:
        print('<p class="changed">\n  Lines changed:')
        print('  <span class="added">' + plus + '</span>')
        print('  <span class="removed">' + minus + '</span>\n</p>')
    print('</dd>\n')
    return line

def print_header(name, style):
    """Print the HTML header for the CVS log page.

    Given the name of the file for which we're generating log data and the
    style sheet to refer to, generate the XHTML header for the output.
    """
    header = page_header.replace("%STYLE%", style).replace("%NAME%", name)
    date = time.strftime("%Y-%m-%d %T -0000", time.gmtime())
    print(header.replace("%VERSION%", version()).replace("%DATE%", date))

def print_footer():
    """Print the HTML footer for the CVS log page.
    """
    print('</dl>\n\n</body>\n</html>')

def main():
    longopts = ['help', 'name=', 'style=', 'version']
    options, arguments = getopt.getopt(sys.argv[1:], "hn:s:v", longopts)
    name = style = ''
    for o, a in options:
        if o in ("-h", "--help"):
            usage()
            sys.exit()
        elif o in ("-n", "--name"):
            name = a
        elif o in ("-s", "--style"):
            style = a
        elif o in ("-v", "--version"):
            print(version())
            sys.exit()
    file = None
    if len(arguments) > 0 and arguments[0] != "-":
        file = open(arguments[0], "r")
    else:
        file = sys.stdin
    skip_header(file)
    print_header(name, style)
    line = process_log_entry(file)
    while line != "=" * 77:
        line = process_log_entry(file)
    print_footer()

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

documentation = """

=head1 NAME

cvs2xhtml - Convert cvs log output to XHTML Strict

=head1 SYNOPSIS

cvs2xhtml [B<-hv>] B<-n> I<name> B<-s> I<style> [I<input>]

=head1 DESCRIPTION

B<cvs2xhtml> converts the output of the CVS log command to XHTML 1.0
Strict, suitably tagged so that it can be formatted by the application of
a style sheet.  The B<-n> option should be given to tell B<cvs2xhtml> the
name of the program that the log data is for.  At present, the B<-s> flag
is also effectively required, since otherwise the generated XHTML isn't
valid.

The data is formatted as a description list since that was the most
appropriate format for the log data that I was using this script to format
(for larger and more complex packages, I use a GNU-style ChangeLog
instead).  This program only deals with log data from a single file.

The data is read from I<input>, or from standard input if no I<input>
argument is given.

B<cvs2xhtml> assumes that CVS log messages are encoded in UTF-8 and
specifies a character set of UTF-8 in its XHTML output.

=head1 OPTIONS

=over 4

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

Print brief usage information and exit.

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

Print the version of B<cvs2xhtml> and exit.

=item B<-n> I<name>, B<--name>=I<name>

Specifies the name of the program that the log data is for, used by
B<cvs2xhtml> to create the page title and top heading.

=item B<-s> I<style>, B<--style>=I<style>

Specifies the style sheet to which the generated XHTML page should refer.
This should be a URL (possibly relative to the location where the XHTML page
will be placed), not just a file name.

=back

=head1 EXAMPLES

Generate foo.html from the output of cvs log foo, using the style sheet
pod.css:

    cvs log foo | cvs2xhtml -n foo -s pod.css > foo.html

In general, it's most convenient to pipe the output of cvs log through
B<cvs2xhtml>, although that output can also be saved to a file which can be
passed to B<cvs2xhtml> as its first argument.

=head1 BUGS

There's no way not to generate a reference to a style sheet.  B<cvs2xhtml>
should also be able to figure out B<-n> from the initial header of the CVS
log output, or at least come up with a reasonable guess.

Currently, this program just throws uncaught exceptions if anything goes
wrong, which is less than ideal.  At least the common errors should probably
be caught and result in good diagnostic output.

The XHTML generation generally isn't as robust as it could be.

=head1 NOTES

This program really isn't at all suitable for dealing with anything other
than revision histories for single files.  Anything more complex than that
is probably better dealt with by using cvs2cl or a similar utility to
generate a GNU-style ChangeLog and then turning that into XHTML.

=head1 SEE ALSO

cvs(1)

The XHTML 1.0 standard at L<http://www.w3.org/TR/xhtml1/>.

Current versions of this program are available from my web tools page at
L<http://www.eyrie.org/~eagle/software/web/>.

=head1 AUTHOR

Russ Allbery <eagle@eyrie.org>

=head1 COPYRIGHT AND LICENSE

Copyright 2002, 2004, 2006-2008, 2021 Russ Allbery <eagle@eyrie.org>

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

"""