diff --git a/framework/yii/gii/ActiveField.php b/framework/yii/gii/ActiveField.php deleted file mode 100644 index 9a03987..0000000 --- a/framework/yii/gii/ActiveField.php +++ /dev/null @@ -1,21 +0,0 @@ - - * @since 2.0 - */ -class ActiveField extends \yii\widgets\ActiveField -{ - public function sticky() - { - $this->options['class'] .= ' sticky'; - return $this; - } -} diff --git a/framework/yii/gii/CodeFile.php b/framework/yii/gii/CodeFile.php index bb42294..d1c6aad 100644 --- a/framework/yii/gii/CodeFile.php +++ b/framework/yii/gii/CodeFile.php @@ -9,6 +9,8 @@ namespace yii\gii; use Yii; use yii\base\Object; +use yii\gii\components\TextDiff; +use yii\helpers\Html; /** * @@ -21,6 +23,7 @@ class CodeFile extends Object const OP_OVERWRITE = 'overwrite'; const OP_SKIP = 'skip'; + public $id; /** * @var string the file path that the new code should be saved to. */ @@ -34,10 +37,6 @@ class CodeFile extends Object * @var string the operation to be performed */ public $operation; - /** - * @var string the error occurred when saving the code into a file - */ - public $error; /** * Constructor. @@ -48,11 +47,9 @@ class CodeFile extends Object { $this->path = strtr($path, array('/' => DIRECTORY_SEPARATOR, '\\' => DIRECTORY_SEPARATOR)); $this->content = $content; + $this->id = md5($this->path); if (is_file($path)) { $this->operation = file_get_contents($path) === $content ? self::OP_SKIP : self::OP_OVERWRITE; - } elseif ($content === null) // is dir - { - $this->operation = is_dir($path) ? self::OP_SKIP : self::OP_NEW; } else { $this->operation = self::OP_NEW; } @@ -60,24 +57,11 @@ class CodeFile extends Object /** * Saves the code into the file {@link path}. + * @return string|boolean */ public function save() { $module = Yii::$app->controller->module; - if ($this->content === null) // a directory - { - if (!is_dir($this->path)) { - $oldmask = @umask(0); - $result = @mkdir($this->path, $module->newDirMode, true); - @umask($oldmask); - if (!$result) { - $this->error = "Unable to create the directory '{$this->path}'."; - return false; - } - } - return true; - } - if ($this->operation === self::OP_NEW) { $dir = dirname($this->path); if (!is_dir($dir)) { @@ -85,14 +69,12 @@ class CodeFile extends Object $result = @mkdir($dir, $module->newDirMode, true); @umask($oldmask); if (!$result) { - $this->error = "Unable to create the directory '$dir'."; - return false; + return "Unable to create the directory '$dir'."; } } } if (@file_put_contents($this->path, $this->content) === false) { - $this->error = "Unable to write the file '{$this->path}'."; - return false; + return "Unable to write the file '{$this->path}'."; } else { $oldmask = @umask(0); @chmod($this->path, $module->newFileMode); @@ -124,4 +106,33 @@ class CodeFile extends Object return 'unknown'; } } + + public function preview() + { + if (($pos = strrpos($this->path, '.')) !== false) { + $type = substr($this->path, $pos + 1); + } else { + $type = 'unknown'; + } + + if ($type === 'php') { + return '
' . highlight_string($this->content, true) . '
'; + } elseif(in_array($type, array('txt','js','css'))) { + return '
' . nl2br(Html::encode($this->content)) . '
'; + } else { + return '
Preview is not available for this file type.
'; + } + } + + public function diff() + { + $type = $this->getType(); + if (!in_array($type, array('php', 'txt','js','css'))) { + return false; + } elseif ($this->operation === self::OP_OVERWRITE) { + return TextDiff::compare(file_get_contents($this->path), $this->content); + } else { + return ''; + } + } } diff --git a/framework/yii/gii/Generator.php b/framework/yii/gii/Generator.php index 40588d9..21a42a2 100644 --- a/framework/yii/gii/Generator.php +++ b/framework/yii/gii/Generator.php @@ -13,6 +13,7 @@ use yii\base\InvalidConfigException; use yii\base\Model; use yii\base\View; + /** * @author Qiang Xue * @since 2.0 @@ -60,6 +61,26 @@ abstract class Generator extends Model return array(); } + public function stickyAttributes() + { + return array('template'); + } + + public function hints() + { + return array(); + } + + /** + * Returns the message to be displayed when the newly generated code is saved successfully. + * Child classes should override this method if the message needs to be customized. + * @return string the message to be displayed when the newly generated code is saved successfully. + */ + public function successMessage() + { + return 'The code has been generated successfully.'; + } + public function formView() { $class = new ReflectionClass($this); @@ -91,7 +112,7 @@ abstract class Generator extends Model { return array( array('template', 'required', 'message' => 'A code template must be selected.'), - array('template', 'validateTemplate', 'skipOnError' => true), + array('template', 'validateTemplate'), ); } @@ -106,74 +127,96 @@ abstract class Generator extends Model } /** - * Saves the generated code into files. + * Loads sticky attributes from a file and populates them into the model. */ - public function save($files, $answers = array()) + public function loadStickyAttributes() { - $result = true; - foreach ($files as $file) { - if ($this->confirmed($file)) { - $result = $file->save() && $result; + $stickyAttributes = $this->stickyAttributes(); + $attributes[] = 'template'; + $path = $this->getStickyDataFile(); + if (is_file($path)) { + $result = @include($path); + if (is_array($result)) { + foreach ($stickyAttributes as $name) { + if (isset($result[$name])) { + $this->$name = $result[$name]; + } + } } } - return $result; } /** - * @return string the directory that contains the template files. - * @throws InvalidConfigException if {@link templates} is empty or template selection is invalid + * Saves sticky attributes into a file. */ - public function getTemplatePath() + public function saveStickyAttributes() { - if (isset($this->templates[$this->template])) { - return $this->templates[$this->template]; - } else { - throw new InvalidConfigException("Unknown template: {$this->template}"); + $stickyAttributes = $this->stickyAttributes(); + $stickyAttributes[] = 'template'; + $values = array(); + foreach ($stickyAttributes as $name) { + $values[$name] = $this->$name; } + $path = $this->getStickyDataFile(); + @mkdir(dirname($path), 0755, true); + file_put_contents($path, "answers === null && $file->operation === CodeFile::OP_NEW - || is_array($this->answers) && isset($this->answers[md5($file->path)]); + return Yii::$app->getRuntimePath() . '/gii-' . Yii::getVersion() . '/' . str_replace('\\', '-',get_class($this)) . '.php'; } /** - * Generates the code using the specified code template file. - * This method is manly used in {@link generate} to generate code. - * @param string $templateFile the code template file path - * @param array $_params_ a set of parameters to be extracted and made available in the code template - * @return string the generated code + * Saves the generated code into files. + * @param CodeFile[] $files + * @param array $answers + * @param boolean $hasError + * @return string */ - public function render($templateFile, $params = array()) + public function save($files, $answers, &$hasError) { - $view = new View; - return $view->renderFile($templateFile, $params, $this); + $lines = array('Generating code using template "' . $this->templatePath . '"...'); + foreach ($files as $file) {; + $relativePath = $file->getRelativePath(); + if (isset($answers[$file->id]) && $file->operation !== CodeFile::OP_SKIP) { + $error = $file->save(); + if (is_string($error)) { + $lines[] = "generating $relativePath
$error
"; + } elseif ($file->operation === CodeFile::OP_NEW) { + $lines[] = " generated $relativePath"; + } else { + $lines[] = " overwrote $relativePath"; + } + } else { + $lines[] = " skipped $relativePath"; + } + } + $lines[] = "done!\n"; + return implode("\n", $lines); } /** - * @return string the code generation result log. + * @return string the directory that contains the template files. + * @throws InvalidConfigException if {@link templates} is empty or template selection is invalid */ - public function renderResults() + public function getTemplatePath() { - $output = 'Generating code using template "' . $this->templatePath . "\"...\n"; - foreach ($this->files as $file) { - if ($file->error !== null) { - $output .= "generating {$file->relativePath}
{$file->error}
\n"; - } elseif ($file->operation === CodeFile::OP_NEW && $this->confirmed($file)) { - $output .= ' generated ' . $file->relativePath . "\n"; - } elseif ($file->operation === CodeFile::OP_OVERWRITE && $this->confirmed($file)) { - $output .= ' overwrote ' . $file->relativePath . "\n"; - } else { - $output .= ' skipped ' . $file->relativePath . "\n"; - } + if (isset($this->templates[$this->template])) { + return $this->templates[$this->template]; + } else { + throw new InvalidConfigException("Unknown template: {$this->template}"); } - $output .= "done!\n"; - return $output; + } + + public function generateCode($template, $params = array()) + { + $view = new View; + $params['generator'] = $this; + return $view->renderFile($template, $params, $this); } /** @@ -211,6 +254,7 @@ abstract class Generator extends Model '__line__', '__method__', '__namespace__', + '__trait__', 'abstract', 'and', 'array', @@ -242,7 +286,7 @@ abstract class Generator extends Model 'exit', 'extends', 'final', - 'final', + 'finally', 'for', 'foreach', 'function', @@ -273,6 +317,7 @@ abstract class Generator extends Model 'switch', 'this', 'throw', + 'trait', 'try', 'unset', 'use', diff --git a/framework/yii/gii/Module.php b/framework/yii/gii/Module.php index a0f4a05..b18cfb0 100644 --- a/framework/yii/gii/Module.php +++ b/framework/yii/gii/Module.php @@ -43,7 +43,6 @@ class Module extends \yii\base\Module * Defaults to 0777, meaning the directory can be read, written and executed by all users. */ public $newDirMode = 0777; - public $enabled = true; /** diff --git a/framework/yii/gii/assets/gii.js b/framework/yii/gii/assets/gii.js index 58c50cb..d91be6d 100644 --- a/framework/yii/gii/assets/gii.js +++ b/framework/yii/gii/assets/gii.js @@ -1,24 +1,85 @@ yii.gii = (function ($) { - return { - init: function () { - $('.hint-block').each(function() { - var $hint = $(this); - $hint.parent().find('input,select,textarea').popover({ - html: true, - trigger: 'focus', - placement: 'right', - content: $hint.html() - }); + var isActive = $('.default-view').length > 0; + + var initHintBlocks = function () { + $('.hint-block').each(function () { + var $hint = $(this); + $hint.parent().find('input,select,textarea').popover({ + html: true, + trigger: 'focus', + placement: 'right', + content: $hint.html() }); + }); + }; - var $checkAll = $('#check-all'); - $checkAll.click(function() { - $('.code-files .check input').prop('checked', this.checked); + var initStickyInputs = function () { + $('.sticky:not(.error) input,select,textarea').each(function () { + var value; + if (this.tagName == 'SELECT') { + value = this.options[this.selectedIndex].text; + } else if (this.tagName == 'TEXTAREA') { + value = $(this).html(); + } else { + value = $(this).val(); + } + if (value == '') { + value = '[empty]'; + } + $(this).before('
' + value + '
').hide(); + }); + $('.sticky-value').on('click', function() { + $(this).hide(); + $(this).next().show().get(0).focus(); + }); + }; + + var initPreviewDiffLinks = function () { + $('.preview-code,.diff-code').on('click', function () { + var $modal = $('#preview-modal'); + var $link = $(this); + $modal.find('.modal-title').text($link.data('title')); + $modal.find('.modal-body').html('Loading ...'); + $modal.modal('show'); + $.ajax({ + type: 'POST', + cache: false, + url: $link.prop('href'), + data: $('.default-view form').serializeArray(), + success: function (data) { + $modal.find('.modal-body').html(data); + $modal.find('.content').css('max-height', ($(window).height() - 200) + 'px'); + }, + error: function (XMLHttpRequest, textStatus, errorThrown) { + $modal.find('.modal-body').html('
' + XMLHttpRequest.responseText + '
') + } }); - $('.code-files .check input').click(function() { - $checkAll.prop('checked', !$('.code-files .check input:not(:checked)').length); + return false; + }); + }; + + var initConfirmationCheckboxes = function () { + var $checkAll = $('#check-all'); + $checkAll.click(function () { + $('.default-view-files table .check input').prop('checked', this.checked); + }); + $('.default-view-files table .check input').click(function () { + $checkAll.prop('checked', !$('.default-view-files table .check input:not(:checked)').length); + }); + $checkAll.prop('checked', !$('.default-view-files table .check input:not(:checked)').length); + }; + + return { + init: function () { + initHintBlocks(); + initStickyInputs(); + initPreviewDiffLinks(); + initConfirmationCheckboxes(); + + $('.default-view .form-group input,select,textarea').change(function(){ + $('.default-view-results,.default-view-files').hide(); + $('.default-view button[name="generate"]').hide(); }); - $checkAll.prop('checked', !$('.code-files .check input:not(:checked)').length); } }; })(jQuery); diff --git a/framework/yii/gii/assets/main.css b/framework/yii/gii/assets/main.css index f1cecc5..12d2b86 100644 --- a/framework/yii/gii/assets/main.css +++ b/framework/yii/gii/assets/main.css @@ -42,15 +42,74 @@ body { display: none; } -table.code-files .file { +.default-view .sticky-value { + padding: 6px 12px; + background: lightyellow; +} + +.default-view .modal-dialog { + width: 800px; +} + +.default-view .modal-dialog .error { + +} +.default-view .modal-dialog .error { + color: #d9534f; } -table.code-files .action { +.default-view .modal-dialog .content { + background: #fafafa; + border-left: #eee 5px solid; + padding: 5px 10px; + overflow: auto; +} + +.default-view .modal-dialog code { + background: transparent; +} + +.default-view-files table .file { + +} + +.default-view-files table .action { width: 100px; } -table.code-files .check { +.default-view-files table .check { width: 25px; text-align: center; } + +.default-view-results pre { + overflow: auto; + background-color: #333; + max-height: 300px; + color: white; + padding: 10px; + border-radius: 0; +} + +.default-view-results pre span.error { + background: #FFE0E1; + color: black; + padding: 1px; +} + +.default-diff pre { + padding: 0; + margin: 0; + background: transparent; + border: none; +} + +.default-diff pre del { + background: pink; +} + +.default-diff pre ins { + background: lightgreen; + text-decoration: none; +} diff --git a/framework/yii/gii/components/ActiveField.php b/framework/yii/gii/components/ActiveField.php new file mode 100644 index 0000000..6a67217 --- /dev/null +++ b/framework/yii/gii/components/ActiveField.php @@ -0,0 +1,40 @@ + + * @since 2.0 + */ +class ActiveField extends \yii\widgets\ActiveField +{ + /** + * @var Generator + */ + public $model; + + public function init() + { + $stickyAttributes = $this->model->stickyAttributes(); + if (in_array($this->attribute, $stickyAttributes)) { + $this->sticky(); + } + $hints = $this->model->hints(); + if (isset($hints[$this->attribute])) { + $this->hint($hints[$this->attribute]); + } + } + + public function sticky() + { + $this->options['class'] .= ' sticky'; + return $this; + } +} diff --git a/framework/yii/gii/components/TextDiff.php b/framework/yii/gii/components/TextDiff.php new file mode 100644 index 0000000..d5c4cef --- /dev/null +++ b/framework/yii/gii/components/TextDiff.php @@ -0,0 +1,31 @@ + + * @since 2.0 + */ +class TextDiff +{ + public static function compare($lines1, $lines2) + { + Yii::setAlias('@Horde', '@yii/gii/lib/Horde'); + if (is_string($lines1)) { + $lines1 = explode("\n", $lines1); + } + if (is_string($lines2)) { + $lines2 = explode("\n", $lines2); + } + $diff = new \Horde_Text_Diff('auto', array($lines1, $lines2)); + $renderer = new \Horde_Text_Diff_Renderer_Inline(); + return $renderer->render($diff); + } +} diff --git a/framework/yii/gii/controllers/DefaultController.php b/framework/yii/gii/controllers/DefaultController.php index 619df2c..df116bd 100644 --- a/framework/yii/gii/controllers/DefaultController.php +++ b/framework/yii/gii/controllers/DefaultController.php @@ -38,9 +38,11 @@ class DefaultController extends Controller $params = array('generator' => $generator); if (isset($_POST['preview']) || isset($_POST['generate'])) { if ($generator->validate()) { + $generator->saveStickyAttributes(); $files = $generator->prepare(); - if (isset($_POST['generate'], $_POST['answers'])) { - $params['result'] = $generator->save($files, $_POST['answers']); + if (isset($_POST['generate']) && !empty($_POST['answers'])) { + $params['results'] = $generator->save($files, (array)$_POST['answers'], $hasError); + $params['hasError'] = $hasError; } else { $params['files'] = $files; $params['answers'] = isset($_POST['answers']) ? $_POST['answers'] : null; @@ -51,14 +53,45 @@ class DefaultController extends Controller return $this->render('view', $params); } - public function actionCode($file) + public function actionPreview($id, $file) { - + $generator = $this->loadGenerator($id); + if ($generator->validate()) { + foreach ($generator->prepare() as $f) { + if ($f->id === $file) { + return $f->preview(); + } + } + } + throw new HttpException(404, "Code file not found: $file"); } - public function actionDiff($file1, $file2) + public function actionDiff($id, $file) { + $generator = $this->loadGenerator($id); + if ($generator->validate()) { + foreach ($generator->prepare() as $f) { + if ($f->id === $file) { + return $this->renderPartial('diff', array( + 'diff' => $f->diff(), + )); + } + } + } + throw new HttpException(404, "Code file not found: $file"); + } + public function createUrl($route, $params = array()) + { + if (!isset($params['id']) && $this->generator !== null) { + foreach ($this->module->generators as $id => $generator) { + if ($generator === $this->generator) { + $params['id'] = $id; + break; + } + } + } + return parent::createUrl($route, $params); } /** @@ -71,6 +104,7 @@ class DefaultController extends Controller { if (isset($this->module->generators[$id])) { $this->generator = $this->module->generators[$id]; + $this->generator->loadStickyAttributes(); $this->generator->load($_POST); return $this->generator; } else { diff --git a/framework/yii/gii/generators/controller/Generator.php b/framework/yii/gii/generators/controller/Generator.php index 56b8f6d..b3654ac 100644 --- a/framework/yii/gii/generators/controller/Generator.php +++ b/framework/yii/gii/generators/controller/Generator.php @@ -65,9 +65,35 @@ class Generator extends \yii\gii\Generator ); } + public function stickyAttributes() + { + return array('ns', 'baseClass'); + } + + public function hints() + { + return array( + 'controller' => 'Controller ID should be in lower case and may contain module ID(s). For example: + ', + 'actions' => 'Provide one or multiple action IDs to generate empty action method(s) in the controller. Separate multiple action IDs with commas or spaces.', + 'ns' => 'This is the namespace that the new controller class will should use.', + 'baseClass' => 'This is the class that the new controller class will extend from. Please make sure the class exists and can be autoloaded.', + ); + } + public function successMessage() { - $link = Html::a('try it now', Yii::$app->getUrlManager()->createUrl($this->controller), array('target' => '_blank')); + $actions = $this->getActionIDs(); + if (in_array('index', $actions)) { + $route = $this->controller . '/index'; + } else { + $route = $this->controller . '/' . reset($actions); + } + $link = Html::a('try it now', Yii::$app->getUrlManager()->createUrl($route), array('target' => '_blank')); return "The controller has been generated successfully. You may $link."; } @@ -79,13 +105,13 @@ class Generator extends \yii\gii\Generator $files[] = new CodeFile( $this->getControllerFile(), - $this->render($templatePath . '/controller.php') + $this->generateCode($templatePath . '/controller.php') ); foreach ($this->getActionIDs() as $action) { $files[] = new CodeFile( $this->getViewFile($action), - $this->render($templatePath . '/view.php', array('action' => $action)) + $this->generateCode($templatePath . '/view.php', array('action' => $action)) ); } @@ -134,17 +160,6 @@ class Generator extends \yii\gii\Generator return $id; } - public function getUniqueControllerID() - { - $id = $this->controller; - if (($pos = strrpos($id, '/')) !== false) { - $id[$pos + 1] = strtolower($id[$pos + 1]); - } else { - $id[0] = strtolower($id[0]); - } - return $id; - } - public function getControllerFile() { $module = $this->getModule(); diff --git a/framework/yii/gii/generators/controller/templates/controller.php b/framework/yii/gii/generators/controller/templates/controller.php index 5f4343d..1f08221 100644 --- a/framework/yii/gii/generators/controller/templates/controller.php +++ b/framework/yii/gii/generators/controller/templates/controller.php @@ -1,8 +1,24 @@ + + +ns)): ?> +namespace ns; ?>; + + +class controllerClass; ?> extends baseClass, '\\') . "\n"; ?> +{ +getActionIDs() as $action): ?> + public function action() + { + return $this->render(''); + } + + +} diff --git a/framework/yii/gii/generators/controller/templates/view.php b/framework/yii/gii/generators/controller/templates/view.php index ff2762a..99e616b 100644 --- a/framework/yii/gii/generators/controller/templates/view.php +++ b/framework/yii/gii/generators/controller/templates/view.php @@ -1,8 +1,21 @@ + +/** + * @var yii\base\View $this + */ +"; ?> + +

+ +

+ You may change the content of this page by modifying + the file echo __FILE__; ?>. +

diff --git a/framework/yii/gii/generators/controller/views/form.php b/framework/yii/gii/generators/controller/views/form.php index 2e48889..845d7e7 100644 --- a/framework/yii/gii/generators/controller/views/form.php +++ b/framework/yii/gii/generators/controller/views/form.php @@ -5,22 +5,7 @@ * @var yii\gii\generators\controller\Generator $generator */ ?> -field($generator, 'controller')->hint(' - Controller ID is case-sensitive and can contain module ID(s). For example: - -'); ?> -field($generator, 'actions')->hint(' - Provide one or multiple action IDs to generate empty action method(s) in the controller. - Separate multiple action IDs with commas or spaces. -'); ?> -field($generator, 'ns')->sticky()->hint(' - This is the namespace that the new controller class will should use. -'); ?> -field($generator, 'baseClass')->sticky()->hint(' - This is the class that the new controller class will extend from. - Please make sure the class exists and can be autoloaded. -'); ?> +field($generator, 'controller'); ?> +field($generator, 'actions'); ?> +field($generator, 'ns'); ?> +field($generator, 'baseClass'); ?> diff --git a/framework/yii/gii/generators/crud/Generator.php b/framework/yii/gii/generators/crud/Generator.php index 8ebecb8..025ff24 100644 --- a/framework/yii/gii/generators/crud/Generator.php +++ b/framework/yii/gii/generators/crud/Generator.php @@ -24,9 +24,4 @@ class Generator extends \yii\gii\Generator return 'This generator generates a controller and views that implement CRUD (Create, Read, Update, Delete) operations for the specified data model.'; } - - public function getViewFile() - { - return __DIR__ . '/views/form.php'; - } } diff --git a/framework/yii/gii/generators/form/Generator.php b/framework/yii/gii/generators/form/Generator.php index f7966a0..750a7b8 100644 --- a/framework/yii/gii/generators/form/Generator.php +++ b/framework/yii/gii/generators/form/Generator.php @@ -23,10 +23,4 @@ class Generator extends \yii\gii\Generator { return 'This generator generates a view script file that displays a form to collect input for the specified model class.'; } - - - public function getViewFile() - { - return __DIR__ . '/views/form.php'; - } } diff --git a/framework/yii/gii/generators/model/Generator.php b/framework/yii/gii/generators/model/Generator.php index 032c86d..936c75a 100644 --- a/framework/yii/gii/generators/model/Generator.php +++ b/framework/yii/gii/generators/model/Generator.php @@ -23,9 +23,4 @@ class Generator extends \yii\gii\Generator { return 'This generator generates a model class for the specified database table.'; } - - public function getViewFile() - { - return __DIR__ . '/views/form.php'; - } } diff --git a/framework/yii/gii/generators/module/Generator.php b/framework/yii/gii/generators/module/Generator.php index cff49e9..7d49122 100644 --- a/framework/yii/gii/generators/module/Generator.php +++ b/framework/yii/gii/generators/module/Generator.php @@ -23,10 +23,4 @@ class Generator extends \yii\gii\Generator { return 'This generator helps you to generate the skeleton code needed by a Yii module.'; } - - - public function getViewFile() - { - return __DIR__ . '/views/form.php'; - } } diff --git a/framework/yii/gii/lib/Horde/Text/Diff.php b/framework/yii/gii/lib/Horde/Text/Diff.php new file mode 100644 index 0000000..7480a90 --- /dev/null +++ b/framework/yii/gii/lib/Horde/Text/Diff.php @@ -0,0 +1,233 @@ +, and is used/adapted with his permission. + * + * Copyright 2004 Geoffrey T. Dairiki + * Copyright 2004-2012 Horde LLC (http://www.horde.org/) + * + * See the enclosed file COPYING for license information (LGPL). If you did + * not receive this file, see http://www.horde.org/licenses/lgpl21. + * + * @package Text_Diff + * @author Geoffrey T. Dairiki + */ +class Horde_Text_Diff +{ + /** + * Array of changes. + * + * @var array + */ + protected $_edits; + + /** + * Computes diffs between sequences of strings. + * + * @param string $engine Name of the diffing engine to use. 'auto' + * will automatically select the best. + * @param array $params Parameters to pass to the diffing engine. + * Normally an array of two arrays, each + * containing the lines from a file. + */ + public function __construct($engine, $params) + { + if ($engine == 'auto') { + $engine = extension_loaded('xdiff') ? 'Xdiff' : 'Native'; + } else { + $engine = ucfirst(basename($engine)); + } + + $class = 'Horde_Text_Diff_Engine_' . $engine; + $diff_engine = new $class(); + + $this->_edits = call_user_func_array(array($diff_engine, 'diff'), $params); + } + + /** + * Returns the array of differences. + */ + public function getDiff() + { + return $this->_edits; + } + + /** + * returns the number of new (added) lines in a given diff. + * + * @return integer The number of new lines + */ + public function countAddedLines() + { + $count = 0; + foreach ($this->_edits as $edit) { + if ($edit instanceof Horde_Text_Diff_Op_Add || + $edit instanceof Horde_Text_Diff_Op_Change) { + $count += $edit->nfinal(); + } + } + return $count; + } + + /** + * Returns the number of deleted (removed) lines in a given diff. + * + * @return integer The number of deleted lines + */ + public function countDeletedLines() + { + $count = 0; + foreach ($this->_edits as $edit) { + if ($edit instanceof Horde_Text_Diff_Op_Delete || + $edit instanceof Horde_Text_Diff_Op_Change) { + $count += $edit->norig(); + } + } + return $count; + } + + /** + * Computes a reversed diff. + * + * Example: + * + * $diff = new Horde_Text_Diff($lines1, $lines2); + * $rev = $diff->reverse(); + * + * + * @return Horde_Text_Diff A Diff object representing the inverse of the + * original diff. Note that we purposely don't return a + * reference here, since this essentially is a clone() + * method. + */ + public function reverse() + { + if (version_compare(zend_version(), '2', '>')) { + $rev = clone($this); + } else { + $rev = $this; + } + $rev->_edits = array(); + foreach ($this->_edits as $edit) { + $rev->_edits[] = $edit->reverse(); + } + return $rev; + } + + /** + * Checks for an empty diff. + * + * @return boolean True if two sequences were identical. + */ + public function isEmpty() + { + foreach ($this->_edits as $edit) { + if (!($edit instanceof Horde_Text_Diff_Op_Copy)) { + return false; + } + } + return true; + } + + /** + * Computes the length of the Longest Common Subsequence (LCS). + * + * This is mostly for diagnostic purposes. + * + * @return integer The length of the LCS. + */ + public function lcs() + { + $lcs = 0; + foreach ($this->_edits as $edit) { + if ($edit instanceof Horde_Text_Diff_Op_Copy) { + $lcs += count($edit->orig); + } + } + return $lcs; + } + + /** + * Gets the original set of lines. + * + * This reconstructs the $from_lines parameter passed to the constructor. + * + * @return array The original sequence of strings. + */ + public function getOriginal() + { + $lines = array(); + foreach ($this->_edits as $edit) { + if ($edit->orig) { + array_splice($lines, count($lines), 0, $edit->orig); + } + } + return $lines; + } + + /** + * Gets the final set of lines. + * + * This reconstructs the $to_lines parameter passed to the constructor. + * + * @return array The sequence of strings. + */ + public function getFinal() + { + $lines = array(); + foreach ($this->_edits as $edit) { + if ($edit->final) { + array_splice($lines, count($lines), 0, $edit->final); + } + } + return $lines; + } + + /** + * Removes trailing newlines from a line of text. This is meant to be used + * with array_walk(). + * + * @param string $line The line to trim. + * @param integer $key The index of the line in the array. Not used. + */ + static public function trimNewlines(&$line, $key) + { + $line = str_replace(array("\n", "\r"), '', $line); + } + + /** + * Checks a diff for validity. + * + * This is here only for debugging purposes. + */ + protected function _check($from_lines, $to_lines) + { + if (serialize($from_lines) != serialize($this->getOriginal())) { + trigger_error("Reconstructed original doesn't match", E_USER_ERROR); + } + if (serialize($to_lines) != serialize($this->getFinal())) { + trigger_error("Reconstructed final doesn't match", E_USER_ERROR); + } + + $rev = $this->reverse(); + if (serialize($to_lines) != serialize($rev->getOriginal())) { + trigger_error("Reversed original doesn't match", E_USER_ERROR); + } + if (serialize($from_lines) != serialize($rev->getFinal())) { + trigger_error("Reversed final doesn't match", E_USER_ERROR); + } + + $prevtype = null; + foreach ($this->_edits as $edit) { + if ($prevtype == get_class($edit)) { + trigger_error("Edit sequence is non-optimal", E_USER_ERROR); + } + $prevtype = get_class($edit); + } + + return true; + } +} diff --git a/framework/yii/gii/lib/Horde/Text/Diff/Engine/Native.php b/framework/yii/gii/lib/Horde/Text/Diff/Engine/Native.php new file mode 100644 index 0000000..e1318e7 --- /dev/null +++ b/framework/yii/gii/lib/Horde/Text/Diff/Engine/Native.php @@ -0,0 +1,435 @@ + 2, and some optimizations) are from + * Geoffrey T. Dairiki . The original PHP version of this + * code was written by him, and is used/adapted with his permission. + * + * Copyright 2004-2012 Horde LLC (http://www.horde.org/) + * + * See the enclosed file COPYING for license information (LGPL). If you did + * not receive this file, see http://www.horde.org/licenses/lgpl21. + * + * @author Geoffrey T. Dairiki + * @package Text_Diff + */ +class Horde_Text_Diff_Engine_Native +{ + public function diff($from_lines, $to_lines) + { + array_walk($from_lines, array('Horde_Text_Diff', 'trimNewlines')); + array_walk($to_lines, array('Horde_Text_Diff', 'trimNewlines')); + + $n_from = count($from_lines); + $n_to = count($to_lines); + + $this->xchanged = $this->ychanged = array(); + $this->xv = $this->yv = array(); + $this->xind = $this->yind = array(); + unset($this->seq); + unset($this->in_seq); + unset($this->lcs); + + // Skip leading common lines. + for ($skip = 0; $skip < $n_from && $skip < $n_to; $skip++) { + if ($from_lines[$skip] !== $to_lines[$skip]) { + break; + } + $this->xchanged[$skip] = $this->ychanged[$skip] = false; + } + + // Skip trailing common lines. + $xi = $n_from; $yi = $n_to; + for ($endskip = 0; --$xi > $skip && --$yi > $skip; $endskip++) { + if ($from_lines[$xi] !== $to_lines[$yi]) { + break; + } + $this->xchanged[$xi] = $this->ychanged[$yi] = false; + } + + // Ignore lines which do not exist in both files. + for ($xi = $skip; $xi < $n_from - $endskip; $xi++) { + $xhash[$from_lines[$xi]] = 1; + } + for ($yi = $skip; $yi < $n_to - $endskip; $yi++) { + $line = $to_lines[$yi]; + if (($this->ychanged[$yi] = empty($xhash[$line]))) { + continue; + } + $yhash[$line] = 1; + $this->yv[] = $line; + $this->yind[] = $yi; + } + for ($xi = $skip; $xi < $n_from - $endskip; $xi++) { + $line = $from_lines[$xi]; + if (($this->xchanged[$xi] = empty($yhash[$line]))) { + continue; + } + $this->xv[] = $line; + $this->xind[] = $xi; + } + + // Find the LCS. + $this->_compareseq(0, count($this->xv), 0, count($this->yv)); + + // Merge edits when possible. + $this->_shiftBoundaries($from_lines, $this->xchanged, $this->ychanged); + $this->_shiftBoundaries($to_lines, $this->ychanged, $this->xchanged); + + // Compute the edit operations. + $edits = array(); + $xi = $yi = 0; + while ($xi < $n_from || $yi < $n_to) { + assert($yi < $n_to || $this->xchanged[$xi]); + assert($xi < $n_from || $this->ychanged[$yi]); + + // Skip matching "snake". + $copy = array(); + while ($xi < $n_from && $yi < $n_to + && !$this->xchanged[$xi] && !$this->ychanged[$yi]) { + $copy[] = $from_lines[$xi++]; + ++$yi; + } + if ($copy) { + $edits[] = new Horde_Text_Diff_Op_Copy($copy); + } + + // Find deletes & adds. + $delete = array(); + while ($xi < $n_from && $this->xchanged[$xi]) { + $delete[] = $from_lines[$xi++]; + } + + $add = array(); + while ($yi < $n_to && $this->ychanged[$yi]) { + $add[] = $to_lines[$yi++]; + } + + if ($delete && $add) { + $edits[] = new Horde_Text_Diff_Op_Change($delete, $add); + } elseif ($delete) { + $edits[] = new Horde_Text_Diff_Op_Delete($delete); + } elseif ($add) { + $edits[] = new Horde_Text_Diff_Op_Add($add); + } + } + + return $edits; + } + + /** + * Divides the Largest Common Subsequence (LCS) of the sequences (XOFF, + * XLIM) and (YOFF, YLIM) into NCHUNKS approximately equally sized + * segments. + * + * Returns (LCS, PTS). LCS is the length of the LCS. PTS is an array of + * NCHUNKS+1 (X, Y) indexes giving the diving points between sub + * sequences. The first sub-sequence is contained in (X0, X1), (Y0, Y1), + * the second in (X1, X2), (Y1, Y2) and so on. Note that (X0, Y0) == + * (XOFF, YOFF) and (X[NCHUNKS], Y[NCHUNKS]) == (XLIM, YLIM). + * + * This public function assumes that the first lines of the specified portions of + * the two files do not match, and likewise that the last lines do not + * match. The caller must trim matching lines from the beginning and end + * of the portions it is going to specify. + */ + protected function _diag ($xoff, $xlim, $yoff, $ylim, $nchunks) + { + $flip = false; + + if ($xlim - $xoff > $ylim - $yoff) { + /* Things seems faster (I'm not sure I understand why) when the + * shortest sequence is in X. */ + $flip = true; + list ($xoff, $xlim, $yoff, $ylim) + = array($yoff, $ylim, $xoff, $xlim); + } + + if ($flip) { + for ($i = $ylim - 1; $i >= $yoff; $i--) { + $ymatches[$this->xv[$i]][] = $i; + } + } else { + for ($i = $ylim - 1; $i >= $yoff; $i--) { + $ymatches[$this->yv[$i]][] = $i; + } + } + + $this->lcs = 0; + $this->seq[0]= $yoff - 1; + $this->in_seq = array(); + $ymids[0] = array(); + + $numer = $xlim - $xoff + $nchunks - 1; + $x = $xoff; + for ($chunk = 0; $chunk < $nchunks; $chunk++) { + if ($chunk > 0) { + for ($i = 0; $i <= $this->lcs; $i++) { + $ymids[$i][$chunk - 1] = $this->seq[$i]; + } + } + + $x1 = $xoff + (int)(($numer + ($xlim - $xoff) * $chunk) / $nchunks); + for (; $x < $x1; $x++) { + $line = $flip ? $this->yv[$x] : $this->xv[$x]; + if (empty($ymatches[$line])) { + continue; + } + $matches = $ymatches[$line]; + reset($matches); + while (list(, $y) = each($matches)) { + if (empty($this->in_seq[$y])) { + $k = $this->_lcsPos($y); + assert($k > 0); + $ymids[$k] = $ymids[$k - 1]; + break; + } + } + while (list(, $y) = each($matches)) { + if ($y > $this->seq[$k - 1]) { + assert($y <= $this->seq[$k]); + /* Optimization: this is a common case: next match is + * just replacing previous match. */ + $this->in_seq[$this->seq[$k]] = false; + $this->seq[$k] = $y; + $this->in_seq[$y] = 1; + } elseif (empty($this->in_seq[$y])) { + $k = $this->_lcsPos($y); + assert($k > 0); + $ymids[$k] = $ymids[$k - 1]; + } + } + } + } + + $seps[] = $flip ? array($yoff, $xoff) : array($xoff, $yoff); + $ymid = $ymids[$this->lcs]; + for ($n = 0; $n < $nchunks - 1; $n++) { + $x1 = $xoff + (int)(($numer + ($xlim - $xoff) * $n) / $nchunks); + $y1 = $ymid[$n] + 1; + $seps[] = $flip ? array($y1, $x1) : array($x1, $y1); + } + $seps[] = $flip ? array($ylim, $xlim) : array($xlim, $ylim); + + return array($this->lcs, $seps); + } + + protected function _lcsPos($ypos) + { + $end = $this->lcs; + if ($end == 0 || $ypos > $this->seq[$end]) { + $this->seq[++$this->lcs] = $ypos; + $this->in_seq[$ypos] = 1; + return $this->lcs; + } + + $beg = 1; + while ($beg < $end) { + $mid = (int)(($beg + $end) / 2); + if ($ypos > $this->seq[$mid]) { + $beg = $mid + 1; + } else { + $end = $mid; + } + } + + assert($ypos != $this->seq[$end]); + + $this->in_seq[$this->seq[$end]] = false; + $this->seq[$end] = $ypos; + $this->in_seq[$ypos] = 1; + return $end; + } + + /** + * Finds LCS of two sequences. + * + * The results are recorded in the vectors $this->{x,y}changed[], by + * storing a 1 in the element for each line that is an insertion or + * deletion (ie. is not in the LCS). + * + * The subsequence of file 0 is (XOFF, XLIM) and likewise for file 1. + * + * Note that XLIM, YLIM are exclusive bounds. All line numbers are + * origin-0 and discarded lines are not counted. + */ + protected function _compareseq ($xoff, $xlim, $yoff, $ylim) + { + /* Slide down the bottom initial diagonal. */ + while ($xoff < $xlim && $yoff < $ylim + && $this->xv[$xoff] == $this->yv[$yoff]) { + ++$xoff; + ++$yoff; + } + + /* Slide up the top initial diagonal. */ + while ($xlim > $xoff && $ylim > $yoff + && $this->xv[$xlim - 1] == $this->yv[$ylim - 1]) { + --$xlim; + --$ylim; + } + + if ($xoff == $xlim || $yoff == $ylim) { + $lcs = 0; + } else { + /* This is ad hoc but seems to work well. $nchunks = + * sqrt(min($xlim - $xoff, $ylim - $yoff) / 2.5); $nchunks = + * max(2,min(8,(int)$nchunks)); */ + $nchunks = min(7, $xlim - $xoff, $ylim - $yoff) + 1; + list($lcs, $seps) + = $this->_diag($xoff, $xlim, $yoff, $ylim, $nchunks); + } + + if ($lcs == 0) { + /* X and Y sequences have no common subsequence: mark all + * changed. */ + while ($yoff < $ylim) { + $this->ychanged[$this->yind[$yoff++]] = 1; + } + while ($xoff < $xlim) { + $this->xchanged[$this->xind[$xoff++]] = 1; + } + } else { + /* Use the partitions to split this problem into subproblems. */ + reset($seps); + $pt1 = $seps[0]; + while ($pt2 = next($seps)) { + $this->_compareseq ($pt1[0], $pt2[0], $pt1[1], $pt2[1]); + $pt1 = $pt2; + } + } + } + + /** + * Adjusts inserts/deletes of identical lines to join changes as much as + * possible. + * + * We do something when a run of changed lines include a line at one end + * and has an excluded, identical line at the other. We are free to + * choose which identical line is included. `compareseq' usually chooses + * the one at the beginning, but usually it is cleaner to consider the + * following identical line to be the "change". + * + * This is extracted verbatim from analyze.c (GNU diffutils-2.7). + */ + protected function _shiftBoundaries($lines, &$changed, $other_changed) + { + $i = 0; + $j = 0; + + assert('count($lines) == count($changed)'); + $len = count($lines); + $other_len = count($other_changed); + + while (1) { + /* Scan forward to find the beginning of another run of + * changes. Also keep track of the corresponding point in the + * other file. + * + * Throughout this code, $i and $j are adjusted together so that + * the first $i elements of $changed and the first $j elements of + * $other_changed both contain the same number of zeros (unchanged + * lines). + * + * Furthermore, $j is always kept so that $j == $other_len or + * $other_changed[$j] == false. */ + while ($j < $other_len && $other_changed[$j]) { + $j++; + } + + while ($i < $len && ! $changed[$i]) { + assert('$j < $other_len && ! $other_changed[$j]'); + $i++; $j++; + while ($j < $other_len && $other_changed[$j]) { + $j++; + } + } + + if ($i == $len) { + break; + } + + $start = $i; + + /* Find the end of this run of changes. */ + while (++$i < $len && $changed[$i]) { + continue; + } + + do { + /* Record the length of this run of changes, so that we can + * later determine whether the run has grown. */ + $runlength = $i - $start; + + /* Move the changed region back, so long as the previous + * unchanged line matches the last changed one. This merges + * with previous changed regions. */ + while ($start > 0 && $lines[$start - 1] == $lines[$i - 1]) { + $changed[--$start] = 1; + $changed[--$i] = false; + while ($start > 0 && $changed[$start - 1]) { + $start--; + } + assert('$j > 0'); + while ($other_changed[--$j]) { + continue; + } + assert('$j >= 0 && !$other_changed[$j]'); + } + + /* Set CORRESPONDING to the end of the changed run, at the + * last point where it corresponds to a changed run in the + * other file. CORRESPONDING == LEN means no such point has + * been found. */ + $corresponding = $j < $other_len ? $i : $len; + + /* Move the changed region forward, so long as the first + * changed line matches the following unchanged one. This + * merges with following changed regions. Do this second, so + * that if there are no merges, the changed region is moved + * forward as far as possible. */ + while ($i < $len && $lines[$start] == $lines[$i]) { + $changed[$start++] = false; + $changed[$i++] = 1; + while ($i < $len && $changed[$i]) { + $i++; + } + + assert('$j < $other_len && ! $other_changed[$j]'); + $j++; + if ($j < $other_len && $other_changed[$j]) { + $corresponding = $i; + while ($j < $other_len && $other_changed[$j]) { + $j++; + } + } + } + } while ($runlength != $i - $start); + + /* If possible, move the fully-merged run of changes back to a + * corresponding run in the other file. */ + while ($corresponding < $i) { + $changed[--$start] = 1; + $changed[--$i] = 0; + assert('$j > 0'); + while ($other_changed[--$j]) { + continue; + } + assert('$j >= 0 && !$other_changed[$j]'); + } + } + } +} diff --git a/framework/yii/gii/lib/Horde/Text/Diff/Engine/Shell.php b/framework/yii/gii/lib/Horde/Text/Diff/Engine/Shell.php new file mode 100644 index 0000000..2f0cfd4 --- /dev/null +++ b/framework/yii/gii/lib/Horde/Text/Diff/Engine/Shell.php @@ -0,0 +1,158 @@ + + * @package Text_Diff + */ +class Horde_Text_Diff_Engine_Shell +{ + /** + * Path to the diff executable + * + * @var string + */ + protected $_diffCommand = 'diff'; + + /** + * Returns the array of differences. + * + * @param array $from_lines lines of text from old file + * @param array $to_lines lines of text from new file + * + * @return array all changes made (array with Horde_Text_Diff_Op_* objects) + */ + public function diff($from_lines, $to_lines) + { + array_walk($from_lines, array('Horde_Text_Diff', 'trimNewlines')); + array_walk($to_lines, array('Horde_Text_Diff', 'trimNewlines')); + + // Execute gnu diff or similar to get a standard diff file. + $from_file = Horde_Util::getTempFile('Horde_Text_Diff'); + $to_file = Horde_Util::getTempFile('Horde_Text_Diff'); + $fp = fopen($from_file, 'w'); + fwrite($fp, implode("\n", $from_lines)); + fclose($fp); + $fp = fopen($to_file, 'w'); + fwrite($fp, implode("\n", $to_lines)); + fclose($fp); + $diff = shell_exec($this->_diffCommand . ' ' . $from_file . ' ' . $to_file); + unlink($from_file); + unlink($to_file); + + if (is_null($diff)) { + // No changes were made + return array(new Horde_Text_Diff_Op_Copy($from_lines)); + } + + $from_line_no = 1; + $to_line_no = 1; + $edits = array(); + + // Get changed lines by parsing something like: + // 0a1,2 + // 1,2c4,6 + // 1,5d6 + preg_match_all('#^(\d+)(?:,(\d+))?([adc])(\d+)(?:,(\d+))?$#m', $diff, + $matches, PREG_SET_ORDER); + + foreach ($matches as $match) { + if (!isset($match[5])) { + // This paren is not set every time (see regex). + $match[5] = false; + } + + if ($match[3] == 'a') { + $from_line_no--; + } + + if ($match[3] == 'd') { + $to_line_no--; + } + + if ($from_line_no < $match[1] || $to_line_no < $match[4]) { + // copied lines + assert('$match[1] - $from_line_no == $match[4] - $to_line_no'); + array_push($edits, + new Horde_Text_Diff_Op_Copy( + $this->_getLines($from_lines, $from_line_no, $match[1] - 1), + $this->_getLines($to_lines, $to_line_no, $match[4] - 1))); + } + + switch ($match[3]) { + case 'd': + // deleted lines + array_push($edits, + new Horde_Text_Diff_Op_Delete( + $this->_getLines($from_lines, $from_line_no, $match[2]))); + $to_line_no++; + break; + + case 'c': + // changed lines + array_push($edits, + new Horde_Text_Diff_Op_Change( + $this->_getLines($from_lines, $from_line_no, $match[2]), + $this->_getLines($to_lines, $to_line_no, $match[5]))); + break; + + case 'a': + // added lines + array_push($edits, + new Horde_Text_Diff_Op_Add( + $this->_getLines($to_lines, $to_line_no, $match[5]))); + $from_line_no++; + break; + } + } + + if (!empty($from_lines)) { + // Some lines might still be pending. Add them as copied + array_push($edits, + new Horde_Text_Diff_Op_Copy( + $this->_getLines($from_lines, $from_line_no, + $from_line_no + count($from_lines) - 1), + $this->_getLines($to_lines, $to_line_no, + $to_line_no + count($to_lines) - 1))); + } + + return $edits; + } + + /** + * Get lines from either the old or new text + * + * @access private + * + * @param array &$text_lines Either $from_lines or $to_lines + * @param int &$line_no Current line number + * @param int $end Optional end line, when we want to chop more + * than one line. + * + * @return array The chopped lines + */ + protected function _getLines(&$text_lines, &$line_no, $end = false) + { + if (!empty($end)) { + $lines = array(); + // We can shift even more + while ($line_no <= $end) { + array_push($lines, array_shift($text_lines)); + $line_no++; + } + } else { + $lines = array(array_shift($text_lines)); + $line_no++; + } + + return $lines; + } +} diff --git a/framework/yii/gii/lib/Horde/Text/Diff/Engine/String.php b/framework/yii/gii/lib/Horde/Text/Diff/Engine/String.php new file mode 100644 index 0000000..cd0c20e --- /dev/null +++ b/framework/yii/gii/lib/Horde/Text/Diff/Engine/String.php @@ -0,0 +1,247 @@ + + * $patch = file_get_contents('example.patch'); + * $diff = new Horde_Text_Diff('string', array($patch)); + * $renderer = new Horde_Text_Diff_Renderer_inline(); + * echo $renderer->render($diff); + * + * + * Copyright 2005 Örjan Persson + * Copyright 2005-2012 Horde LLC (http://www.horde.org/) + * + * See the enclosed file COPYING for license information (LGPL). If you did + * not receive this file, see http://www.horde.org/licenses/lgpl21. + * + * @author Örjan Persson + * @package Text_Diff + */ +class Horde_Text_Diff_Engine_String +{ + /** + * Parses a unified or context diff. + * + * First param contains the whole diff and the second can be used to force + * a specific diff type. If the second parameter is 'autodetect', the + * diff will be examined to find out which type of diff this is. + * + * @param string $diff The diff content. + * @param string $mode The diff mode of the content in $diff. One of + * 'context', 'unified', or 'autodetect'. + * + * @return array List of all diff operations. + * @throws Horde_Text_Diff_Exception + */ + public function diff($diff, $mode = 'autodetect') + { + // Detect line breaks. + $lnbr = "\n"; + if (strpos($diff, "\r\n") !== false) { + $lnbr = "\r\n"; + } elseif (strpos($diff, "\r") !== false) { + $lnbr = "\r"; + } + + // Make sure we have a line break at the EOF. + if (substr($diff, -strlen($lnbr)) != $lnbr) { + $diff .= $lnbr; + } + + if ($mode != 'autodetect' && $mode != 'context' && $mode != 'unified') { + throw new Horde_Text_Diff_Exception('Type of diff is unsupported'); + } + + if ($mode == 'autodetect') { + $context = strpos($diff, '***'); + $unified = strpos($diff, '---'); + if ($context === $unified) { + throw new Horde_Text_Diff_Exception('Type of diff could not be detected'); + } elseif ($context === false || $unified === false) { + $mode = $context !== false ? 'context' : 'unified'; + } else { + $mode = $context < $unified ? 'context' : 'unified'; + } + } + + // Split by new line and remove the diff header, if there is one. + $diff = explode($lnbr, $diff); + if (($mode == 'context' && strpos($diff[0], '***') === 0) || + ($mode == 'unified' && strpos($diff[0], '---') === 0)) { + array_shift($diff); + array_shift($diff); + } + + if ($mode == 'context') { + return $this->parseContextDiff($diff); + } else { + return $this->parseUnifiedDiff($diff); + } + } + + /** + * Parses an array containing the unified diff. + * + * @param array $diff Array of lines. + * + * @return array List of all diff operations. + */ + public function parseUnifiedDiff($diff) + { + $edits = array(); + $end = count($diff) - 1; + for ($i = 0; $i < $end;) { + $diff1 = array(); + switch (substr($diff[$i], 0, 1)) { + case ' ': + do { + $diff1[] = substr($diff[$i], 1); + } while (++$i < $end && substr($diff[$i], 0, 1) == ' '); + $edits[] = new Horde_Text_Diff_Op_Copy($diff1); + break; + + case '+': + // get all new lines + do { + $diff1[] = substr($diff[$i], 1); + } while (++$i < $end && substr($diff[$i], 0, 1) == '+'); + $edits[] = new Horde_Text_Diff_Op_Add($diff1); + break; + + case '-': + // get changed or removed lines + $diff2 = array(); + do { + $diff1[] = substr($diff[$i], 1); + } while (++$i < $end && substr($diff[$i], 0, 1) == '-'); + + while ($i < $end && substr($diff[$i], 0, 1) == '+') { + $diff2[] = substr($diff[$i++], 1); + } + if (count($diff2) == 0) { + $edits[] = new Horde_Text_Diff_Op_Delete($diff1); + } else { + $edits[] = new Horde_Text_Diff_Op_Change($diff1, $diff2); + } + break; + + default: + $i++; + break; + } + } + + return $edits; + } + + /** + * Parses an array containing the context diff. + * + * @param array $diff Array of lines. + * + * @return array List of all diff operations. + */ + public function parseContextDiff(&$diff) + { + $edits = array(); + $i = $max_i = $j = $max_j = 0; + $end = count($diff) - 1; + while ($i < $end && $j < $end) { + while ($i >= $max_i && $j >= $max_j) { + // Find the boundaries of the diff output of the two files + for ($i = $j; + $i < $end && substr($diff[$i], 0, 3) == '***'; + $i++); + for ($max_i = $i; + $max_i < $end && substr($diff[$max_i], 0, 3) != '---'; + $max_i++); + for ($j = $max_i; + $j < $end && substr($diff[$j], 0, 3) == '---'; + $j++); + for ($max_j = $j; + $max_j < $end && substr($diff[$max_j], 0, 3) != '***'; + $max_j++); + } + + // find what hasn't been changed + $array = array(); + while ($i < $max_i && + $j < $max_j && + strcmp($diff[$i], $diff[$j]) == 0) { + $array[] = substr($diff[$i], 2); + $i++; + $j++; + } + + while ($i < $max_i && ($max_j-$j) <= 1) { + if ($diff[$i] != '' && substr($diff[$i], 0, 1) != ' ') { + break; + } + $array[] = substr($diff[$i++], 2); + } + + while ($j < $max_j && ($max_i-$i) <= 1) { + if ($diff[$j] != '' && substr($diff[$j], 0, 1) != ' ') { + break; + } + $array[] = substr($diff[$j++], 2); + } + if (count($array) > 0) { + $edits[] = new Horde_Text_Diff_Op_Copy($array); + } + + if ($i < $max_i) { + $diff1 = array(); + switch (substr($diff[$i], 0, 1)) { + case '!': + $diff2 = array(); + do { + $diff1[] = substr($diff[$i], 2); + if ($j < $max_j && substr($diff[$j], 0, 1) == '!') { + $diff2[] = substr($diff[$j++], 2); + } + } while (++$i < $max_i && substr($diff[$i], 0, 1) == '!'); + $edits[] = new Horde_Text_Diff_Op_Change($diff1, $diff2); + break; + + case '+': + do { + $diff1[] = substr($diff[$i], 2); + } while (++$i < $max_i && substr($diff[$i], 0, 1) == '+'); + $edits[] = new Horde_Text_Diff_Op_Add($diff1); + break; + + case '-': + do { + $diff1[] = substr($diff[$i], 2); + } while (++$i < $max_i && substr($diff[$i], 0, 1) == '-'); + $edits[] = new Horde_Text_Diff_Op_Delete($diff1); + break; + } + } + + if ($j < $max_j) { + $diff2 = array(); + switch (substr($diff[$j], 0, 1)) { + case '+': + do { + $diff2[] = substr($diff[$j++], 2); + } while ($j < $max_j && substr($diff[$j], 0, 1) == '+'); + $edits[] = new Horde_Text_Diff_Op_Add($diff2); + break; + + case '-': + do { + $diff2[] = substr($diff[$j++], 2); + } while ($j < $max_j && substr($diff[$j], 0, 1) == '-'); + $edits[] = new Horde_Text_Diff_Op_Delete($diff2); + break; + } + } + } + + return $edits; + } +} diff --git a/framework/yii/gii/lib/Horde/Text/Diff/Engine/Xdiff.php b/framework/yii/gii/lib/Horde/Text/Diff/Engine/Xdiff.php new file mode 100644 index 0000000..1be600c --- /dev/null +++ b/framework/yii/gii/lib/Horde/Text/Diff/Engine/Xdiff.php @@ -0,0 +1,67 @@ + + * @package Text_Diff + */ +class Horde_Text_Diff_Engine_Xdiff +{ + /** + */ + public function diff($from_lines, $to_lines) + { + if (!extension_loaded('xdiff')) { + throw new Horde_Text_Diff_Exception('The xdiff extension is required for this diff engine'); + } + + array_walk($from_lines, array('Horde_Text_Diff', 'trimNewlines')); + array_walk($to_lines, array('Horde_Text_Diff', 'trimNewlines')); + + /* Convert the two input arrays into strings for xdiff processing. */ + $from_string = implode("\n", $from_lines); + $to_string = implode("\n", $to_lines); + + /* Diff the two strings and convert the result to an array. */ + $diff = xdiff_string_diff($from_string, $to_string, count($to_lines)); + $diff = explode("\n", $diff); + + /* Walk through the diff one line at a time. We build the $edits + * array of diff operations by reading the first character of the + * xdiff output (which is in the "unified diff" format). + * + * Note that we don't have enough information to detect "changed" + * lines using this approach, so we can't add Horde_Text_Diff_Op_Changed + * instances to the $edits array. The result is still perfectly + * valid, albeit a little less descriptive and efficient. */ + $edits = array(); + foreach ($diff as $line) { + if (!strlen($line)) { + continue; + } + switch ($line[0]) { + case ' ': + $edits[] = new Horde_Text_Diff_Op_Copy(array(substr($line, 1))); + break; + + case '+': + $edits[] = new Horde_Text_Diff_Op_Add(array(substr($line, 1))); + break; + + case '-': + $edits[] = new Horde_Text_Diff_Op_Delete(array(substr($line, 1))); + break; + } + } + + return $edits; + } +} diff --git a/framework/yii/gii/lib/Horde/Text/Diff/Exception.php b/framework/yii/gii/lib/Horde/Text/Diff/Exception.php new file mode 100644 index 0000000..428582c --- /dev/null +++ b/framework/yii/gii/lib/Horde/Text/Diff/Exception.php @@ -0,0 +1,17 @@ + + * @category Horde + * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 + * @package Text_Diff + */ +class Horde_Text_Diff_Exception extends Horde_Exception_Wrapped +{ +} diff --git a/framework/yii/gii/lib/Horde/Text/Diff/Mapped.php b/framework/yii/gii/lib/Horde/Text/Diff/Mapped.php new file mode 100644 index 0000000..43ce5f4 --- /dev/null +++ b/framework/yii/gii/lib/Horde/Text/Diff/Mapped.php @@ -0,0 +1,52 @@ + + */ +class Horde_Text_Diff_Mapped extends Horde_Text_Diff +{ + /** + * Computes a diff between sequences of strings. + * + * This can be used to compute things like case-insensitve diffs, or diffs + * which ignore changes in white-space. + * + * @param array $from_lines An array of strings. + * @param array $to_lines An array of strings. + * @param array $mapped_from_lines This array should have the same size + * number of elements as $from_lines. The + * elements in $mapped_from_lines and + * $mapped_to_lines are what is actually + * compared when computing the diff. + * @param array $mapped_to_lines This array should have the same number + * of elements as $to_lines. + */ + public function __construct($from_lines, $to_lines, + $mapped_from_lines, $mapped_to_lines) + { + assert(count($from_lines) == count($mapped_from_lines)); + assert(count($to_lines) == count($mapped_to_lines)); + + parent::__construct($mapped_from_lines, $mapped_to_lines); + + $xi = $yi = 0; + for ($i = 0; $i < count($this->_edits); $i++) { + $orig = &$this->_edits[$i]->orig; + if (is_array($orig)) { + $orig = array_slice($from_lines, $xi, count($orig)); + $xi += count($orig); + } + + $final = &$this->_edits[$i]->final; + if (is_array($final)) { + $final = array_slice($to_lines, $yi, count($final)); + $yi += count($final); + } + } + } +} diff --git a/framework/yii/gii/lib/Horde/Text/Diff/Op/Add.php b/framework/yii/gii/lib/Horde/Text/Diff/Op/Add.php new file mode 100644 index 0000000..1443b34 --- /dev/null +++ b/framework/yii/gii/lib/Horde/Text/Diff/Op/Add.php @@ -0,0 +1,27 @@ +, and is used/adapted with his permission. + * + * Copyright 2004 Geoffrey T. Dairiki + * Copyright 2004-2012 Horde LLC (http://www.horde.org/) + * + * See the enclosed file COPYING for license information (LGPL). If you did + * not receive this file, see http://www.horde.org/licenses/lgpl21. + * + * @package Text_Diff + * @author Geoffrey T. Dairiki + */ +class Horde_Text_Diff_Op_Add extends Horde_Text_Diff_Op_Base +{ + public function __construct($lines) + { + $this->final = $lines; + $this->orig = false; + } + + public function reverse() + { + return new Horde_Text_Diff_Op_Delete($this->final); + } +} diff --git a/framework/yii/gii/lib/Horde/Text/Diff/Op/Base.php b/framework/yii/gii/lib/Horde/Text/Diff/Op/Base.php new file mode 100644 index 0000000..dbf4466 --- /dev/null +++ b/framework/yii/gii/lib/Horde/Text/Diff/Op/Base.php @@ -0,0 +1,31 @@ +, and is used/adapted with his permission. + * + * Copyright 2004 Geoffrey T. Dairiki + * Copyright 2004-2012 Horde LLC (http://www.horde.org/) + * + * See the enclosed file COPYING for license information (LGPL). If you did + * not receive this file, see http://www.horde.org/licenses/lgpl21. + * + * @package Text_Diff + * @author Geoffrey T. Dairiki + */ +abstract class Horde_Text_Diff_Op_Base +{ + public $orig; + public $final; + + abstract public function reverse(); + + public function norig() + { + return $this->orig ? count($this->orig) : 0; + } + + public function nfinal() + { + return $this->final ? count($this->final) : 0; + } +} diff --git a/framework/yii/gii/lib/Horde/Text/Diff/Op/Change.php b/framework/yii/gii/lib/Horde/Text/Diff/Op/Change.php new file mode 100644 index 0000000..b8450c3 --- /dev/null +++ b/framework/yii/gii/lib/Horde/Text/Diff/Op/Change.php @@ -0,0 +1,27 @@ +, and is used/adapted with his permission. + * + * Copyright 2004 Geoffrey T. Dairiki + * Copyright 2004-2012 Horde LLC (http://www.horde.org/) + * + * See the enclosed file COPYING for license information (LGPL). If you did + * not receive this file, see http://www.horde.org/licenses/lgpl21. + * + * @package Text_Diff + * @author Geoffrey T. Dairiki + */ +class Horde_Text_Diff_Op_Change extends Horde_Text_Diff_Op_Base +{ + public function __construct($orig, $final) + { + $this->orig = $orig; + $this->final = $final; + } + + public function reverse() + { + return new Horde_Text_Diff_Op_Change($this->final, $this->orig); + } +} diff --git a/framework/yii/gii/lib/Horde/Text/Diff/Op/Copy.php b/framework/yii/gii/lib/Horde/Text/Diff/Op/Copy.php new file mode 100644 index 0000000..b4066e5 --- /dev/null +++ b/framework/yii/gii/lib/Horde/Text/Diff/Op/Copy.php @@ -0,0 +1,30 @@ +, and is used/adapted with his permission. + * + * Copyright 2004 Geoffrey T. Dairiki + * Copyright 2004-2012 Horde LLC (http://www.horde.org/) + * + * See the enclosed file COPYING for license information (LGPL). If you did + * not receive this file, see http://www.horde.org/licenses/lgpl21. + * + * @package Text_Diff + * @author Geoffrey T. Dairiki + */ +class Horde_Text_Diff_Op_Copy extends Horde_Text_Diff_Op_Base +{ + public function __construct($orig, $final = false) + { + if (!is_array($final)) { + $final = $orig; + } + $this->orig = $orig; + $this->final = $final; + } + + public function reverse() + { + return new Horde_Text_Diff_Op_Copy($this->final, $this->orig); + } +} diff --git a/framework/yii/gii/lib/Horde/Text/Diff/Op/Delete.php b/framework/yii/gii/lib/Horde/Text/Diff/Op/Delete.php new file mode 100644 index 0000000..57d2f20 --- /dev/null +++ b/framework/yii/gii/lib/Horde/Text/Diff/Op/Delete.php @@ -0,0 +1,27 @@ +, and is used/adapted with his permission. + * + * Copyright 2004 Geoffrey T. Dairiki + * Copyright 2004-2012 Horde LLC (http://www.horde.org/) + * + * See the enclosed file COPYING for license information (LGPL). If you did + * not receive this file, see http://www.horde.org/licenses/lgpl21. + * + * @package Text_Diff + * @author Geoffrey T. Dairiki + */ +class Horde_Text_Diff_Op_Delete extends Horde_Text_Diff_Op_Base +{ + public function __construct($lines) + { + $this->orig = $lines; + $this->final = false; + } + + public function reverse() + { + return new Horde_Text_Diff_Op_Add($this->orig); + } +} diff --git a/framework/yii/gii/lib/Horde/Text/Diff/Renderer.php b/framework/yii/gii/lib/Horde/Text/Diff/Renderer.php new file mode 100644 index 0000000..d415c7d --- /dev/null +++ b/framework/yii/gii/lib/Horde/Text/Diff/Renderer.php @@ -0,0 +1,234 @@ + $value) { + $v = '_' . $param; + if (isset($this->$v)) { + $this->$v = $value; + } + } + } + + /** + * Get any renderer parameters. + * + * @return array All parameters of this renderer object. + */ + public function getParams() + { + $params = array(); + foreach (get_object_vars($this) as $k => $v) { + if ($k[0] == '_') { + $params[substr($k, 1)] = $v; + } + } + + return $params; + } + + /** + * Renders a diff. + * + * @param Horde_Text_Diff $diff A Horde_Text_Diff object. + * + * @return string The formatted output. + */ + public function render($diff) + { + $xi = $yi = 1; + $block = false; + $context = array(); + + $nlead = $this->_leading_context_lines; + $ntrail = $this->_trailing_context_lines; + + $output = $this->_startDiff(); + + $diffs = $diff->getDiff(); + foreach ($diffs as $i => $edit) { + /* If these are unchanged (copied) lines, and we want to keep + * leading or trailing context lines, extract them from the copy + * block. */ + if ($edit instanceof Horde_Text_Diff_Op_Copy) { + /* Do we have any diff blocks yet? */ + if (is_array($block)) { + /* How many lines to keep as context from the copy + * block. */ + $keep = $i == count($diffs) - 1 ? $ntrail : $nlead + $ntrail; + if (count($edit->orig) <= $keep) { + /* We have less lines in the block than we want for + * context => keep the whole block. */ + $block[] = $edit; + } else { + if ($ntrail) { + /* Create a new block with as many lines as we need + * for the trailing context. */ + $context = array_slice($edit->orig, 0, $ntrail); + $block[] = new Horde_Text_Diff_Op_Copy($context); + } + /* @todo */ + $output .= $this->_block($x0, $ntrail + $xi - $x0, + $y0, $ntrail + $yi - $y0, + $block); + $block = false; + } + } + /* Keep the copy block as the context for the next block. */ + $context = $edit->orig; + } else { + /* Don't we have any diff blocks yet? */ + if (!is_array($block)) { + /* Extract context lines from the preceding copy block. */ + $context = array_slice($context, count($context) - $nlead); + $x0 = $xi - count($context); + $y0 = $yi - count($context); + $block = array(); + if ($context) { + $block[] = new Horde_Text_Diff_Op_Copy($context); + } + } + $block[] = $edit; + } + + if ($edit->orig) { + $xi += count($edit->orig); + } + if ($edit->final) { + $yi += count($edit->final); + } + } + + if (is_array($block)) { + $output .= $this->_block($x0, $xi - $x0, + $y0, $yi - $y0, + $block); + } + + return $output . $this->_endDiff(); + } + + protected function _block($xbeg, $xlen, $ybeg, $ylen, &$edits) + { + $output = $this->_startBlock($this->_blockHeader($xbeg, $xlen, $ybeg, $ylen)); + + foreach ($edits as $edit) { + switch (get_class($edit)) { + case 'Horde_Text_Diff_Op_Copy': + $output .= $this->_context($edit->orig); + break; + + case 'Horde_Text_Diff_Op_Add': + $output .= $this->_added($edit->final); + break; + + case 'Horde_Text_Diff_Op_Delete': + $output .= $this->_deleted($edit->orig); + break; + + case 'Horde_Text_Diff_Op_Change': + $output .= $this->_changed($edit->orig, $edit->final); + break; + } + } + + return $output . $this->_endBlock(); + } + + protected function _startDiff() + { + return ''; + } + + protected function _endDiff() + { + return ''; + } + + protected function _blockHeader($xbeg, $xlen, $ybeg, $ylen) + { + if ($xlen > 1) { + $xbeg .= ',' . ($xbeg + $xlen - 1); + } + if ($ylen > 1) { + $ybeg .= ',' . ($ybeg + $ylen - 1); + } + + // this matches the GNU Diff behaviour + if ($xlen && !$ylen) { + $ybeg--; + } elseif (!$xlen) { + $xbeg--; + } + + return $xbeg . ($xlen ? ($ylen ? 'c' : 'd') : 'a') . $ybeg; + } + + protected function _startBlock($header) + { + return $header . "\n"; + } + + protected function _endBlock() + { + return ''; + } + + protected function _lines($lines, $prefix = ' ') + { + return $prefix . implode("\n$prefix", $lines) . "\n"; + } + + protected function _context($lines) + { + return $this->_lines($lines, ' '); + } + + protected function _added($lines) + { + return $this->_lines($lines, '> '); + } + + protected function _deleted($lines) + { + return $this->_lines($lines, '< '); + } + + protected function _changed($orig, $final) + { + return $this->_deleted($orig) . "---\n" . $this->_added($final); + } +} diff --git a/framework/yii/gii/lib/Horde/Text/Diff/Renderer/Context.php b/framework/yii/gii/lib/Horde/Text/Diff/Renderer/Context.php new file mode 100644 index 0000000..9891001 --- /dev/null +++ b/framework/yii/gii/lib/Horde/Text/Diff/Renderer/Context.php @@ -0,0 +1,68 @@ +_second_block = "--- $ybeg ----\n"; + return "***************\n*** $xbeg ****"; + } + + protected function _endBlock() + { + return $this->_second_block; + } + + protected function _context($lines) + { + $this->_second_block .= $this->_lines($lines, ' '); + return $this->_lines($lines, ' '); + } + + protected function _added($lines) + { + $this->_second_block .= $this->_lines($lines, '+ '); + return ''; + } + + protected function _deleted($lines) + { + return $this->_lines($lines, '- '); + } + + protected function _changed($orig, $final) + { + $this->_second_block .= $this->_lines($final, '! '); + return $this->_lines($orig, '! '); + } + +} diff --git a/framework/yii/gii/lib/Horde/Text/Diff/Renderer/Inline.php b/framework/yii/gii/lib/Horde/Text/Diff/Renderer/Inline.php new file mode 100644 index 0000000..df83fb7 --- /dev/null +++ b/framework/yii/gii/lib/Horde/Text/Diff/Renderer/Inline.php @@ -0,0 +1,193 @@ +'; + + /** + * Suffix for inserted text. + * + * @var string + */ + protected $_ins_suffix = ''; + + /** + * Prefix for deleted text. + * + * @var string + */ + protected $_del_prefix = ''; + + /** + * Suffix for deleted text. + * + * @var string + */ + protected $_del_suffix = ''; + + /** + * Header for each change block. + * + * @var string + */ + protected $_block_header = ''; + + /** + * Whether to split down to character-level. + * + * @var boolean + */ + protected $_split_characters = false; + + /** + * What are we currently splitting on? Used to recurse to show word-level + * or character-level changes. + * + * @var string + */ + protected $_split_level = 'lines'; + + protected function _blockHeader($xbeg, $xlen, $ybeg, $ylen) + { + return $this->_block_header; + } + + protected function _startBlock($header) + { + return $header; + } + + protected function _lines($lines, $prefix = ' ', $encode = true) + { + if ($encode) { + array_walk($lines, array(&$this, '_encode')); + } + + if ($this->_split_level == 'lines') { + return implode("\n", $lines) . "\n"; + } else { + return implode('', $lines); + } + } + + protected function _added($lines) + { + array_walk($lines, array(&$this, '_encode')); + $lines[0] = $this->_ins_prefix . $lines[0]; + $lines[count($lines) - 1] .= $this->_ins_suffix; + return $this->_lines($lines, ' ', false); + } + + protected function _deleted($lines, $words = false) + { + array_walk($lines, array(&$this, '_encode')); + $lines[0] = $this->_del_prefix . $lines[0]; + $lines[count($lines) - 1] .= $this->_del_suffix; + return $this->_lines($lines, ' ', false); + } + + protected function _changed($orig, $final) + { + /* If we've already split on characters, just display. */ + if ($this->_split_level == 'characters') { + return $this->_deleted($orig) + . $this->_added($final); + } + + /* If we've already split on words, just display. */ + if ($this->_split_level == 'words') { + $prefix = ''; + while ($orig[0] !== false && $final[0] !== false && + substr($orig[0], 0, 1) == ' ' && + substr($final[0], 0, 1) == ' ') { + $prefix .= substr($orig[0], 0, 1); + $orig[0] = substr($orig[0], 1); + $final[0] = substr($final[0], 1); + } + return $prefix . $this->_deleted($orig) . $this->_added($final); + } + + $text1 = implode("\n", $orig); + $text2 = implode("\n", $final); + + /* Non-printing newline marker. */ + $nl = "\0"; + + if ($this->_split_characters) { + $diff = new Horde_Text_Diff('native', + array(preg_split('//', $text1), + preg_split('//', $text2))); + } else { + /* We want to split on word boundaries, but we need to preserve + * whitespace as well. Therefore we split on words, but include + * all blocks of whitespace in the wordlist. */ + $diff = new Horde_Text_Diff('native', + array($this->_splitOnWords($text1, $nl), + $this->_splitOnWords($text2, $nl))); + } + + /* Get the diff in inline format. */ + $renderer = new Horde_Text_Diff_Renderer_inline + (array_merge($this->getParams(), + array('split_level' => $this->_split_characters ? 'characters' : 'words'))); + + /* Run the diff and get the output. */ + return str_replace($nl, "\n", $renderer->render($diff)) . "\n"; + } + + protected function _splitOnWords($string, $newlineEscape = "\n") + { + // Ignore \0; otherwise the while loop will never finish. + $string = str_replace("\0", '', $string); + + $words = array(); + $length = strlen($string); + $pos = 0; + + while ($pos < $length) { + // Eat a word with any preceding whitespace. + $spaces = strspn(substr($string, $pos), " \n"); + $nextpos = strcspn(substr($string, $pos + $spaces), " \n"); + $words[] = str_replace("\n", $newlineEscape, substr($string, $pos, $spaces + $nextpos)); + $pos += $spaces + $nextpos; + } + + return $words; + } + + protected function _encode(&$string) + { + $string = htmlspecialchars($string); + } +} diff --git a/framework/yii/gii/lib/Horde/Text/Diff/Renderer/Unified.php b/framework/yii/gii/lib/Horde/Text/Diff/Renderer/Unified.php new file mode 100644 index 0000000..560b500 --- /dev/null +++ b/framework/yii/gii/lib/Horde/Text/Diff/Renderer/Unified.php @@ -0,0 +1,57 @@ +_lines($lines, ' '); + } + + protected function _added($lines) + { + return $this->_lines($lines, '+'); + } + + protected function _deleted($lines) + { + return $this->_lines($lines, '-'); + } + + protected function _changed($orig, $final) + { + return $this->_deleted($orig) . $this->_added($final); + } +} diff --git a/framework/yii/gii/lib/Horde/Text/Diff/ThreeWay.php b/framework/yii/gii/lib/Horde/Text/Diff/ThreeWay.php new file mode 100644 index 0000000..d6e5a95 --- /dev/null +++ b/framework/yii/gii/lib/Horde/Text/Diff/ThreeWay.php @@ -0,0 +1,143 @@ + + */ +class Horde_Text_Diff_ThreeWay +{ + /** + * Array of changes. + * + * @var array + */ + protected $_edits; + + /** + * Conflict counter. + * + * @var integer + */ + protected $_conflictingBlocks = 0; + + /** + * Computes diff between 3 sequences of strings. + * + * @param array $orig The original lines to use. + * @param array $final1 The first version to compare to. + * @param array $final2 The second version to compare to. + */ + public function __construct($orig, $final1, $final2) + { + if (extension_loaded('xdiff')) { + $engine = new Horde_Text_Diff_Engine_Xdiff(); + } else { + $engine = new Horde_Text_Diff_Engine_Native(); + } + + $this->_edits = $this->_diff3($engine->diff($orig, $final1), + $engine->diff($orig, $final2)); + } + + /** + */ + public function mergedOutput($label1 = false, $label2 = false) + { + $lines = array(); + foreach ($this->_edits as $edit) { + if ($edit->isConflict()) { + /* FIXME: this should probably be moved somewhere else. */ + $lines = array_merge($lines, + array('<<<<<<<' . ($label1 ? ' ' . $label1 : '')), + $edit->final1, + array("======="), + $edit->final2, + array('>>>>>>>' . ($label2 ? ' ' . $label2 : ''))); + $this->_conflictingBlocks++; + } else { + $lines = array_merge($lines, $edit->merged()); + } + } + + return $lines; + } + + /** + */ + protected function _diff3($edits1, $edits2) + { + $edits = array(); + $bb = new Horde_Text_Diff_ThreeWay_BlockBuilder(); + + $e1 = current($edits1); + $e2 = current($edits2); + while ($e1 || $e2) { + if ($e1 && $e2 && + $e1 instanceof Horde_Text_Diff_Op_Copy && + $e2 instanceof Horde_Text_Diff_Op_Copy) { + /* We have copy blocks from both diffs. This is the (only) + * time we want to emit a diff3 copy block. Flush current + * diff3 diff block, if any. */ + if ($edit = $bb->finish()) { + $edits[] = $edit; + } + + $ncopy = min($e1->norig(), $e2->norig()); + assert($ncopy > 0); + $edits[] = new Horde_Text_Diff_ThreeWay_Op_Copy(array_slice($e1->orig, 0, $ncopy)); + + if ($e1->norig() > $ncopy) { + array_splice($e1->orig, 0, $ncopy); + array_splice($e1->final, 0, $ncopy); + } else { + $e1 = next($edits1); + } + + if ($e2->norig() > $ncopy) { + array_splice($e2->orig, 0, $ncopy); + array_splice($e2->final, 0, $ncopy); + } else { + $e2 = next($edits2); + } + } else { + if ($e1 && $e2) { + if ($e1->orig && $e2->orig) { + $norig = min($e1->norig(), $e2->norig()); + $orig = array_splice($e1->orig, 0, $norig); + array_splice($e2->orig, 0, $norig); + $bb->input($orig); + } + + if ($e1 instanceof Horde_Text_Diff_Op_Copy) { + $bb->out1(array_splice($e1->final, 0, $norig)); + } + + if ($e2 instanceof Horde_Text_Diff_Op_Copy) { + $bb->out2(array_splice($e2->final, 0, $norig)); + } + } + + if ($e1 && ! $e1->orig) { + $bb->out1($e1->final); + $e1 = next($edits1); + } + if ($e2 && ! $e2->orig) { + $bb->out2($e2->final); + $e2 = next($edits2); + } + } + } + + if ($edit = $bb->finish()) { + $edits[] = $edit; + } + + return $edits; + } +} diff --git a/framework/yii/gii/lib/Horde/Text/Diff/ThreeWay/BlockBuilder.php b/framework/yii/gii/lib/Horde/Text/Diff/ThreeWay/BlockBuilder.php new file mode 100644 index 0000000..a524d7f --- /dev/null +++ b/framework/yii/gii/lib/Horde/Text/Diff/ThreeWay/BlockBuilder.php @@ -0,0 +1,64 @@ + + */ +class Horde_Text_Diff_ThreeWay_BlockBuilder +{ + public function __construct() + { + $this->_init(); + } + + public function input($lines) + { + if ($lines) { + $this->_append($this->orig, $lines); + } + } + + public function out1($lines) + { + if ($lines) { + $this->_append($this->final1, $lines); + } + } + + public function out2($lines) + { + if ($lines) { + $this->_append($this->final2, $lines); + } + } + + public function isEmpty() + { + return !$this->orig && !$this->final1 && !$this->final2; + } + + public function finish() + { + if ($this->isEmpty()) { + return false; + } else { + $edit = new Horde_Text_Diff_ThreeWay_Op_Base($this->orig, $this->final1, $this->final2); + $this->_init(); + return $edit; + } + } + + protected function _init() + { + $this->orig = $this->final1 = $this->final2 = array(); + } + + protected function _append(&$array, $lines) + { + array_splice($array, sizeof($array), 0, $lines); + } +} diff --git a/framework/yii/gii/lib/Horde/Text/Diff/ThreeWay/Op/Base.php b/framework/yii/gii/lib/Horde/Text/Diff/ThreeWay/Op/Base.php new file mode 100644 index 0000000..b8d0a6e --- /dev/null +++ b/framework/yii/gii/lib/Horde/Text/Diff/ThreeWay/Op/Base.php @@ -0,0 +1,41 @@ + + */ +class Horde_Text_Diff_ThreeWay_Op_Base +{ + public function __construct($orig = false, $final1 = false, $final2 = false) + { + $this->orig = $orig ? $orig : array(); + $this->final1 = $final1 ? $final1 : array(); + $this->final2 = $final2 ? $final2 : array(); + } + + public function merged() + { + if (!isset($this->_merged)) { + if ($this->final1 === $this->final2) { + $this->_merged = &$this->final1; + } elseif ($this->final1 === $this->orig) { + $this->_merged = &$this->final2; + } elseif ($this->final2 === $this->orig) { + $this->_merged = &$this->final1; + } else { + $this->_merged = false; + } + } + + return $this->_merged; + } + + public function isConflict() + { + return $this->merged() === false; + } +} diff --git a/framework/yii/gii/lib/Horde/Text/Diff/ThreeWay/Op/Copy.php b/framework/yii/gii/lib/Horde/Text/Diff/ThreeWay/Op/Copy.php new file mode 100644 index 0000000..ae4cd48 --- /dev/null +++ b/framework/yii/gii/lib/Horde/Text/Diff/ThreeWay/Op/Copy.php @@ -0,0 +1,29 @@ + + */ +class Horde_Text_Diff_ThreeWay_Op_Copy extends Horde_Text_Diff_ThreeWay_Op_Base +{ + public function __construct($lines = false) + { + $this->orig = $lines ? $lines : array(); + $this->final1 = &$this->orig; + $this->final2 = &$this->orig; + } + + public function merged() + { + return $this->orig; + } + + public function isConflict() + { + return false; + } +} diff --git a/framework/yii/gii/views/default/_files.php b/framework/yii/gii/views/default/_files.php deleted file mode 100644 index 3fe1048..0000000 --- a/framework/yii/gii/views/default/_files.php +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - - - - - - $file): ?> - - - - - - - -
Code FileAction - operation !== CodeFile::OP_SKIP) { - $count++; - } - } - if ($count > 1) { - echo ''; - } - ?> -
- getRelativePath()), array('code', 'file' => $i), array('class' => 'view-code', 'rel' => $file->path)); ?> - operation === CodeFile::OP_OVERWRITE): ?> - $i), array('class' => 'view-code label label-warning', 'rel' => $file->path)); ?> - - - operation === CodeFile::OP_SKIP) { - echo 'unchanged'; - } else { - echo $file->operation; - } - ?> - - operation === CodeFile::OP_SKIP) { - echo ' '; - } else { - $key = md5($file->path); - echo Html::checkBox("answers[$key]", isset($answers) ? isset($answers[$key]) : ($file->operation === CodeFile::OP_NEW)); - } - ?> -
diff --git a/framework/yii/gii/views/default/diff.php b/framework/yii/gii/views/default/diff.php new file mode 100644 index 0000000..53763ab --- /dev/null +++ b/framework/yii/gii/views/default/diff.php @@ -0,0 +1,17 @@ + +
+ +
Diff is not supported for this file type.
+ +
Identical.
+ +
+
+
+ +
diff --git a/framework/yii/gii/views/default/index.php b/framework/yii/gii/views/default/index.php index 426d2fb..7dd1fdd 100644 --- a/framework/yii/gii/views/default/index.php +++ b/framework/yii/gii/views/default/index.php @@ -13,7 +13,7 @@ $this->title = 'Welcome to Gii'; ?>

