#!/usr/bin/perl

# This is a demo script which illustrates how to write a
# special-purpose CVS client.  It is a CGI script which converts the
# output from "cvs log" to XML/HTML for easy access from ECMAscript/DOM (for
# example).  Note that you need a CVS with a tagged text patch for "cvs log".

# It was written by Cyclic Software, http://www.cyclic.com/, and is in
# the public domain.

$cvsroot = '/home2/cvsroot';
# Make sure to pick a file with at least one log message longer than
# one line.
$file = 'cyclic-admin/web/cvs/dev-res.html';

use FileHandle;
use IPC::Open2;

# For demo purposes, we just run the "cvs server" command as a subprocess.
# This might be sometimes useful in Real Life(TM) but it would be
# also relatively easy to use perl sockets to open a pserver/rsh/etc
# connection.  Alternately, just change "cvs server" to "rsh foo cvs server".
# Or change it to "tee log | cvs server" for debugging.
$pid = open2( \*Reader, \*Writer, "cvs server" ) || die "cannot open2: $!";
Writer->autoflush(); # default here, actually

# OK, first send the request to the server.  A simplified example is:
#     Root /home/kingdon/zwork/cvsroot
#     Argument foo/xx
#     Directory foo
#     /home/kingdon/zwork/cvsroot/foo
#     Directory .
#     /home/kingdon/zwork/cvsroot
#     log
# although as you can see there are a few more details.

print Writer "Root ", $cvsroot, "\n";
print Writer "Valid-responses ok error Valid-requests Checked-in Updated Merged Removed M E MT\n";
# Don't worry about sending valid-requests, the server just needs to
# support "log" and if it doesn't, there isn't anything to be done.
print Writer "UseUnchanged\n";

