Remove all references to speculator (#3430)
Signed-off-by: DCFargo <drew.fargo@gmail.com>pull/977/head
parent
f1a4a58755
commit
43a71c0092
@ -1,18 +0,0 @@
|
|||||||
speculator allows you to preview pull requests to the matrix.org specification.
|
|
||||||
|
|
||||||
It serves the following HTTP endpoints:
|
|
||||||
- / lists open pull requests
|
|
||||||
- /spec/123 which renders the spec as html at pull request 123.
|
|
||||||
- /diff/rst/123 which gives a diff of the spec's rst at pull request 123.
|
|
||||||
- /diff/html/123 which gives a diff of the spec's HTML at pull request 123.
|
|
||||||
|
|
||||||
The build or run, you need a working `go` installation.
|
|
||||||
Then fetch dependencies:
|
|
||||||
` go get github.com/hashicorp/golang-lru`
|
|
||||||
|
|
||||||
To run it, then run:
|
|
||||||
`go run main.go`
|
|
||||||
|
|
||||||
To build the binary (which is necessary for deployment to the matrix.org
|
|
||||||
servers), you must again install `go` and dependencies, and then run:
|
|
||||||
`go build`
|
|
@ -1,564 +0,0 @@
|
|||||||
#!/usr/bin/perl
|
|
||||||
#
|
|
||||||
# htmldiff - present a diff marked version of two html documents
|
|
||||||
#
|
|
||||||
# Copyright (c) 1998-2006 MACS, Inc.
|
|
||||||
#
|
|
||||||
# Copyright (c) 2007 SiSco, Inc.
|
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
#
|
|
||||||
# See http://www.themacs.com for more information.
|
|
||||||
#
|
|
||||||
# usage: htmldiff [[-c] [-l] [-o] oldversion newversion [output]]
|
|
||||||
#
|
|
||||||
# -c - disable metahtml comment processing
|
|
||||||
# -o - disable outputting of old text
|
|
||||||
# -l - use navindex to create sequence of diffs
|
|
||||||
# oldversion - the previous version of the document
|
|
||||||
# newversion - the newer version of the document
|
|
||||||
# output - a filename to place the output in. If omitted, the output goes to
|
|
||||||
# standard output.
|
|
||||||
#
|
|
||||||
# if invoked with no options or arguments, operates as a CGI script. It then
|
|
||||||
# takes the following parameters:
|
|
||||||
#
|
|
||||||
# oldfile - the URL of the original file
|
|
||||||
# newfile - the URL of the new file
|
|
||||||
# mhtml - a flag to indicate whether it should be aware of MetaHTML comments.
|
|
||||||
#
|
|
||||||
# requires GNU diff utility
|
|
||||||
# also requires the perl modules Getopt::Std
|
|
||||||
#
|
|
||||||
# NOTE: The markup created by htmldiff may not validate against the HTML 4.0
|
|
||||||
# DTD. This is because the algorithm is realtively simple, and there are
|
|
||||||
# places in the markup content model where the span element is not allowed.
|
|
||||||
# Htmldiff is NOT aware of these places.
|
|
||||||
#
|
|
||||||
# $Source: /u/sources/public/2009/htmldiff/htmldiff.pl,v $
|
|
||||||
# $Revision: 1.1 $
|
|
||||||
#
|
|
||||||
# $Log: htmldiff.pl,v $
|
|
||||||
# Revision 1.1 2014/01/06 08:04:51 dom
|
|
||||||
# added copy of htmldiff perl script since aptest.com repo no longer available
|
|
||||||
#
|
|
||||||
# Revision 1.5 2008/03/05 13:23:16 ahby
|
|
||||||
# Fixed a problem with leading whitespace before markup.
|
|
||||||
#
|
|
||||||
# Revision 1.4 2007/12/13 13:09:16 ahby
|
|
||||||
# Updated copyright and license.
|
|
||||||
#
|
|
||||||
# Revision 1.3 2007/12/13 12:53:34 ahby
|
|
||||||
# Changed use of span to ins and del
|
|
||||||
#
|
|
||||||
# Revision 1.2 2002/02/13 16:27:23 ahby
|
|
||||||
# Changed processing model.
|
|
||||||
# Improved handling of old text and changed styles.
|
|
||||||
#
|
|
||||||
# Revision 1.1 2000/07/12 12:20:04 ahby
|
|
||||||
# Updated to remove empty spans - this fixes validation problems under
|
|
||||||
# strict.
|
|
||||||
#
|
|
||||||
# Revision 1.11 1999/12/08 19:46:45 ahby
|
|
||||||
# Fixed validation errors introduced by placing markup where it didn't
|
|
||||||
# belong.
|
|
||||||
#
|
|
||||||
# Revision 1.10 1999/10/18 13:42:58 ahby
|
|
||||||
# Added -o to the usage message.
|
|
||||||
#
|
|
||||||
# Revision 1.9 1999/05/04 12:29:11 ahby
|
|
||||||
# Added an option to turn off the display of old text.
|
|
||||||
#
|
|
||||||
# Revision 1.8 1999/04/09 14:37:27 ahby
|
|
||||||
# Fixed a perl syntax error.
|
|
||||||
#
|
|
||||||
# Revision 1.7 1999/04/09 14:35:49 ahby
|
|
||||||
# Added reference to MACS homepage.
|
|
||||||
#
|
|
||||||
# Revision 1.6 1999/04/09 14:35:09 ahby
|
|
||||||
# Added comment about validity of generated markup.
|
|
||||||
#
|
|
||||||
# Revision 1.5 1999/02/22 22:17:54 ahby
|
|
||||||
# Changed to use stylesheets.
|
|
||||||
# Changed to rely upon span.
|
|
||||||
# Changed to work around content model problems.
|
|
||||||
#
|
|
||||||
# Revision 1.4 1999/02/08 02:32:22 ahby
|
|
||||||
# Added a copyright statement.
|
|
||||||
#
|
|
||||||
# Revision 1.3 1999/02/08 02:30:40 ahby
|
|
||||||
# Added header processing.
|
|
||||||
#
|
|
||||||
# Revision 1.2 1998/12/10 17:31:31 ahby
|
|
||||||
# Fixed to escape less-thans in change blocks and to not permit change
|
|
||||||
# markup within specific elements (like TITLE).
|
|
||||||
#
|
|
||||||
# Revision 1.1 1998/11/26 00:09:22 ahby
|
|
||||||
# Initial revision
|
|
||||||
#
|
|
||||||
#
|
|
||||||
|
|
||||||
use Getopt::Std;
|
|
||||||
|
|
||||||
sub usage {
|
|
||||||
print STDERR "htmldiff [-c] [-o] oldversion newversion [output]\n";
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
sub url_encode {
|
|
||||||
my $str = shift;
|
|
||||||
$str =~ s/([\x00-\x1f\x7F-\xFF])/
|
|
||||||
sprintf ('%%%02x', ord ($1))/eg;
|
|
||||||
return $str;
|
|
||||||
}
|
|
||||||
|
|
||||||
# markit - diff-mark the streams
|
|
||||||
#
|
|
||||||
# markit(file1, file2)
|
|
||||||
#
|
|
||||||
# markit relies upon GNUdiff to mark up the text.
|
|
||||||
#
|
|
||||||
# The markup is encoded using special control sequences:
|
|
||||||
#
|
|
||||||
# a block wrapped in control-a is deleted text
|
|
||||||
# a block wrapped in control-b is old text
|
|
||||||
# a block wrapped in control-c is new text
|
|
||||||
#
|
|
||||||
# The main processing loop attempts to wrap the text blocks in appropriate
|
|
||||||
# SPANs based upon the type of text that it is.
|
|
||||||
#
|
|
||||||
# When the loop encounters a < in the text, it stops the span. Then it outputs
|
|
||||||
# the element that is defined, then it restarts the span.
|
|
||||||
|
|
||||||
sub markit {
|
|
||||||
my $retval = "";
|
|
||||||
my($file1) = shift;
|
|
||||||
my($file2) = shift;
|
|
||||||
# my $old="<span class=\\\"diff-old-a\\\">deleted text: </span>%c'\012'%c'\001'%c'\012'%<%c'\012'%c'\001'%c'\012'";
|
|
||||||
my $old="%c'\012'%c'\001'%c'\012'%<%c'\012'%c'\001'%c'\012'";
|
|
||||||
my $new="%c'\012'%c'\003'%c'\012'%>%c'\012'%c'\003'%c'\012'";
|
|
||||||
my $unchanged="%=";
|
|
||||||
my $changed="%c'\012'%c'\001'%c'\012'%<%c'\012'%c'\001'%c'\012'%c'\004'%c'\012'%>%c'\012'%c'\004'%c'\012'";
|
|
||||||
if ($opt_o) {
|
|
||||||
$old = "";
|
|
||||||
$changed = "%c'\012'%c'\004'%c'\012'%>%c'\012'%c'\004'%c'\012'";
|
|
||||||
}
|
|
||||||
# my $old="%c'\002'<font color=\\\"purple\\\" size=\\\"-2\\\">deleted text:</font><s>%c'\012'%c'\001'%c'\012'%<%c'\012'%c'\001'%c'\012'</s>%c'\012'%c'\002'";
|
|
||||||
# my $new="%c'\002'<font color=\\\"purple\\\"><u>%c'\012'%c'\002'%>%c'\002'</u></font>%c'\002'%c'\012'";
|
|
||||||
# my $unchanged="%=";
|
|
||||||
# my $changed="%c'\002'<s>%c'\012'%c'\001'%c'\012'%<%c'\012'%c'\001'%c'\012'</s><font color=\\\"purple\\\"><u>%c'\002'%c'\012'%>%c'\012'%c'\002'</u></font>%c'\002'%c'\012'";
|
|
||||||
|
|
||||||
my @span;
|
|
||||||
$span[0]="</span>";
|
|
||||||
$span[1]="<del class=\"diff-old\">";
|
|
||||||
$span[2]="<del class=\"diff-old\">";
|
|
||||||
$span[3]="<ins class=\"diff-new\">";
|
|
||||||
$span[4]="<ins class=\"diff-chg\">";
|
|
||||||
|
|
||||||
my @diffEnd ;
|
|
||||||
$diffEnd[1] = '</del>';
|
|
||||||
$diffEnd[2] = '</del>';
|
|
||||||
$diffEnd[3] = '</ins>';
|
|
||||||
$diffEnd[4] = '</ins>';
|
|
||||||
|
|
||||||
my $diffcounter = 0;
|
|
||||||
|
|
||||||
open(FILE, qq(diff -d --old-group-format="$old" --new-group-format="$new" --changed-group-format="$changed" --unchanged-group-format="$unchanged" $file1 $file2 |)) || die("Diff failed: $!");
|
|
||||||
# system (qq(diff --old-group-format="$old" --new-group-format="$new" --changed-group-format="$changed" --unchanged-group-format="$unchanged" $file1 $file2 > /tmp/output));
|
|
||||||
|
|
||||||
my $state = 0;
|
|
||||||
my $inblock = 0;
|
|
||||||
my $temp = "";
|
|
||||||
my $lineCount = 0;
|
|
||||||
|
|
||||||
# strategy:
|
|
||||||
#
|
|
||||||
# process the output of diff...
|
|
||||||
#
|
|
||||||
# a link with control A-D means the start/end of the corresponding ordinal
|
|
||||||
# state (1-4). Resting state is state 0.
|
|
||||||
#
|
|
||||||
# While in a state, accumulate the contents for that state. When exiting the
|
|
||||||
# state, determine if it is appropriate to emit the contents with markup or
|
|
||||||
# not (basically, if the accumulated buffer contains only empty lines or lines
|
|
||||||
# with markup, then we don't want to emit the wrappers. We don't need them.
|
|
||||||
#
|
|
||||||
# Note that if there is markup in the "old" block, that markup is silently
|
|
||||||
# removed. It isn't really that interesting, and it messes up the output
|
|
||||||
# something fierce.
|
|
||||||
|
|
||||||
while (<FILE>) {
|
|
||||||
my $anchor = $opt_l ? qq[<a tabindex="$diffcounter">] : "" ;
|
|
||||||
my $anchorEnd = $opt_l ? q[</a>] : "" ;
|
|
||||||
$lineCount ++;
|
|
||||||
if ($state == 0) { # if we are resting and we find a marker,
|
|
||||||
# then we must be entering a block
|
|
||||||
if (m/^([\001-\004])/) {
|
|
||||||
$state = ord($1);
|
|
||||||
$_ = "";
|
|
||||||
}
|
|
||||||
# if (m/^\001/) {
|
|
||||||
# $state = 1;
|
|
||||||
# s/^/$span[1]/;
|
|
||||||
# } elsif (m/^\002/) {
|
|
||||||
# $state = 2;
|
|
||||||
# s/^/$span[2]/;
|
|
||||||
# } elsif (m/^\003/) {
|
|
||||||
# $state = 3;
|
|
||||||
# s/^/$span[3]/;
|
|
||||||
# } elsif (m/^\004/) {
|
|
||||||
# $state = 4;
|
|
||||||
# s/^/$span[4]/;
|
|
||||||
# }
|
|
||||||
} else {
|
|
||||||
# if we are in "old" state, remove markup
|
|
||||||
if (($state == 1) || ($state == 2)) {
|
|
||||||
s/\<.*\>//; # get rid of any old markup
|
|
||||||
s/\</</g; # escape any remaining STAG or ETAGs
|
|
||||||
s/\>/>/g;
|
|
||||||
}
|
|
||||||
# if we found another marker, we must be exiting the state
|
|
||||||
if (m/^([\001-\004])/) {
|
|
||||||
if ($temp ne "") {
|
|
||||||
$_ = $span[$state] . $anchor . $temp . $anchorEnd . $diffEnd[$state] . "\n";
|
|
||||||
$temp = "";
|
|
||||||
} else {
|
|
||||||
$_ = "" ;
|
|
||||||
}
|
|
||||||
$state = 0;
|
|
||||||
} elsif (m/^\s*\</) { # otherwise, is this line markup?
|
|
||||||
# if it is markup AND we haven't seen anything else yet,
|
|
||||||
# then we will emit the markup
|
|
||||||
if ($temp eq "") {
|
|
||||||
$retval .= $_;
|
|
||||||
$_ = "";
|
|
||||||
} else { # we wrap it with the state switches and hold it
|
|
||||||
s/^/$anchorEnd$diffEnd[$state]/;
|
|
||||||
s/$/$span[$state]$anchor/;
|
|
||||||
$temp .= $_;
|
|
||||||
$_ = "";
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (m/.+/) {
|
|
||||||
$temp .= $_;
|
|
||||||
$_ = "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
s/\001//g;
|
|
||||||
s/\002//g;
|
|
||||||
s/\003//g;
|
|
||||||
s/\004//g;
|
|
||||||
if ($_ !~ m/^$/) {
|
|
||||||
$retval .= $_;
|
|
||||||
}
|
|
||||||
$diffcounter++;
|
|
||||||
}
|
|
||||||
close FILE;
|
|
||||||
$retval =~ s/$span[1]\n+$diffEnd[1]//g;
|
|
||||||
$retval =~ s/$span[2]\n+$diffEnd[2]//g;
|
|
||||||
$retval =~ s/$span[3]\n+$diffEnd[3]//g;
|
|
||||||
$retval =~ s/$span[4]\n+$diffEnd[4]//g;
|
|
||||||
$retval =~ s/$span[1]\n*$//g;
|
|
||||||
$retval =~ s/$span[2]\n*$//g;
|
|
||||||
$retval =~ s/$span[3]\n*$//g;
|
|
||||||
$retval =~ s/$span[4]\n*$//g;
|
|
||||||
return $retval;
|
|
||||||
}
|
|
||||||
|
|
||||||
sub splitit {
|
|
||||||
my $filename = shift;
|
|
||||||
my $headertmp = shift;
|
|
||||||
my $inheader=0;
|
|
||||||
my $preformatted=0;
|
|
||||||
my $inelement=0;
|
|
||||||
my $retval = "";
|
|
||||||
my $styles = q(<style type='text/css'>
|
|
||||||
.diff-old-a {
|
|
||||||
font-size: smaller;
|
|
||||||
color: red;
|
|
||||||
}
|
|
||||||
|
|
||||||
.diff-new { background-color: yellow; }
|
|
||||||
.diff-chg { background-color: lime; }
|
|
||||||
.diff-new:before,
|
|
||||||
.diff-new:after
|
|
||||||
{ content: "\2191" }
|
|
||||||
.diff-chg:before, .diff-chg:after
|
|
||||||
{ content: "\2195" }
|
|
||||||
.diff-old { text-decoration: line-through; background-color: #FBB; }
|
|
||||||
.diff-old:before,
|
|
||||||
.diff-old:after
|
|
||||||
{ content: "\2193" }
|
|
||||||
:focus { border: thin red solid}
|
|
||||||
</style>
|
|
||||||
);
|
|
||||||
if ($opt_t) {
|
|
||||||
$styles .= q(
|
|
||||||
<script type="text/javascript">
|
|
||||||
<!--
|
|
||||||
function setOldDisplay() {
|
|
||||||
for ( var s = 0; s < document.styleSheets.length; s++ ) {
|
|
||||||
var css = document.styleSheets[s];
|
|
||||||
var mydata ;
|
|
||||||
try { mydata = css.cssRules ;
|
|
||||||
if ( ! mydata ) mydata = css.rules;
|
|
||||||
for ( var r = 0; r < mydata.length; r++ ) {
|
|
||||||
if ( mydata[r].selectorText == '.diff-old' ) {
|
|
||||||
mydata[r].style.display = ( mydata[r].style.display == '' ) ? 'none'
|
|
||||||
: '';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch(e) {} ;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-->
|
|
||||||
</script>
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($stripheader) {
|
|
||||||
open(HEADER, ">$headertmp");
|
|
||||||
}
|
|
||||||
|
|
||||||
my $incomment = 0;
|
|
||||||
my $inhead = 1;
|
|
||||||
open(FILE, $filename) || die("File $filename cannot be opened: $!");
|
|
||||||
while (<FILE>) {
|
|
||||||
if ($inhead == 1) {
|
|
||||||
if (m/\<\/head/i) {
|
|
||||||
print HEADER $styles;
|
|
||||||
}
|
|
||||||
if (m/\<body/i) {
|
|
||||||
$inhead = 0;
|
|
||||||
print HEADER;
|
|
||||||
if ($opt_t) {
|
|
||||||
print HEADER q(
|
|
||||||
<form action=""><input type="button" onclick="setOldDisplay()" value="Show/Hide Old Content" /></form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
close HEADER;
|
|
||||||
} else {
|
|
||||||
print HEADER;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if ($incomment) {
|
|
||||||
if (m;-->;) {
|
|
||||||
$incomment = 0;
|
|
||||||
s/.*-->//;
|
|
||||||
} else {
|
|
||||||
next;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (m;<!--;) {
|
|
||||||
while (m;<!--.*-->;) {
|
|
||||||
s/<!--.*?-->//;
|
|
||||||
}
|
|
||||||
if (m;<!--; ) {
|
|
||||||
$incomment = 1;
|
|
||||||
s/<!--.*//;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (m/\<pre/i) {
|
|
||||||
$preformatted = 1;
|
|
||||||
}
|
|
||||||
if (m/\<\/pre\>/i) {
|
|
||||||
$preformatted = 0;
|
|
||||||
}
|
|
||||||
if ($preformatted) {
|
|
||||||
$retval .= $_;
|
|
||||||
} elsif ($mhtmlcomments && /^;;;/) {
|
|
||||||
$retval .= $_;
|
|
||||||
} else {
|
|
||||||
my @list = split(' ');
|
|
||||||
foreach $element (@list) {
|
|
||||||
if ($element =~ m/\<H[1-6]/i) {
|
|
||||||
# $inheader = 1;
|
|
||||||
}
|
|
||||||
if ($inheader == 0) {
|
|
||||||
$element =~ s/</\n</g;
|
|
||||||
$element =~ s/^\n//;
|
|
||||||
$element =~ s/>/>\n/g;
|
|
||||||
$element =~ s/\n$//;
|
|
||||||
$element =~ s/>\n([.,:!]+)/>$1/g;
|
|
||||||
}
|
|
||||||
if ($element =~ m/\<\/H[1-6]\>/i) {
|
|
||||||
$inheader = 0;
|
|
||||||
}
|
|
||||||
$retval .= "$element";
|
|
||||||
$inelement += ($element =~ s/</</g);
|
|
||||||
$inelement -= ($element =~ s/>/>/g);
|
|
||||||
if ($inelement < 0) {
|
|
||||||
$inelement = 0;
|
|
||||||
}
|
|
||||||
if (($inelement == 0) && ($inheader == 0)) {
|
|
||||||
$retval .= "\n";
|
|
||||||
} else {
|
|
||||||
$retval .= " ";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
undef @list;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$retval .= "\n";
|
|
||||||
close FILE;
|
|
||||||
return $retval;
|
|
||||||
}
|
|
||||||
|
|
||||||
$mhtmlcomments = 1;
|
|
||||||
|
|
||||||
sub cli {
|
|
||||||
getopts("clto") || usage();
|
|
||||||
|
|
||||||
if ($opt_c) {$mhtmlcomments = 0;}
|
|
||||||
|
|
||||||
if (@ARGV < 2) { usage(); }
|
|
||||||
|
|
||||||
$file1 = $ARGV[0];
|
|
||||||
$file2 = $ARGV[1];
|
|
||||||
$file3 = $ARGV[2];
|
|
||||||
|
|
||||||
$tmp = splitit($file1, $headertmp1);
|
|
||||||
open (FILE, ">$tmp1");
|
|
||||||
print FILE $tmp;
|
|
||||||
close FILE;
|
|
||||||
|
|
||||||
$tmp = splitit($file2, $headertmp2);
|
|
||||||
open (FILE, ">$tmp2");
|
|
||||||
print FILE $tmp;
|
|
||||||
close FILE;
|
|
||||||
|
|
||||||
$output = "";
|
|
||||||
|
|
||||||
if ($stripheader) {
|
|
||||||
open(FILE, $headertmp2);
|
|
||||||
while (<FILE>) {
|
|
||||||
$output .= $_;
|
|
||||||
}
|
|
||||||
close(FILE);
|
|
||||||
}
|
|
||||||
|
|
||||||
$output .= markit($tmp1, $tmp2);
|
|
||||||
|
|
||||||
if ($file3) {
|
|
||||||
open(FILE, ">$file3");
|
|
||||||
print FILE $output;
|
|
||||||
close FILE;
|
|
||||||
} else {
|
|
||||||
print $output;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sub cgi {
|
|
||||||
# use LWP::UserAgent;
|
|
||||||
# use CGI;
|
|
||||||
|
|
||||||
my $query = new CGI;
|
|
||||||
my $url1 = $query->param("oldfile");
|
|
||||||
my $url2 = $query->param("newfile");
|
|
||||||
my $mhtml = $query->param("mhtml");
|
|
||||||
|
|
||||||
my $file1 = "/tmp/htdcgi1.$$";
|
|
||||||
my $file2 = "/tmp/htdcgi2.$$";
|
|
||||||
|
|
||||||
my $ua = new LWP::UserAgent;
|
|
||||||
$ua->agent("MACS, Inc. HTMLdiff/0.9 " . $ua->agent);
|
|
||||||
|
|
||||||
# Create a request
|
|
||||||
|
|
||||||
my $req1 = new HTTP::Request GET => $url1;
|
|
||||||
|
|
||||||
my $res1 = $ua->request($req1, $file1);
|
|
||||||
if ($res1->is_error) {
|
|
||||||
print $res1->error_as_HTML();
|
|
||||||
print "<p>The URL $url1 could not be found. Please check it and try again.</p>";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
my $req2 = new HTTP::Request GET => $url2;
|
|
||||||
|
|
||||||
my $res2 = $ua->request($req2, $file2);
|
|
||||||
if ($res2->is_error) {
|
|
||||||
print $res2->error_as_HTML();
|
|
||||||
print "<p>The URL $url2 could not be found. Please check it and try again.</p>";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$split1 = splitit($file1, $headertmp1);
|
|
||||||
open (FILE, ">$tmp1");
|
|
||||||
print FILE $split1;
|
|
||||||
close FILE;
|
|
||||||
|
|
||||||
$split2 = splitit($file2, $headertmp2);
|
|
||||||
open (FILE, ">$tmp2");
|
|
||||||
print FILE $split2;
|
|
||||||
close FILE;
|
|
||||||
|
|
||||||
$output = "";
|
|
||||||
|
|
||||||
if ($stripheader) {
|
|
||||||
open(FILE, $headertmp2);
|
|
||||||
while (<FILE>) {
|
|
||||||
$output .= $_;
|
|
||||||
}
|
|
||||||
close(FILE);
|
|
||||||
}
|
|
||||||
|
|
||||||
$output .= markit($tmp1, $tmp2);
|
|
||||||
|
|
||||||
my $base=$res2->base;
|
|
||||||
|
|
||||||
if ($base !~ /\/$/) {
|
|
||||||
$base =~ s/[^\/]*$//;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( $output !~ /<base/i ) {
|
|
||||||
$output =~ s/<head>/<head>\n<base href="$base">/i ||
|
|
||||||
$output =~ s/<html>/<html>\n<base href="$base">/i ;
|
|
||||||
}
|
|
||||||
|
|
||||||
print $query->header(-type=>'text/html',-nph=>1);
|
|
||||||
print $output;
|
|
||||||
|
|
||||||
unlink $file1;
|
|
||||||
unlink $file2;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
$tmp1="/tmp/htdtmp1.$$";
|
|
||||||
$headertmp1="/tmp/htdhtmp1.$$";
|
|
||||||
$tmp2="/tmp/htdtmp2.$$";
|
|
||||||
$headertmp2="/tmp/htdhtmp2.$$";
|
|
||||||
$stripheader = 1;
|
|
||||||
|
|
||||||
if (@ARGV == 0) {
|
|
||||||
cgi(); # if no arguments, we must be operating as a cgi script
|
|
||||||
} else {
|
|
||||||
cli(); # if there are arguments, then we are operating as a CLI
|
|
||||||
}
|
|
||||||
|
|
||||||
unlink $tmp1;
|
|
||||||
unlink $headertmp1;
|
|
||||||
unlink $tmp2;
|
|
||||||
unlink $headertmp2;
|
|
@ -1,775 +0,0 @@
|
|||||||
// speculator allows you to preview pull requests to the matrix.org specification.
|
|
||||||
// It serves the following HTTP endpoints:
|
|
||||||
// - / lists open pull requests
|
|
||||||
// - /spec/123 which renders the spec as html at pull request 123.
|
|
||||||
// - /diff/rst/123 which gives a diff of the spec's rst at pull request 123.
|
|
||||||
// - /diff/html/123 which gives a diff of the spec's HTML at pull request 123.
|
|
||||||
// It is currently woefully inefficient, and there is a lot of low hanging fruit for improvement.
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
|
||||||
"math/rand"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"path"
|
|
||||||
"path/filepath"
|
|
||||||
"regexp"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"syscall"
|
|
||||||
"text/template"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/hashicorp/golang-lru"
|
|
||||||
)
|
|
||||||
|
|
||||||
type PullRequest struct {
|
|
||||||
Number int
|
|
||||||
Base Commit
|
|
||||||
Head Commit
|
|
||||||
Title string
|
|
||||||
User User
|
|
||||||
HTMLURL string `json:"html_url"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Commit struct {
|
|
||||||
SHA string
|
|
||||||
Repo RequestRepo
|
|
||||||
}
|
|
||||||
|
|
||||||
type RequestRepo struct {
|
|
||||||
CloneURL string `json:"clone_url"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type User struct {
|
|
||||||
Login string
|
|
||||||
HTMLURL string `json:"html_url"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
port = flag.Int("port", 9000, "Port on which to listen for HTTP")
|
|
||||||
includesDir = flag.String("includes_dir", "", "Directory containing include files for styling like matrix.org")
|
|
||||||
accessToken = flag.String("access_token", "", "github.com access token")
|
|
||||||
allowedMembers map[string]bool
|
|
||||||
specCache *lru.Cache // string -> map[string][]byte filename -> contents
|
|
||||||
styledSpecCache *lru.Cache // string -> map[string][]byte filename -> contents
|
|
||||||
)
|
|
||||||
|
|
||||||
func (u *User) IsTrusted() bool {
|
|
||||||
return allowedMembers[u.Login]
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
pullsPrefix = "https://api.github.com/repos/matrix-org/matrix-doc/pulls"
|
|
||||||
matrixDocCloneURL = "https://github.com/matrix-org/matrix-doc.git"
|
|
||||||
permissionsOwnerFull = 0700
|
|
||||||
)
|
|
||||||
|
|
||||||
var numericRegex = regexp.MustCompile(`^\d+$`)
|
|
||||||
|
|
||||||
func accessTokenQuerystring() string {
|
|
||||||
if *accessToken == "" {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("?access_token=%s", *accessToken)
|
|
||||||
}
|
|
||||||
|
|
||||||
func gitClone(url string, directory string, shared bool) error {
|
|
||||||
args := []string{"clone", url, directory}
|
|
||||||
if shared {
|
|
||||||
args = append(args, "--shared")
|
|
||||||
}
|
|
||||||
if err := runGitCommand(directory, args); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func gitCheckout(path, sha string) error {
|
|
||||||
return runGitCommand(path, []string{"checkout", sha})
|
|
||||||
}
|
|
||||||
|
|
||||||
func runGitCommand(path string, args []string) error {
|
|
||||||
cmd := exec.Command("git", args...)
|
|
||||||
cmd.Dir = path
|
|
||||||
var b bytes.Buffer
|
|
||||||
cmd.Stderr = &b
|
|
||||||
if err := cmd.Run(); err != nil {
|
|
||||||
return fmt.Errorf("error running %q: %v (stderr: %s)", strings.Join(cmd.Args, " "), err, b.String())
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func lookupPullRequest(prNumber string) (*PullRequest, error) {
|
|
||||||
resp, err := http.Get(fmt.Sprintf("%s/%s%s", pullsPrefix, prNumber, accessTokenQuerystring()))
|
|
||||||
defer resp.Body.Close()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error getting pulls: %v", err)
|
|
||||||
}
|
|
||||||
if resp.StatusCode != 200 {
|
|
||||||
body, _ := ioutil.ReadAll(resp.Body)
|
|
||||||
return nil, fmt.Errorf("error getting pull request %s: %v", prNumber, string(body))
|
|
||||||
}
|
|
||||||
dec := json.NewDecoder(resp.Body)
|
|
||||||
var pr PullRequest
|
|
||||||
if err := dec.Decode(&pr); err != nil {
|
|
||||||
return nil, fmt.Errorf("error decoding pulls: %v", err)
|
|
||||||
}
|
|
||||||
return &pr, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *server) lookupBranch(branch string) (string, error) {
|
|
||||||
err := s.updateBase()
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Error fetching: %v, will use cached branches")
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.ToLower(branch) == "head" {
|
|
||||||
branch = "master"
|
|
||||||
}
|
|
||||||
branch = "origin/" + branch
|
|
||||||
sha, err := s.getSHAOf(branch)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("error getting branch %s: %v", branch, err)
|
|
||||||
}
|
|
||||||
if sha == "" {
|
|
||||||
return "", fmt.Errorf("Unable to get sha for %s", branch)
|
|
||||||
}
|
|
||||||
return sha, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func generate(dir string) error {
|
|
||||||
cmd := exec.Command("python", "gendoc.py", "--nodelete")
|
|
||||||
cmd.Dir = path.Join(dir, "scripts")
|
|
||||||
var b bytes.Buffer
|
|
||||||
cmd.Stderr = &b
|
|
||||||
if err := cmd.Run(); err != nil {
|
|
||||||
return fmt.Errorf("error generating spec: %v\nOutput from gendoc:\n%v", err, b.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
// cheekily dump the swagger docs into the gen directory so they can be
|
|
||||||
// served by serveSpec
|
|
||||||
cmd = exec.Command("python", "dump-swagger.py", "-o", "gen/api-docs.json")
|
|
||||||
cmd.Dir = path.Join(dir, "scripts")
|
|
||||||
cmd.Stderr = &b
|
|
||||||
if err := cmd.Run(); err != nil {
|
|
||||||
return fmt.Errorf("error generating api docs: %v\nOutput from dump-swagger:\n%v", err, b.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeError(w http.ResponseWriter, code int, err error) {
|
|
||||||
w.Header().Set("Content-Type", "text/plain")
|
|
||||||
w.WriteHeader(code)
|
|
||||||
io.WriteString(w, fmt.Sprintf("%v\n", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
type server struct {
|
|
||||||
mu sync.Mutex // Must be locked around any git command on matrixDocCloneURL
|
|
||||||
matrixDocCloneURL string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *server) updateBase() error {
|
|
||||||
s.mu.Lock()
|
|
||||||
defer s.mu.Unlock()
|
|
||||||
return runGitCommand(s.matrixDocCloneURL, []string{"fetch"})
|
|
||||||
}
|
|
||||||
|
|
||||||
// canCheckout returns whether a given sha can currently be checked out from s.matrixDocCloneURL.
|
|
||||||
func (s *server) canCheckout(sha string) bool {
|
|
||||||
s.mu.Lock()
|
|
||||||
defer s.mu.Unlock()
|
|
||||||
return runGitCommand(s.matrixDocCloneURL, []string{"cat-file", "-e", sha + "^{commit}"}) == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// generateAt generates spec from repo at sha.
|
|
||||||
// Returns the path where the generation was done.
|
|
||||||
func (s *server) generateAt(sha string) (dst string, err error) {
|
|
||||||
if !s.canCheckout(sha) {
|
|
||||||
err = s.updateBase()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dst, err = makeTempDir()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log.Printf("Generating %s in %s\n", sha, dst)
|
|
||||||
s.mu.Lock()
|
|
||||||
err = gitClone(s.matrixDocCloneURL, dst, true)
|
|
||||||
s.mu.Unlock()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = gitCheckout(dst, sha); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = generate(dst)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *server) getSHAOf(ref string) (string, error) {
|
|
||||||
cmd := exec.Command("git", "rev-list", ref, "-n1")
|
|
||||||
cmd.Dir = path.Join(s.matrixDocCloneURL)
|
|
||||||
var b bytes.Buffer
|
|
||||||
cmd.Stdout = &b
|
|
||||||
s.mu.Lock()
|
|
||||||
err := cmd.Run()
|
|
||||||
s.mu.Unlock()
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("error generating spec: %v\nOutput from git:\n%v", err, b.String())
|
|
||||||
}
|
|
||||||
return strings.TrimSpace(b.String()), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// extractPRNumber checks that the path begins with the given base, and returns
|
|
||||||
// the following component.
|
|
||||||
func extractPRNumber(path, base string) (string, error) {
|
|
||||||
if !strings.HasPrefix(path, base+"/") {
|
|
||||||
return "", fmt.Errorf("invalid path passed: %q expect %s/123", path, base)
|
|
||||||
}
|
|
||||||
return strings.Split(path[len(base)+1:], "/")[0], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// extractPath extracts the file path within the gen directory which should be served for the request.
|
|
||||||
// Returns one of (file to serve, path to redirect to).
|
|
||||||
// path is the actual path being requested, e.g. "/spec/head/client_server.html".
|
|
||||||
// base is the base path of the handler, including a trailing slash, before the PR number, e.g. "/spec/".
|
|
||||||
func extractPath(path, base string) (string, string) {
|
|
||||||
// Assumes exactly one flat directory
|
|
||||||
|
|
||||||
// Count slashes in /spec/head/client_server.html
|
|
||||||
// base is /spec/
|
|
||||||
// +1 for the PR number - /spec/head
|
|
||||||
// +1 for the path-part after the slash after the PR number
|
|
||||||
max := strings.Count(base, "/") + 2
|
|
||||||
parts := strings.SplitN(path, "/", max)
|
|
||||||
|
|
||||||
if len(parts) < max {
|
|
||||||
// Path is base/pr - redirect to base/pr/index.html
|
|
||||||
return "", path + "/index.html"
|
|
||||||
}
|
|
||||||
if parts[max-1] == "" {
|
|
||||||
// Path is base/pr/ - serve index.html
|
|
||||||
return "index.html", ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// Path is base/pr/file.html - serve file
|
|
||||||
return parts[max-1], ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *server) serveSpec(w http.ResponseWriter, req *http.Request) {
|
|
||||||
var sha string
|
|
||||||
|
|
||||||
var styleLikeMatrixDotOrg = req.URL.Query().Get("matrixdotorgstyle") != ""
|
|
||||||
|
|
||||||
if styleLikeMatrixDotOrg && *includesDir == "" {
|
|
||||||
writeError(w, 500, fmt.Errorf("Cannot style like matrix.org - no include dir specified"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// we use URL.EscapedPath() to get hold of the %-encoded version of the
|
|
||||||
// path, so that we can handle branch names with slashes in.
|
|
||||||
urlPath := req.URL.EscapedPath()
|
|
||||||
|
|
||||||
if urlPath == "/spec" {
|
|
||||||
// special treatment for /spec - redirect to /spec/HEAD/
|
|
||||||
s.redirectTo(w, req, "/spec/HEAD/")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !strings.HasPrefix(urlPath, "/spec/") {
|
|
||||||
writeError(w, 500, fmt.Errorf("invalid path passed: %q expect /spec/...", urlPath))
|
|
||||||
}
|
|
||||||
|
|
||||||
splits := strings.SplitN(urlPath[6:], "/", 2)
|
|
||||||
|
|
||||||
if len(splits) == 1 {
|
|
||||||
// "/spec/foo" - redirect to "/spec/foo/" (so that relative links from the index work)
|
|
||||||
if splits[0] == "" {
|
|
||||||
s.redirectTo(w, req, "/spec/HEAD/")
|
|
||||||
} else {
|
|
||||||
s.redirectTo(w, req, urlPath+"/")
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// now we have:
|
|
||||||
// splits[0] is a PR#, or a branch name
|
|
||||||
// splits[1] is the file to serve
|
|
||||||
|
|
||||||
branchName, _ := url.QueryUnescape(splits[0])
|
|
||||||
requestedPath, _ := url.QueryUnescape(splits[1])
|
|
||||||
if requestedPath == "" {
|
|
||||||
requestedPath = "index.html"
|
|
||||||
}
|
|
||||||
|
|
||||||
if numericRegex.MatchString(branchName) {
|
|
||||||
// PR number
|
|
||||||
pr, err := lookupPullRequest(branchName)
|
|
||||||
if err != nil {
|
|
||||||
writeError(w, 400, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// We're going to run whatever Python is specified in the pull request, which
|
|
||||||
// may do bad things, so only trust people we trust.
|
|
||||||
if err := checkAuth(pr); err != nil {
|
|
||||||
writeError(w, 403, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
sha = pr.Head.SHA
|
|
||||||
log.Printf("Serving pr %s (%s)\n", branchName, sha)
|
|
||||||
} else if strings.ToLower(branchName) == "head" ||
|
|
||||||
branchName == "master" ||
|
|
||||||
strings.HasPrefix(branchName, "attic/drafts/") {
|
|
||||||
branchSHA, err := s.lookupBranch(branchName)
|
|
||||||
if err != nil {
|
|
||||||
writeError(w, 400, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
sha = branchSHA
|
|
||||||
log.Printf("Serving branch %s (%s)\n", branchName, sha)
|
|
||||||
} else {
|
|
||||||
writeError(w, 404, fmt.Errorf("invalid branch name"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var cache = specCache
|
|
||||||
if styleLikeMatrixDotOrg {
|
|
||||||
cache = styledSpecCache
|
|
||||||
}
|
|
||||||
|
|
||||||
var pathToContent map[string][]byte
|
|
||||||
|
|
||||||
if cached, ok := cache.Get(sha); ok {
|
|
||||||
pathToContent = cached.(map[string][]byte)
|
|
||||||
} else {
|
|
||||||
dst, err := s.generateAt(sha)
|
|
||||||
defer os.RemoveAll(dst)
|
|
||||||
if err != nil {
|
|
||||||
writeError(w, 500, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
pathToContent = make(map[string][]byte)
|
|
||||||
scriptsdir := path.Join(dst, "scripts")
|
|
||||||
base := path.Join(scriptsdir, "gen")
|
|
||||||
walker := func(path string, info os.FileInfo, err error) error {
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if info.IsDir() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
rel, err := filepath.Rel(base, path)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Failed to get relative path of %s: %v", path, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if styleLikeMatrixDotOrg {
|
|
||||||
cmd := exec.Command("./add-matrix-org-stylings.pl", *includesDir, path)
|
|
||||||
cmd.Dir = scriptsdir
|
|
||||||
var b bytes.Buffer
|
|
||||||
cmd.Stderr = &b
|
|
||||||
if err := cmd.Run(); err != nil {
|
|
||||||
return fmt.Errorf("error styling spec: %v\nOutput:\n%v", err, b.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bytes, err := ioutil.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Error reading spec: %v", err)
|
|
||||||
}
|
|
||||||
pathToContent[rel] = bytes
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
err = filepath.Walk(base, walker)
|
|
||||||
if err != nil {
|
|
||||||
writeError(w, 500, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
cache.Add(sha, pathToContent)
|
|
||||||
}
|
|
||||||
|
|
||||||
if requestedPath == "api-docs.json" {
|
|
||||||
// allow other swagger UIs access to our swagger
|
|
||||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
|
||||||
}
|
|
||||||
|
|
||||||
if b, ok := pathToContent[requestedPath]; ok {
|
|
||||||
w.Write(b)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if requestedPath == "index.html" {
|
|
||||||
// Fall back to single-page spec for old PRs
|
|
||||||
if b, ok := pathToContent["specification.html"]; ok {
|
|
||||||
w.Write(b)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
w.WriteHeader(404)
|
|
||||||
w.Write([]byte("Not found"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *server) redirectTo(w http.ResponseWriter, req *http.Request, path string) {
|
|
||||||
u := *req.URL
|
|
||||||
u.Scheme = "http"
|
|
||||||
u.Host = req.Host
|
|
||||||
u.Path = path
|
|
||||||
w.Header().Set("Location", u.String())
|
|
||||||
w.WriteHeader(302)
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkAuth(pr *PullRequest) error {
|
|
||||||
if !pr.User.IsTrusted() {
|
|
||||||
return fmt.Errorf("%q is not a trusted pull requester", pr.User.Login)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *server) serveRSTDiff(w http.ResponseWriter, req *http.Request) {
|
|
||||||
prNumber, err := extractPRNumber(req.URL.Path, "/diff/rst")
|
|
||||||
if err != nil {
|
|
||||||
writeError(w, 400, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
pr, err := lookupPullRequest(prNumber)
|
|
||||||
if err != nil {
|
|
||||||
writeError(w, 400, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// We're going to run whatever Python is specified in the pull request, which
|
|
||||||
// may do bad things, so only trust people we trust.
|
|
||||||
if err := checkAuth(pr); err != nil {
|
|
||||||
writeError(w, 403, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
base, err := s.generateAt(pr.Base.SHA)
|
|
||||||
defer os.RemoveAll(base)
|
|
||||||
if err != nil {
|
|
||||||
writeError(w, 500, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
head, err := s.generateAt(pr.Head.SHA)
|
|
||||||
defer os.RemoveAll(head)
|
|
||||||
if err != nil {
|
|
||||||
writeError(w, 500, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
diffCmd := exec.Command("diff", "-r", "-u", path.Join(base, "scripts", "tmp"), path.Join(head, "scripts", "tmp"))
|
|
||||||
var diff bytes.Buffer
|
|
||||||
diffCmd.Stdout = &diff
|
|
||||||
if err := ignoreExitCodeOne(diffCmd.Run()); err != nil {
|
|
||||||
writeError(w, 500, fmt.Errorf("error running diff: %v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Write(diff.Bytes())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *server) serveHTMLDiff(w http.ResponseWriter, req *http.Request) {
|
|
||||||
prNumber, err := extractPRNumber(req.URL.Path, "/diff/html")
|
|
||||||
if err != nil {
|
|
||||||
writeError(w, 400, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
pr, err := lookupPullRequest(prNumber)
|
|
||||||
if err != nil {
|
|
||||||
writeError(w, 400, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// We're going to run whatever Python is specified in the pull request, which
|
|
||||||
// may do bad things, so only trust people we trust.
|
|
||||||
if err := checkAuth(pr); err != nil {
|
|
||||||
writeError(w, 403, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
base, err := s.generateAt(pr.Base.SHA)
|
|
||||||
defer os.RemoveAll(base)
|
|
||||||
if err != nil {
|
|
||||||
writeError(w, 500, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
head, err := s.generateAt(pr.Head.SHA)
|
|
||||||
defer os.RemoveAll(head)
|
|
||||||
if err != nil {
|
|
||||||
writeError(w, 500, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
htmlDiffer, err := findHTMLDiffer()
|
|
||||||
if err != nil {
|
|
||||||
writeError(w, 500, fmt.Errorf("could not find HTML differ"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
requestedPath, redirect := extractPath(req.URL.Path, "/diff/spec/")
|
|
||||||
if redirect != "" {
|
|
||||||
s.redirectTo(w, req, redirect)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
cmd := exec.Command(htmlDiffer, path.Join(base, "scripts", "gen", requestedPath), path.Join(head, "scripts", "gen", requestedPath))
|
|
||||||
var stdout bytes.Buffer
|
|
||||||
var stderr bytes.Buffer
|
|
||||||
cmd.Stdout = &stdout
|
|
||||||
cmd.Stderr = &stderr
|
|
||||||
if err := cmd.Run(); err != nil {
|
|
||||||
writeError(w, 500, fmt.Errorf("error running HTML differ: %v\nOutput:\n%v", err, stderr.String()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Write(stdout.Bytes())
|
|
||||||
}
|
|
||||||
|
|
||||||
func findHTMLDiffer() (string, error) {
|
|
||||||
wd, err := os.Getwd()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
differ := path.Join(wd, "htmldiff.pl")
|
|
||||||
if _, err := os.Stat(differ); err == nil {
|
|
||||||
return differ, nil
|
|
||||||
}
|
|
||||||
return "", fmt.Errorf("unable to find htmldiff.pl")
|
|
||||||
}
|
|
||||||
|
|
||||||
func getPulls() ([]PullRequest, error) {
|
|
||||||
resp, err := http.Get(fmt.Sprintf("%s%s", pullsPrefix, accessTokenQuerystring()))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
if resp.StatusCode != 200 {
|
|
||||||
body, _ := ioutil.ReadAll(resp.Body)
|
|
||||||
return nil, fmt.Errorf("error getting pull requests: %v", string(body))
|
|
||||||
}
|
|
||||||
dec := json.NewDecoder(resp.Body)
|
|
||||||
var pulls []PullRequest
|
|
||||||
err = dec.Decode(&pulls)
|
|
||||||
return pulls, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// getBranches returns a list of the upstream branch names.
|
|
||||||
// It attempts to `git fetch` before doing so.
|
|
||||||
func (s *server) getBranches() ([]string, error) {
|
|
||||||
err := s.updateBase()
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Error fetching: %v, will use cached branches")
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := exec.Command("git", "branch", "-r")
|
|
||||||
cmd.Dir = path.Join(s.matrixDocCloneURL)
|
|
||||||
var b bytes.Buffer
|
|
||||||
cmd.Stdout = &b
|
|
||||||
s.mu.Lock()
|
|
||||||
err = cmd.Run()
|
|
||||||
s.mu.Unlock()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("Error reading branch names: %v. Output from git:\n%v", err, b.String())
|
|
||||||
}
|
|
||||||
branches := []string{}
|
|
||||||
for _, b := range strings.Split(b.String(), "\n") {
|
|
||||||
b = strings.TrimSpace(b)
|
|
||||||
if strings.HasPrefix(b, "origin/") {
|
|
||||||
branches = append(branches, b[7:])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return branches, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (srv *server) makeIndex(w http.ResponseWriter, req *http.Request) {
|
|
||||||
pulls, err := getPulls()
|
|
||||||
if err != nil {
|
|
||||||
writeError(w, 500, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
branches, err := srv.getBranches()
|
|
||||||
if err != nil {
|
|
||||||
writeError(w, 500, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// write our stuff into a buffer so that we can change our minds
|
|
||||||
// and write a 500 if it all goes wrong.
|
|
||||||
var b bytes.Buffer
|
|
||||||
b.Write([]byte(`
|
|
||||||
<head>
|
|
||||||
<script>
|
|
||||||
function redirectToApiDocs(relativePath) {
|
|
||||||
var url = new URL(window.location);
|
|
||||||
url.pathname += relativePath;
|
|
||||||
var newLoc = "http://matrix.org/docs/api/client-server/?url=" + encodeURIComponent(url);
|
|
||||||
window.location = newLoc;
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</head>
|
|
||||||
<body><ul>
|
|
||||||
`))
|
|
||||||
|
|
||||||
tmpl, err := template.New("pr entry").Parse(`
|
|
||||||
<li>{{.Number}}:
|
|
||||||
<a href="{{.User.HTMLURL}}">{{.User.Login}}</a>:
|
|
||||||
<a href="{{.HTMLURL}}">{{.Title}}</a>:
|
|
||||||
<a href="spec/{{.Number}}/">spec</a>
|
|
||||||
<a href="#" onclick="redirectToApiDocs('spec/{{.Number}}/api-docs.json')">api docs</a>
|
|
||||||
<a href="diff/html/{{.Number}}/">spec diff</a>
|
|
||||||
<a href="diff/rst/{{.Number}}/">rst diff</a>
|
|
||||||
</li>
|
|
||||||
`)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, pull := range pulls {
|
|
||||||
err = tmpl.Execute(&b, pull)
|
|
||||||
if err != nil {
|
|
||||||
writeError(w, 500, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
b.Write([]byte(`
|
|
||||||
</ul>
|
|
||||||
<div>View the spec at:<ul>
|
|
||||||
`))
|
|
||||||
branchNames := []string{}
|
|
||||||
for _, branch := range branches {
|
|
||||||
if strings.HasPrefix(branch, "drafts/") {
|
|
||||||
branchNames = append(branchNames, branch)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
branchNames = append(branchNames, "HEAD")
|
|
||||||
for _, branch := range branchNames {
|
|
||||||
href := "spec/" + url.QueryEscape(branch) + "/"
|
|
||||||
fmt.Fprintf(&b, "<li><a href=\"%s\">%s</a></li>\n", href, branch)
|
|
||||||
if *includesDir != "" {
|
|
||||||
fmt.Fprintf(&b, "<li><a href=\"%s?matrixdotorgstyle=1\">%s, styled like matrix.org</a></li>\n",
|
|
||||||
href, branch)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
b.Write([]byte("</ul></div>\n\n"))
|
|
||||||
|
|
||||||
b.Write([]byte("<div>View the API docs at:<ul>"))
|
|
||||||
for _, branch := range branchNames {
|
|
||||||
fmt.Fprintf(&b,
|
|
||||||
"<li><a href=\"#\" onclick=\"redirectToApiDocs('spec/%s/api-docs.json')\">%s</a></li>\n",
|
|
||||||
url.QueryEscape(branch), branch)
|
|
||||||
}
|
|
||||||
b.Write([]byte("</ul></div>"))
|
|
||||||
|
|
||||||
b.Write([]byte("</body>"))
|
|
||||||
b.WriteTo(w)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ignoreExitCodeOne(err error) error {
|
|
||||||
if err == nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if exiterr, ok := err.(*exec.ExitError); ok {
|
|
||||||
if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
|
|
||||||
if status.ExitStatus() == 1 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
flag.Parse()
|
|
||||||
// It would be great to read this from github
|
|
||||||
// cf https://github.com/matrix-org/matrix-doc/issues/1384
|
|
||||||
allowedMembers = map[string]bool{
|
|
||||||
"dbkr": true,
|
|
||||||
"erikjohnston": true,
|
|
||||||
"illicitonion": true,
|
|
||||||
"Kegsay": true,
|
|
||||||
"NegativeMjark": true,
|
|
||||||
"richvdh": true,
|
|
||||||
"ara4n": true,
|
|
||||||
"leonerd": true,
|
|
||||||
"rxl881": true,
|
|
||||||
"uhoreg": true,
|
|
||||||
"turt2live": true,
|
|
||||||
"Half-Shot": true,
|
|
||||||
"anoadragon453": true,
|
|
||||||
"mujx": true,
|
|
||||||
"benparsons": true,
|
|
||||||
"KitsuneRal": true,
|
|
||||||
}
|
|
||||||
if err := initCache(); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
rand.Seed(time.Now().Unix())
|
|
||||||
masterCloneDir, err := makeTempDir()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
log.Printf("Creating master clone dir %s\n", masterCloneDir)
|
|
||||||
if err = gitClone(matrixDocCloneURL, masterCloneDir, false); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
s := server{matrixDocCloneURL: masterCloneDir}
|
|
||||||
http.HandleFunc("/spec/", forceHTML(s.serveSpec))
|
|
||||||
http.HandleFunc("/diff/rst/", s.serveRSTDiff)
|
|
||||||
http.HandleFunc("/diff/html/", forceHTML(s.serveHTMLDiff))
|
|
||||||
http.HandleFunc("/healthz", serveText("ok"))
|
|
||||||
http.HandleFunc("/", forceHTML(s.makeIndex))
|
|
||||||
|
|
||||||
fmt.Printf("Listening on port %d\n", *port)
|
|
||||||
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", *port), nil))
|
|
||||||
}
|
|
||||||
|
|
||||||
func forceHTML(h func(w http.ResponseWriter, req *http.Request)) func(w http.ResponseWriter, req *http.Request) {
|
|
||||||
return func(w http.ResponseWriter, req *http.Request) {
|
|
||||||
w.Header().Set("Content-Type", "text/html")
|
|
||||||
h(w, req)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func serveText(s string) func(http.ResponseWriter, *http.Request) {
|
|
||||||
return func(w http.ResponseWriter, req *http.Request) {
|
|
||||||
io.WriteString(w, s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func initCache() error {
|
|
||||||
c1, err := lru.New(50) // Evict after 50 entries (i.e. 50 sha1s)
|
|
||||||
specCache = c1
|
|
||||||
|
|
||||||
c2, err := lru.New(50) // Evict after 50 entries (i.e. 50 sha1s)
|
|
||||||
styledSpecCache = c2
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeTempDir() (string, error) {
|
|
||||||
directory := path.Join("/tmp/matrix-doc", strconv.FormatInt(rand.Int63(), 10))
|
|
||||||
if err := os.MkdirAll(directory, permissionsOwnerFull); err != nil {
|
|
||||||
return "", fmt.Errorf("error making directory %s: %v", directory, err)
|
|
||||||
}
|
|
||||||
return directory, nil
|
|
||||||
}
|
|
Loading…
Reference in New Issue