phpdiff

Convert .patch to html output with styling for online viewing


So I have a tool where users can users upload their SVN .patch files. Rather than downloading and viewing it separately I was hoping if it can be viewed directly online with the red green styling.

The patch file [/uploads/Bug123.patch]

    ### Eclipse Workspace Patch 1.0
    Index: src/main/java/com/admin/Screen.java
    ===================================================================
    --- src/main/java/com/admin/Screen.java (revision 2)
    +++ src/main/java/com/admin/Screen.java (working copy)
    @@ -147,20 +147,22 @@
    -       System.out.println("Hello World"); 
    +       System.out.println("Hello New World"); 

The Viewable [/viewer.php?file=Bug123.patch]

<html>
<style>
    .red {
        background: lightcoral
    }

    .green {
        background: greenyellow
    }

    .yellow {
        background: yellow;
        font-weight: bold
    }
</style>
<pre>
### Eclipse Workspace Patch 1.0
Index: src/main/java/com/admin/Screen.java
===================================================================
<span class="yellow">--- src/main/java/com/admin/Screen.java    (revision 2)</span>
<span class="yellow">+++ src/main/java/com/admin/Screen.java    (working copy)</span>
@@ -147,20 +147,22 @@
<span class="red">-     System.out.println("Hello World"); </span>
<span class="green">+       System.out.println("Hello New World"); </span>
</pre>
</html>

Is there a tool to convert patch files to a corresponding html rendering/report for review directly from the browser?


Solution

  • You could try something along the lines of this:

    <?php
    
    $file = __DIR__ . '/foo.patch';
    
    echo '<doctype html>
    <html>
    <head>
    <style>
    .header {
       color: black;
    }
    .header-comment {
       color: grey;
    }
    .file-input, .file-change {
       color: green;
    }
    .file-output {
       color: blue;
    }
    .file-line-nos {
       color: blue;
    }
    .line-context {
       color: darkgrey;
    }
    .line-add {
       color: green;
    }
    .line-change {
       color: yellow;
    }
    .line-del {
       color: red;
    }
    </style>
    </head>
    <body>
    ';
    
    
    echo PatchParser::file_to_html($file);
    
    echo '</body></html>';
    
    class PatchParser {
       const MODE_HEADER = 1;
       const MODE_FILE = 2;
    
       const LINE_TYPE_HEADER       = 0x0001;
       const LINE_TYPE_COMMENT      = 0x0002;
       const LINE_TYPE_FILE_IN      = 0x1001;
       const LINE_TYPE_FILE_OUT     = 0x1002;
       const LINE_TYPE_FILE_CHANGE  = 0x1003;
       const LINE_TYPE_LINE_NUMBERS = 0x1004;
       const LINE_TYPE_CONTEXT      = 0x2001;
       const LINE_TYPE_ADDITION     = 0x2002;
       const LINE_TYPE_DELETION     = 0x2003;
       const LINE_TYPE_CHANGE       = 0x2004;
    
       const MARKER_IN_FILE               = '+++';
       const MARKER_CHANGED_FILE          = '***'; // Used for context diffs
       const MARKER_OUT_FILE              = '---';
       const MARKER_FILE_LINE_NUMBERS     = '@';
       const MARKER_HEADER_COMMENT        = '#';
       const MARKER_FILE_CHANGE           = '!';
       const MARKER_FILE_NORMAL_ADDITION  = '>';
       const MARKER_FILE_NORMAL_DELETION  = '<';
       const MARKER_FILE_UNIFIED_ADDITION = '+';
       const MARKER_FILE_UNIFIED_DELETION = '-';
    
    
       static public function file_to_html($file){
          return self::text_to_html(file_get_contents($file));
       }
    
       static public function text_to_html($contents){
          $parsed_lines = self::parse($contents);
    
          $classes = array(
             self::LINE_TYPE_HEADER        => 'header header-text',
             self::LINE_TYPE_COMMENT       => 'header header-comment',
             self::LINE_TYPE_FILE_IN       => 'file file-input',
             self::LINE_TYPE_FILE_OUT      => 'file file-output',
             self::LINE_TYPE_FILE_CHANGE   => 'file file-change',
             self::LINE_TYPE_LINE_NUMBERS  => 'file file-line-nos',
             self::LINE_TYPE_CONTEXT       => 'line line-context',
             self::LINE_TYPE_ADDITION      => 'line line-add',
             self::LINE_TYPE_DELETION      => 'line line-del',
             self::LINE_TYPE_CHANGE        => 'line line-change'
          );
    
          $no_lines = count($parsed_lines);
          $lines_width = strlen("$no_lines");
    
          $output = '<pre>';
          foreach ($parsed_lines AS $line_no => $line){
             ++$line_no;
             $output .= str_pad($line_no, $lines_width, " ", STR_PAD_LEFT)
                . ' '
                . '<span class="'
                . $classes[$line[1]]
                . '">'
                . htmlspecialchars($line[0])
                . '</span><br />';
          }
          return $output . '</pre>';
       }
    
       static public function parse($contents) {
          $lines = explode("\n", $contents);
          $output = array();
    
          $mode = self::MODE_HEADER;// Stay's in header mode until we encounter a file handle
          foreach ($lines AS $line) {
             // trim the line to remove excess whitespace, then test the prefix
             $trimmed_line = trim($line);
             $first_three_chars = substr($trimmed_line,0,3);
    
             // The types that can occur in both modes are the file names/types
             if ($first_three_chars === self::MARKER_IN_FILE) {
                $output[] = array($line, self::LINE_TYPE_FILE_IN);
                $mode = self::MODE_FILE;
                continue;
             }
    
             if ($first_three_chars === self::MARKER_CHANGED_FILE) {
                $output[] = array($line, self::LINE_TYPE_FILE_CHANGE);
                $mode = self::MODE_FILE;
                continue;
             }
    
             if (substr($trimmed_line,0,3) === self::MARKER_OUT_FILE) {
                $output[] = array($line, self::LINE_TYPE_FILE_OUT);
                $mode = self::MODE_FILE;
                continue;
             }
    
             // Otherwise our mode is important:
             if ($mode === self::MODE_HEADER){
                // Header mode only supports header text and comments:
                if (substr($trimmed_line, 0, 1) === self::MARKER_HEADER_COMMENT) {
                   $output[] = array($line, self::LINE_TYPE_COMMENT);
                } else {
                   $output[] = array($line, self::LINE_TYPE_HEADER);
                }
                continue;
             } else {
                $first_char = substr($line, 0, 1);
                $type = self::LINE_TYPE_CONTEXT;
                switch ($first_char) {
                   case self::MARKER_FILE_CHANGE:
                      $type = self::LINE_TYPE_CHANGE;
                      break;
                   case self::MARKER_FILE_LINE_NUMBERS:
                      $type = self::LINE_TYPE_LINE_NUMBERS;
                      break;
                   case self::MARKER_FILE_NORMAL_ADDITION:
                   case self::MARKER_FILE_UNIFIED_ADDITION:
                      $type = self::LINE_TYPE_ADDITION;
                      break;
                   case self::MARKER_FILE_NORMAL_DELETION:
                   case self::MARKER_FILE_UNIFIED_DELETION:
                      $type = self::LINE_TYPE_DELETION;
                      break;
                }
                $output[] = array($line, $type);
             }
          }
          return $output;
       }
    }
    

    There's a JS fiddle of the output of what I just posted here.

    Let me know what you think. It's logic is pretty simple.