Start the fun with the following code generators:

diff --git a/framework/yii/gii/views/default/view.php b/framework/yii/gii/views/default/view.php index 541ae26..b8ff95c 100644 --- a/framework/yii/gii/views/default/view.php +++ b/framework/yii/gii/views/default/view.php @@ -3,13 +3,15 @@ use yii\gii\Generator; use yii\helpers\Html; use yii\widgets\ActiveForm; +use yii\gii\components\ActiveField; use yii\gii\CodeFile; /** * @var yii\base\View $this * @var yii\gii\Generator $generator * @var yii\widgets\ActiveForm $form - * @var string $result + * @var string $results + * @var boolean $hasError * @var CodeFile[] $files * @var array $answers */ @@ -25,37 +27,42 @@ foreach ($generator->templates as $name => $path) {

getDescription(); ?>

- array('class' => 'yii\gii\ActiveField'))); ?> -
-
- renderFile($generator->formView(), array( - 'generator' => $generator, - 'form' => $form, - )); ?> - field($generator, 'template')->label(array('label' => 'Code Template'))->dropDownList($templates)->hint(' - Please select which set of the templates should be used to generated the code. - '); ?> -
- 'preview', 'class' => 'btn btn-primary')); ?> + array('class' => ActiveField::className()))); ?> +
+
+ renderFile($generator->formView(), array( + 'generator' => $generator, + 'form' => $form, + )); ?> + field($generator, 'template')->sticky() + ->label(array('label' => 'Code Template')) + ->dropDownList($templates)->hint(' + Please select which set of the templates should be used to generated the code. + '); ?> +
+ 'preview', 'class' => 'btn btn-primary')); ?> - - 'generate', 'class' => 'btn btn-danger')); ?> - + + 'generate', 'class' => 'btn btn-danger')); ?> + +
-
- - ' . $result . '
'; - } elseif (isset($files)) { - echo $this->render('_files', array( - 'generator' => $generator, - 'files' => $files, - 'answers' => $answers, - )); - } - ?> + render('view/results', array( + 'generator' => $generator, + 'results' => $results, + 'hasError' => $hasError, + )); + } elseif (isset($files)) { + echo $this->render('view/files', array( + 'generator' => $generator, + 'files' => $files, + 'answers' => $answers, + )); + } + ?>
diff --git a/framework/yii/gii/views/default/view/files.php b/framework/yii/gii/views/default/view/files.php new file mode 100644 index 0000000..ff49e89 --- /dev/null +++ b/framework/yii/gii/views/default/view/files.php @@ -0,0 +1,79 @@ + +
+

