diff --git a/composer.json b/composer.json index a98a74f4a..9f0db4106 100644 --- a/composer.json +++ b/composer.json @@ -31,7 +31,8 @@ "elkuku/console-progressbar": "1.0", "babdev/library": "dev-master", "codeguy/upload": "1.3.2", - "league/flysystem": "0.4.*@stable" + "league/flysystem": "0.4.*@stable", + "adaptive/php-text-difference": "1.*@stable" }, "require-dev": { "monolog/monolog": "1.*@stable", diff --git a/src/App/Tracker/DiffRenderer/Html/Inline.php b/src/App/Tracker/DiffRenderer/Html/Inline.php new file mode 100644 index 000000000..a699f1dcd --- /dev/null +++ b/src/App/Tracker/DiffRenderer/Html/Inline.php @@ -0,0 +1,236 @@ + + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of the Chris Boulton nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @copyright (c) 2009 Chris Boulton + * @license New BSD License http://www.opensource.org/licenses/bsd-license.php + * @link http://github.com/chrisboulton/php-diff + */ + +namespace App\Tracker\DiffRenderer\Html; + +use Adaptive\Diff\Renderer\Html\ArrayRenderer; + +/** + * Class Inline + * + * @since 1.0 + */ +class Inline extends ArrayRenderer +{ + private $showLineNumbers = true; + + private $showHeader = true; + + /** + * Render a and return diff with changes between the two sequences displayed inline (under each other). + * + * @return string The generated inline diff. + * + * @since 1.0 + */ + public function render() + { + $changes = parent::render(); + $html = ''; + + if (empty($changes)) + { + return $html; + } + + $html .= ''; + + if ($this->showHeader) + { + $html .= ''; + $html .= ''; + + if ($this->showLineNumbers) + { + $html .= ''; + $html .= ''; + } + + $html .= ''; + $html .= ''; + $html .= ''; + } + + foreach ($changes as $i => $blocks) + { + // If this is a separate block, we're condensing code so output ..., + // indicating a significant portion of the code has been collapsed as it is the same + if ($i > 0) + { + $html .= ''; + $html .= ''; + + if ($this->showLineNumbers) + { + $html .= ''; + $html .= ''; + } + + $html .= ''; + } + + foreach ($blocks as $change) + { + $html .= ''; + + // Equal changes should be shown on both sides of the diff + + if ($change['tag'] == 'equal') + { + foreach ($change['base']['lines'] as $no => $line) + { + $html .= ''; + + if ($this->showLineNumbers) + { + $html .= ''; + $html .= ''; + } + + $html .= ''; + $html .= ''; + } + } + // Added lines only on the right side + elseif ($change['tag'] == 'insert') + { + foreach ($change['changed']['lines'] as $no => $line) + { + $html .= ''; + + if ($this->showLineNumbers) + { + $html .= ''; + $html .= ''; + } + + $html .= ''; + $html .= ''; + } + } + // Show deleted lines only on the left side + elseif ($change['tag'] == 'delete') + { + foreach ($change['base']['lines'] as $no => $line) + { + $html .= ''; + + if ($this->showLineNumbers) + { + $html .= ''; + $html .= ''; + } + + $html .= ''; + $html .= ''; + } + } + // Show modified lines on both sides + elseif ($change['tag'] == 'replace') + { + foreach ($change['base']['lines'] as $no => $line) + { + $html .= ''; + + if ($this->showLineNumbers) + { + $html .= ''; + $html .= ''; + } + + $html .= ''; + $html .= ''; + } + + foreach ($change['changed']['lines'] as $no => $line) + { + $html .= ''; + + if ($this->showLineNumbers) + { + $html .= ''; + $html .= ''; + } + + $html .= ''; + $html .= ''; + } + } + + $html .= ''; + } + } + + $html .= '
' . g11n3t('Old') . '' . g11n3t('New') . '' . g11n3t('Differences') . '
 
' . ($change['base']['offset'] + $no + 1) . '' . ($change['changed']['offset'] + $no + 1) . '' . $line . '
 ' . ($change['changed']['offset'] + $no + 1) . '' . $line . ' 
' . ($change['base']['offset'] + $no + 1) . ' ' . $line . ' 
' . ($change['base']['offset'] + $no + 1) . ' ' . $line . '
 ' . ($change['changed']['offset'] + $no + 1) . '' . $line . '
'; + + return $html; + } + + /** + * Set showLineNumbers. + * + * @param boolean $showLineNumbers Show the line numbers. + * + * @return $this + * + * @since 1.0 + */ + public function setShowLineNumbers($showLineNumbers) + { + $this->showLineNumbers = (bool) $showLineNumbers; + + return $this; + } + + /** + * Set showHeader. + * + * @param boolean $showHeader Show the table header. + * + * @return $this + * + * @since 1.0 + */ + public function setShowHeader($showHeader) + { + $this->showHeader = (bool) $showHeader; + + return $this; + } +} diff --git a/src/App/Tracker/Table/IssuesTable.php b/src/App/Tracker/Table/IssuesTable.php index 13328a0b9..b58157ac6 100644 --- a/src/App/Tracker/Table/IssuesTable.php +++ b/src/App/Tracker/Table/IssuesTable.php @@ -288,9 +288,9 @@ private function processChanges() // Expected change ;) break; - case 'description_raw' : - // @todo do something ? - $changes[] = $change; + case 'description' : + + // Do nothing break; diff --git a/src/JTracker/View/Renderer/TrackerExtension.php b/src/JTracker/View/Renderer/TrackerExtension.php index 4e5953350..b936e020e 100644 --- a/src/JTracker/View/Renderer/TrackerExtension.php +++ b/src/JTracker/View/Renderer/TrackerExtension.php @@ -8,6 +8,10 @@ namespace JTracker\View\Renderer; +use Adaptive\Diff\Diff; + +use App\Tracker\DiffRenderer\Html\Inline; + use g11n\g11n; use Joomla\DI\Container; @@ -98,6 +102,7 @@ public function getFunctions() new \Twig_SimpleFunction('issueLink', array($this, 'issueLink')), new \Twig_SimpleFunction('getRelTypes', array($this, 'getRelTypes')), new \Twig_SimpleFunction('getTimezones', array($this, 'getTimezones')), + new \Twig_SimpleFunction('renderDiff', array($this, 'renderDiff')), ); if (!JDEBUG) @@ -529,4 +534,30 @@ public function getMergeStatus($status) throw new \RuntimeException('Unknown status: ' . $status); } + + /** + * Render the differences between two text strings. + * + * @param string $old The "old" text. + * @param string $new The "new" text. + * @param boolean $showLineNumbers To show line numbers. + * @param boolean $showHeader To show the table header. + * + * @return string + * + * @since 1.0 + */ + public function renderDiff($old, $new, $showLineNumbers = true, $showHeader = true) + { + $options = []; + + $diff = new Diff(explode("\n", $old), explode("\n", $new), $options); + + $renderer = new Inline; + + $renderer->setShowLineNumbers($showLineNumbers); + $renderer->setShowHeader($showHeader); + + return $diff->Render($renderer); + } } diff --git a/templates/tracker/issue.index.twig b/templates/tracker/issue.index.twig index 96a6c4be4..04dfc9d7a 100644 --- a/templates/tracker/issue.index.twig +++ b/templates/tracker/issue.index.twig @@ -11,6 +11,7 @@ {{ parent() }} + @@ -290,6 +291,7 @@ + {% set activitiesCnt = 0 %} {% for activity in item.activities %}
@@ -313,22 +315,58 @@ {% if "change" == activity.event %} {% for change in activity.text|json_decode %} - - - {% if "status" == change.name %} - - - - {% else %} - - - - {% endif %} - + {% if change.name == 'description_raw' %} + + + + + {% elseif change.name == 'title' %} + + + + + {% else %} + + + {% if "status" == change.name %} + + + + {% else %} + + + + {% endif %} + + {% endif %} {% endfor %}
{{ change.name|title }} - {{ status(change.old).status|_ }} - - {{ status(change.new).status|_ }} - {{ change.old }}{{ change.new }}
+ {{ 'Description'|_ }} + + {% set activitiesCnt = activitiesCnt + 1 %} + + +
+ {{ renderDiff(change.old, change.new)|raw }} +
+
+ {{ 'Title'|_ }} + + {% set activitiesCnt = activitiesCnt + 1 %} + + +
+ {{ renderDiff(change.old, change.new, false, false)|raw }} +
+
+ {{ change.name|title }} + + {{ status(change.old).status|_ }} + + {{ status(change.new).status|_ }} + {{ change.old }}{{ change.new }}
{% else %} diff --git a/www/jtracker/core/css/diff.css b/www/jtracker/core/css/diff.css new file mode 100644 index 000000000..ef93c6055 --- /dev/null +++ b/www/jtracker/core/css/diff.css @@ -0,0 +1,95 @@ +/* + @copyright Copyright (C) 2012 - 2014 Open Source Matters, Inc. All rights reserved. + @license GNU General Public License version 2 or later; see LICENSE.txt + + This CSS provides code highlighting with GitHub styles + */ + +.Differences { + width: 100%; + border-collapse: collapse; + border-spacing: 0; + empty-cells: show; +} + +.Differences thead th { + text-align: left; + border-bottom: 1px solid #000; + background: #aaa; + color: #000; + padding: 4px; +} +.Differences tbody th { + text-align: right; + background: #ccc; + width: 4em; + padding: 1px 2px; + border-right: 1px solid #000; + vertical-align: top; + font-size: 13px; +} + +.Differences td { + padding: 1px 2px; + font-family: Consolas, monospace; + font-size: 13px; +} + +.DifferencesSideBySide .ChangeInsert td.Left { + background: #dfd; +} + +.DifferencesSideBySide .ChangeInsert td.Right { + background: #cfc; +} + +.DifferencesSideBySide .ChangeDelete td.Left { + background: #f88; +} + +.DifferencesSideBySide .ChangeDelete td.Right { + background: #faa; +} + +.DifferencesSideBySide .ChangeReplace .Left { + background: #fe9; +} + +.DifferencesSideBySide .ChangeReplace .Right { + background: #fd8; +} + +.Differences ins, .Differences del { + text-decoration: none; +} + +.DifferencesSideBySide .ChangeReplace ins, .DifferencesSideBySide .ChangeReplace del { + background: #fc0; +} + +.Differences .Skipped { + background: #f7f7f7; +} + +.DifferencesInline .ChangeReplace .Left, +.DifferencesInline .ChangeDelete .Left { + background: #fdd; +} + +.DifferencesInline .ChangeReplace .Right, +.DifferencesInline .ChangeInsert .Right { + background: #dfd; +} + +.DifferencesInline .ChangeReplace ins { + background: #9e9; +} + +.DifferencesInline .ChangeReplace del { + background: #e99; +} + +pre { + width: 100%; + overflow: auto; +}