With this patch it is possible to launch git-instaweb by using Python http.server CGI handler via `-d python` option. git-instaweb generates a small wrapper around the http.server (in GIT_DIR/gitweb/) that address a limitation of the CGI handler where CGI scripts have to be in a cgi-bin subdirectory and directory index can't be easily changed. To keep the implementation small, gitweb is running on url `/cgi-bin/gitweb.cgi` and an automatic redirection is done when opening `/`. The generated wrapper is compatible with both Python 2 and 3. Python is by default installed on most modern Linux distributions which enables running `git instaweb -d python` without needing anything else. Signed-off-by: Arti Zirk <arti.zirk@xxxxxxxxx> --- Changes v1..v2: - Add compatibily for Python 2, tested with 2.7, 3.4, 3.5, 3.6, 3.7 Notes: Base Ref: master Web-Diff: https://github.com/artizirk/git/commit/fe30635765 Checkout: git fetch https://github.com/artizirk/git instaweb-python-v2 && git checkout fe30635765 ### Interdiff (v1..v2): diff --git a/git-instaweb.sh b/git-instaweb.sh index c8f9f03447..7c55229773 100755 --- a/git-instaweb.sh +++ b/git-instaweb.sh @@ -614,10 +614,11 @@ python_conf() { ln -sf "$root/gitweb.cgi" "$fqgitdir/gitweb/$httpd_only/cgi-bin/gitweb.cgi" ln -sf "$root/static" "$fqgitdir/gitweb/$httpd_only/" - # generate a standalone 'python3 http.server' script in $fqgitdir/gitweb - # This asumes that python3 is in user's $PATH + # generate a standalone 'python http.server' script in $fqgitdir/gitweb + # This asumes that python is in user's $PATH + # This script is Python 2 and 3 compatible cat > "$fqgitdir/gitweb/gitweb.py" <<EOF -#!/usr/bin/env python3 +#!/usr/bin/env python import os import sys @@ -639,7 +640,14 @@ os.dup2(errorlogfile.fileno(), _orig_stderr_fd) sys.stderr = errorlogfile from functools import partial -from http.server import CGIHTTPRequestHandler, test + +if sys.version_info < (3, 0): # Python 2 + from CGIHTTPServer import CGIHTTPRequestHandler + from BaseHTTPServer import HTTPServer as ServerClass +else: # Python 3 + from http.server import CGIHTTPRequestHandler + from http.server import HTTPServer as ServerClass + # Those environment variables will be passed to the cgi script os.environ.update({ @@ -660,7 +668,7 @@ class GitWebRequestHandler(CGIHTTPRequestHandler): def do_HEAD(self): self.redirect_path() - super().do_HEAD() + CGIHTTPRequestHandler.do_HEAD(self) def do_GET(self): if self.path == "/": @@ -669,11 +677,11 @@ class GitWebRequestHandler(CGIHTTPRequestHandler): self.end_headers() return self.redirect_path() - super().do_GET() + CGIHTTPRequestHandler.do_GET(self) def do_POST(self): self.redirect_path() - super().do_POST() + CGIHTTPRequestHandler.do_POST(self) # rewrite path of every request that is not gitweb.cgi to out of cgi-bin def redirect_path(self): @@ -685,7 +693,7 @@ class GitWebRequestHandler(CGIHTTPRequestHandler): def is_cgi(self): result = False if self.path.startswith('/cgi-bin/gitweb.cgi'): - result = super().is_cgi() + result = CGIHTTPRequestHandler.is_cgi(self) return result @@ -698,7 +706,12 @@ if "$local" == "true": # as this was added to SimpleHTTPRequestHandler in Python 3.7 os.chdir("$fqgitdir/gitweb/$httpd_only/") -test(HandlerClass=GitWebRequestHandler, port=$port, bind=bind) +GitWebRequestHandler.protocol_version = "HTTP/1.0" +httpd = ServerClass((bind, $port), GitWebRequestHandler) + +sa = httpd.socket.getsockname() +print("Serving HTTP on", sa[0], "port", sa[1], "...") +httpd.serve_forever() EOF chmod a+x "$fqgitdir/gitweb/gitweb.py" ### Patches Documentation/git-instaweb.txt | 3 +- git-instaweb.sh | 127 ++++++++++++++++++++++++++++++++- 2 files changed, 128 insertions(+), 2 deletions(-) diff --git a/Documentation/git-instaweb.txt b/Documentation/git-instaweb.txt index e8ecdbf927..a54fe4401b 100644 --- a/Documentation/git-instaweb.txt +++ b/Documentation/git-instaweb.txt @@ -29,7 +29,8 @@ OPTIONS The HTTP daemon command-line that will be executed. Command-line options may be specified here, and the configuration file will be added at the end of the command-line. - Currently apache2, lighttpd, mongoose, plackup and webrick are supported. + Currently apache2, lighttpd, mongoose, plackup, python and + webrick are supported. (Default: lighttpd) -m:: diff --git a/git-instaweb.sh b/git-instaweb.sh index eec264e630..7c55229773 100755 --- a/git-instaweb.sh +++ b/git-instaweb.sh @@ -67,6 +67,13 @@ resolve_full_httpd () { httpd_only="${httpd%% *}" # cut on first space return ;; + *python*) + # server is started by running via generated gitweb.py in + # $fqgitdir/gitweb + full_httpd="$fqgitdir/gitweb/gitweb.py" + httpd_only="${httpd%% *}" # cut on first space + return + ;; esac httpd_only="$(echo $httpd | cut -f1 -d' ')" @@ -110,7 +117,7 @@ start_httpd () { # don't quote $full_httpd, there can be arguments to it (-f) case "$httpd" in - *mongoose*|*plackup*) + *mongoose*|*plackup*|*python*) #These servers don't have a daemon mode so we'll have to fork it $full_httpd "$conf" & #Save the pid before doing anything else (we'll print it later) @@ -595,6 +602,121 @@ EOF rm -f "$conf" } +python_conf() { + # Python's builtin http.server and its CGI support is very limited. + # CGI handler is capable of running CGI script only from inside a directory. + # Trying to set cgi_directories=["/"] will add double slash to SCRIPT_NAME + # and that in turn breaks gitweb's relative link generation. + + # create a simple web root where $fqgitdir/gitweb/$httpd_only is our root + mkdir -p "$fqgitdir/gitweb/$httpd_only/cgi-bin" + # Python http.server follows the symlinks + ln -sf "$root/gitweb.cgi" "$fqgitdir/gitweb/$httpd_only/cgi-bin/gitweb.cgi" + ln -sf "$root/static" "$fqgitdir/gitweb/$httpd_only/" + + # generate a standalone 'python http.server' script in $fqgitdir/gitweb + # This asumes that python is in user's $PATH + # This script is Python 2 and 3 compatible + cat > "$fqgitdir/gitweb/gitweb.py" <<EOF +#!/usr/bin/env python +import os +import sys + +# Open log file in line buffering mode +accesslogfile = open("$fqgitdir/gitweb/access.log", 'a', buffering=1) +errorlogfile = open("$fqgitdir/gitweb/error.log", 'a', buffering=1) + +# and replace our stdout and stderr with log files +# also do a lowlevel duplicate of the logfile file descriptors so that +# our CGI child process writes any stderr warning also to the log file +_orig_stdout_fd = sys.stdout.fileno() +sys.stdout.close() +os.dup2(accesslogfile.fileno(), _orig_stdout_fd) +sys.stdout = accesslogfile + +_orig_stderr_fd = sys.stderr.fileno() +sys.stderr.close() +os.dup2(errorlogfile.fileno(), _orig_stderr_fd) +sys.stderr = errorlogfile + +from functools import partial + +if sys.version_info < (3, 0): # Python 2 + from CGIHTTPServer import CGIHTTPRequestHandler + from BaseHTTPServer import HTTPServer as ServerClass +else: # Python 3 + from http.server import CGIHTTPRequestHandler + from http.server import HTTPServer as ServerClass + + +# Those environment variables will be passed to the cgi script +os.environ.update({ + "GIT_EXEC_PATH": "$GIT_EXEC_PATH", + "GIT_DIR": "$GIT_DIR", + "GITWEB_CONFIG": "$GITWEB_CONFIG" +}) + + +class GitWebRequestHandler(CGIHTTPRequestHandler): + + def log_message(self, format, *args): + # Write access logs to stdout + sys.stdout.write("%s - - [%s] %s\n" % + (self.address_string(), + self.log_date_time_string(), + format%args)) + + def do_HEAD(self): + self.redirect_path() + CGIHTTPRequestHandler.do_HEAD(self) + + def do_GET(self): + if self.path == "/": + self.send_response(303, "See Other") + self.send_header("Location", "/cgi-bin/gitweb.cgi") + self.end_headers() + return + self.redirect_path() + CGIHTTPRequestHandler.do_GET(self) + + def do_POST(self): + self.redirect_path() + CGIHTTPRequestHandler.do_POST(self) + + # rewrite path of every request that is not gitweb.cgi to out of cgi-bin + def redirect_path(self): + if not self.path.startswith("/cgi-bin/gitweb.cgi"): + self.path = self.path.replace("/cgi-bin/", "/") + + # gitweb.cgi is the only thing that is ever going to be run here. + # Ignore everything else + def is_cgi(self): + result = False + if self.path.startswith('/cgi-bin/gitweb.cgi'): + result = CGIHTTPRequestHandler.is_cgi(self) + return result + + +bind = "127.0.0.1" +if "$local" == "true": + bind = "0.0.0.0" + +# Set our http root directory +# This is a work around for a missing directory argument in older Python versions +# as this was added to SimpleHTTPRequestHandler in Python 3.7 +os.chdir("$fqgitdir/gitweb/$httpd_only/") + +GitWebRequestHandler.protocol_version = "HTTP/1.0" +httpd = ServerClass((bind, $port), GitWebRequestHandler) + +sa = httpd.socket.getsockname() +print("Serving HTTP on", sa[0], "port", sa[1], "...") +httpd.serve_forever() +EOF + + chmod a+x "$fqgitdir/gitweb/gitweb.py" +} + gitweb_conf() { cat > "$fqgitdir/gitweb/gitweb_config.perl" <<EOF #!@@PERL@@ @@ -623,6 +745,9 @@ configure_httpd() { *plackup*) plackup_conf ;; + *python*) + python_conf + ;; *) echo "Unknown httpd specified: $httpd" exit 1 base-commit: 16a465bc018d09e9d7bbbdc5f40a7fb99c21f8ef -- 2.20.1