#! /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("&", "&") line = line.replace("<", "<") line = line.replace(">", ">"); 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 """