Click on the above Generate button to generate the files selected below:

+ + + + + + + + + + + + + + + + + + +
Code FileAction + operation !== CodeFile::OP_SKIP) { + echo ''; + break; + } + } + ?> +
+ getRelativePath()), array('preview', 'file' => $file->id), array('class' => 'preview-code', 'data-title' => $file->getRelativePath())); ?> + operation === CodeFile::OP_OVERWRITE): ?> + $file->id), array('class' => 'diff-code label label-warning', 'data-title' => $file->getRelativePath())); ?> + + + operation === CodeFile::OP_SKIP) { + echo 'unchanged'; + } else { + echo $file->operation; + } + ?> + + operation === CodeFile::OP_SKIP) { + echo ' '; + } else { + echo Html::checkBox("answers[{$file->id}]", isset($answers) ? isset($answers[$file->id]) : ($file->operation === CodeFile::OP_NEW)); + } + ?> +
+ + +
diff --git a/framework/yii/gii/views/default/view/results.php b/framework/yii/gii/views/default/view/results.php new file mode 100644 index 0000000..0173882 --- /dev/null +++ b/framework/yii/gii/views/default/view/results.php @@ -0,0 +1,22 @@ + +
+ There was something wrong when generating the code. Please check the following messages.
'; + } else { + echo '
' . $generator->successMessage() . '
'; + } + ?> +
+