print Writer "Argument ", $file, "\n";
# The protocol requires us to fully fake a working directory (at
# least to the point of including the directories down to the one
# containing the file in question).  Not sure this is still true as
# of CVS 1.10.1.
# So if $file is "dir/sdir/file", then @dirs will be ("dir","sdir","file")
@dirs = split (/\//, $file);
$path = "";
foreach (@dirs) {
    if ($path eq "") {
	# In our example, $_ is "dir".
	$path = $_;
    } else {
	print Writer "Directory ", $path, "\n";
	print Writer $cvsroot, "/", $path, "\n";
	# In our example, $_ is "sdir" and $path becomes "dir/sdir"
	# And the next time, "file" and "dir/sdir/file" (which then gets
	# ignored, because we don't need to send Directory for the file).
	$path = $path . "/" . $_;
	$file = $_;
    }
}

# The "admin" request seems to require us to send "Entry".  I don't
# think it matters what the contents of the Entry are; perhaps it is a
# bug that it requires it in the first place.
# Apparently we don't need it for "log".
#print Writer "Entry /", $file, "/1.1///\n";

# And the last "Directory" before the command is the top level.
print Writer "Directory .\n";
print Writer $cvsroot, "\n";

print Writer "log\n";
# OK, we've sent our command to the server.  Thing to do is to
# close the writer side and get all the responses.  If "cvs server"
# were nicer about buffering, then we could just leave it open, I think.
close (Writer) || die "cannot close: $!";

print "Content-type: text/html\n\n";
print "<HTML><HEAD><TITLE>Demo of DOM and CVS</TITLE></HEAD>\n";
print "<BODY>\n";
print "<!-- Generated by DOM demo script -->\n";

print "<script>
// If you are reading this text in your browser, just keep reading
// past the line which says \"End of script\" (of course the buttons
// won't do anything in that case).
function findByName(e, name) {
  if (typeof(e) != \"object\") {
    alert(\"e not object: \" + e);
    return null;
  }
  if (e.nodeName == name)
    return e;
  if (e.hasChildNodes) {
    t = findByName(e.firstChild, name);
    if (t != null)
      return t;
  }
  if (e.nextSibling != null) {
    t = findByName(e.nextSibling, name);
    if (t != null)
      return t;
  }
  return null;
}

element = document.documentElement;
t = findByName(element, \"cvs-head\");
if (t == null) {
    textElement = null;
} else {
    textElement = t.firstChild;
}
function hideHead() {
    if (textElement == null) {
	alert(\"no textElement!\");
	return;
    }
    realhead = textElement.data;
    textElement.data = \"\";
}
function showHead() {
    if (textElement == null) {
	alert(\"no textElement!\");
	return;
    }
    textElement.data = realhead;
}
// End of script
</script>
<form name=\"chgForm\" onSubmit=\"hideHead(); return false;\">
 <input type=button value=\"Hide Head\" onClick=\"hideHead()\">
 <input type=button value=\"Show Head\" onClick=\"showHead()\">
</form>
";

# Ready to get the responses from the server.
# For example:
#     E Annotations for foo/xx
#     E ***************
#     M 1.3          (kingdon  06-Sep-97): hello 
#     ok
while (<Reader>) {
    @words = split;
    # Adding one is for the (single) space which follows $words[0].
    $rest = substr ($_, length ($words[0]) + 1);
    if ($words[0] eq "E") {
	# Not doing anything fancy is a bit of a kludge.  This _is_ a
	# demo script, so I'm sure anyone would be disappointed with more :-).
	print "Error message follows:\n";
	print &htmlify($rest);
    } elsif ($words[0] eq "M") {
	print &htmlify($rest);
    } elsif ($words[0] eq "MT") {
	if (substr ($words[1], 0, 1) eq "+") {
	    &start_tag (substr ($words[1], 1));
	} elsif (substr ($words[1], 0, 1) eq "-") {
	    &end_tag (substr ($words[1], 1));
	} elsif ($words[1] eq "newline") {
	    print "<br>\n";
	} else {
	    &start_tag ($words[1]);
	    # Adding one is for the (single) space which follows $words[1].
	    $text = substr ($rest, length ($words[1]) + 1);
	    # Remove the trailing newline.
	    chop($text);
	    print &htmlify ($text);
	    &end_tag ($words[1]);
	}
    } elsif ($words[0] eq "ok") {
	# We could complain about any text received after this, like the
	# CVS command line client.  But for simplicity, we don't.
    } elsif ($words[0] eq "error") {
	die $rest;
    } else {
	# This covers Checked-in, Updated, etc., which I don't think
	# the server should be sending in response to this command (not
	# that cvsclient.texi tries to specify this).
	warn "unrecognized response: $_";
    }
}
close (Reader) || warn "cannot close: $!";
print "</BODY></HTML>\n";

# Here are the XML-style versions of start_tag and end_tag.
# Probably this is cleaner than <span>.
#sub start_tag {
#    local($string) = @_;
#    print "<cvs-" . $string . ">";
#}
#sub end_tag {
#    local($string) = @_;
#    print "</cvs-" . $string . ">\n";
#}

# Here are the <span> versions of start_tag and end_tag.
# Hopefully I can get this working faster than the XML-style.
sub start_tag {
    local($string) = @_;
    print "<span name=\"" . $string . "\">";
}
sub end_tag {
    local($string) = @_;
    print "</span>\n";
}

sub htmlify {
    local($string) = @_;

    # I guess we want a non-breaking space.  The whole issue of filling
    # paragraphs is a bit fuzzy, but we don't want to lose information
    # when going to HTML/XML, so we want to make sure that spaces and
    # tabs and such do get distinguished from the normal whitespace
    # that separates the various bits of HTML/XML.
    $string =~ s/&/&amp;/g;
    $string =~ s/ /&nbsp;/g;
    $string =~ s/\t/&#09;/g;
    $string =~ s/</&lt;/g;
    $string =~ s/>/&gt;/g;
    $string =~ s/\n/<br>\n/g;

    $string;
}

