From b20884a842a6833d5977273508d31cbfe3b28f40 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 20 Jun 2014 11:49:41 -0700 Subject: [PATCH] Substantially support character encodings and "Highlight As" in changesets Summary: Ref T5179. Ref T4045. Ref T832. We can now write non-utf8 hunks into the database, so try to do more reasonable things with them in the UI. Test Plan: (See screenshots...) Reviewers: btrahan Reviewed By: btrahan Subscribers: epriestley Maniphest Tasks: T832, T4045, T5179 Differential Revision: https://secure.phabricator.com/D9294 --- resources/celerity/map.php | 67 +-- src/__phutil_library_map__.php | 4 + .../DifferentialChangesetViewController.php | 2 + .../parser/DifferentialChangesetParser.php | 75 ++- .../DifferentialChangesetHTMLRenderer.php | 503 +++++++++--------- .../render/DifferentialChangesetRenderer.php | 10 + .../storage/DifferentialHunkLegacy.php | 9 + .../storage/DifferentialHunkModern.php | 8 +- .../view/DifferentialChangesetListView.php | 5 + .../controller/DiffusionDiffController.php | 3 + .../PhabricatorApplicationSystem.php | 4 + ...bricatorSystemSelectEncodingController.php | 55 ++ ...ricatorSystemSelectHighlightController.php | 38 ++ .../storage/lisk/PhabricatorLiskDAO.php | 23 + .../differential/ChangesetViewManager.js | 35 +- .../differential/behavior-dropdown-menus.js | 91 +++- 16 files changed, 626 insertions(+), 306 deletions(-) create mode 100644 src/applications/system/controller/PhabricatorSystemSelectEncodingController.php create mode 100644 src/applications/system/controller/PhabricatorSystemSelectHighlightController.php diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 83b11080f9..f90fc3562c 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -11,7 +11,7 @@ return array( 'core.pkg.js' => '07b01d4f', 'darkconsole.pkg.js' => 'ca8671ce', 'differential.pkg.css' => '4a93db37', - 'differential.pkg.js' => 'eca39a2c', + 'differential.pkg.js' => '2b128f3a', 'diffusion.pkg.css' => '471bc9eb', 'diffusion.pkg.js' => '077e3ad0', 'maniphest.pkg.css' => 'f88a8402', @@ -358,13 +358,13 @@ return array( 'rsrc/js/application/dashboard/behavior-dashboard-move-panels.js' => 'fa187a68', 'rsrc/js/application/dashboard/behavior-dashboard-query-panel-select.js' => '3be3eef5', 'rsrc/js/application/dashboard/behavior-dashboard-tab-panel.js' => 'aa077691', - 'rsrc/js/application/differential/ChangesetViewManager.js' => 'db09a523', + 'rsrc/js/application/differential/ChangesetViewManager.js' => '1f304ef8', 'rsrc/js/application/differential/DifferentialInlineCommentEditor.js' => 'f2441746', 'rsrc/js/application/differential/behavior-add-reviewers-and-ccs.js' => '533a187b', 'rsrc/js/application/differential/behavior-comment-jump.js' => '71755c79', 'rsrc/js/application/differential/behavior-comment-preview.js' => '127f2018', 'rsrc/js/application/differential/behavior-diff-radios.js' => 'e1ff79b1', - 'rsrc/js/application/differential/behavior-dropdown-menus.js' => '64a79839', + 'rsrc/js/application/differential/behavior-dropdown-menus.js' => '710f209e', 'rsrc/js/application/differential/behavior-edit-inline-comments.js' => '00861799', 'rsrc/js/application/differential/behavior-keyboard-nav.js' => '173ce7e7', 'rsrc/js/application/differential/behavior-populate.js' => 'bdb3e4d0', @@ -509,7 +509,7 @@ return array( 'aphront-two-column-view-css' => '16ab3ad2', 'aphront-typeahead-control-css' => 'a989b5b3', 'auth-css' => '1e655982', - 'changeset-view-manager' => 'db09a523', + 'changeset-view-manager' => '1f304ef8', 'config-options-css' => '7fedf08b', 'conpherence-menu-css' => 'e1e0fdf1', 'conpherence-message-pane-css' => '11a393ca', @@ -564,7 +564,7 @@ return array( 'javelin-behavior-differential-add-reviewers-and-ccs' => '533a187b', 'javelin-behavior-differential-comment-jump' => '71755c79', 'javelin-behavior-differential-diff-radios' => 'e1ff79b1', - 'javelin-behavior-differential-dropdown-menus' => '64a79839', + 'javelin-behavior-differential-dropdown-menus' => '710f209e', 'javelin-behavior-differential-edit-inline-comments' => '00861799', 'javelin-behavior-differential-feedback-preview' => '127f2018', 'javelin-behavior-differential-keyboard-navigation' => '173ce7e7', @@ -1001,6 +1001,17 @@ return array( 5 => 'phabricator-drag-and-drop-file-upload', 6 => 'phabricator-draggable-list', ), + '1f304ef8' => + array( + 0 => 'javelin-dom', + 1 => 'javelin-util', + 2 => 'javelin-stratcom', + 3 => 'javelin-install', + 4 => 'javelin-workflow', + 5 => 'javelin-router', + 6 => 'javelin-behavior-device', + 7 => 'javelin-vector', + ), '2290aeef' => array( 0 => 'javelin-install', @@ -1273,11 +1284,6 @@ return array( 2 => 'javelin-util', 3 => 'phabricator-shaped-request', ), - '7319e029' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-dom', - ), '62e18640' => array( 0 => 'javelin-install', @@ -1291,18 +1297,6 @@ return array( 1 => 'javelin-dom', 2 => 'javelin-fx', ), - '64a79839' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-dom', - 2 => 'javelin-util', - 3 => 'javelin-stratcom', - 4 => 'phuix-dropdown-menu', - 5 => 'phuix-action-list-view', - 6 => 'phuix-action-view', - 7 => 'phabricator-phtize', - 8 => 'changeset-view-manager', - ), '64ef2fd2' => array( 0 => 'javelin-behavior', @@ -1344,12 +1338,30 @@ return array( 1 => 'javelin-dom', 2 => 'javelin-util', ), + '710f209e' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-dom', + 2 => 'javelin-util', + 3 => 'javelin-stratcom', + 4 => 'javelin-workflow', + 5 => 'phuix-dropdown-menu', + 6 => 'phuix-action-list-view', + 7 => 'phuix-action-view', + 8 => 'phabricator-phtize', + 9 => 'changeset-view-manager', + ), '71755c79' => array( 0 => 'javelin-behavior', 1 => 'javelin-stratcom', 2 => 'javelin-dom', ), + '7319e029' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-dom', + ), '76f4ebed' => array( 0 => 'javelin-install', @@ -1916,17 +1928,6 @@ return array( 1 => 'javelin-util', 2 => 'javelin-stratcom', ), - 'db09a523' => - array( - 0 => 'javelin-dom', - 1 => 'javelin-util', - 2 => 'javelin-stratcom', - 3 => 'javelin-install', - 4 => 'javelin-workflow', - 5 => 'javelin-router', - 6 => 'javelin-behavior-device', - 7 => 'javelin-vector', - ), 'dd7e8ef5' => array( 0 => 'javelin-behavior', diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 346daefb0c..2ba17d3e22 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2258,6 +2258,8 @@ phutil_register_library_map(array( 'PhabricatorSystemRemoveDestroyWorkflow' => 'applications/system/management/PhabricatorSystemRemoveDestroyWorkflow.php', 'PhabricatorSystemRemoveLogWorkflow' => 'applications/system/management/PhabricatorSystemRemoveLogWorkflow.php', 'PhabricatorSystemRemoveWorkflow' => 'applications/system/management/PhabricatorSystemRemoveWorkflow.php', + 'PhabricatorSystemSelectEncodingController' => 'applications/system/controller/PhabricatorSystemSelectEncodingController.php', + 'PhabricatorSystemSelectHighlightController' => 'applications/system/controller/PhabricatorSystemSelectHighlightController.php', 'PhabricatorTaskmasterDaemon' => 'infrastructure/daemon/workers/PhabricatorTaskmasterDaemon.php', 'PhabricatorTestCase' => 'infrastructure/testing/PhabricatorTestCase.php', 'PhabricatorTestController' => 'applications/base/controller/__tests__/PhabricatorTestController.php', @@ -5134,6 +5136,8 @@ phutil_register_library_map(array( 'PhabricatorSystemRemoveDestroyWorkflow' => 'PhabricatorSystemRemoveWorkflow', 'PhabricatorSystemRemoveLogWorkflow' => 'PhabricatorSystemRemoveWorkflow', 'PhabricatorSystemRemoveWorkflow' => 'PhabricatorManagementWorkflow', + 'PhabricatorSystemSelectEncodingController' => 'PhabricatorController', + 'PhabricatorSystemSelectHighlightController' => 'PhabricatorController', 'PhabricatorTaskmasterDaemon' => 'PhabricatorDaemon', 'PhabricatorTestCase' => 'ArcanistPhutilTestCase', 'PhabricatorTestController' => 'PhabricatorController', diff --git a/src/applications/differential/controller/DifferentialChangesetViewController.php b/src/applications/differential/controller/DifferentialChangesetViewController.php index abbbb4c031..1aba4d6b6a 100644 --- a/src/applications/differential/controller/DifferentialChangesetViewController.php +++ b/src/applications/differential/controller/DifferentialChangesetViewController.php @@ -155,6 +155,8 @@ final class DifferentialChangesetViewController extends DifferentialController { $parser->setRightSideCommentMapping($right_source, $right_new); $parser->setLeftSideCommentMapping($left_source, $left_new); $parser->setWhitespaceMode($request->getStr('whitespace')); + $parser->setCharacterEncoding($request->getStr('encoding')); + $parser->setHighlightAs($request->getStr('highlight')); if ($request->getStr('renderer') == '1up') { $parser->setRenderer(new DifferentialChangesetOneUpRenderer()); diff --git a/src/applications/differential/parser/DifferentialChangesetParser.php b/src/applications/differential/parser/DifferentialChangesetParser.php index 6ca27db88e..a2df4ba918 100644 --- a/src/applications/differential/parser/DifferentialChangesetParser.php +++ b/src/applications/differential/parser/DifferentialChangesetParser.php @@ -41,6 +41,26 @@ final class DifferentialChangesetParser { private $highlightErrors; private $disableCache; private $renderer; + private $characterEncoding; + private $highlightAs; + + public function setHighlightAs($highlight_as) { + $this->highlightAs = $highlight_as; + return $this; + } + + public function getHighlightAs() { + return $this->highlightAs; + } + + public function setCharacterEncoding($character_encoding) { + $this->characterEncoding = $character_encoding; + return $this; + } + + public function getCharacterEncoding() { + return $this->characterEncoding; + } public function setRenderer($renderer) { $this->renderer = $renderer; @@ -422,8 +442,15 @@ final class DifferentialChangesetParser { } private function getHighlightFuture($corpus) { + $language = $this->highlightAs; + + if (!$language) { + $language = $this->highlightEngine->getLanguageFromFilename( + $this->filename); + } + return $this->highlightEngine->getHighlightFuture( - $this->highlightEngine->getLanguageFromFilename($this->filename), + $language, $corpus); } @@ -455,6 +482,14 @@ final class DifferentialChangesetParser { $skip_cache = true; } + if ($this->characterEncoding) { + $skip_cache = true; + } + + if ($this->highlightAs) { + $skip_cache = true; + } + $this->whitespaceMode = $whitespace_mode; $changeset = $this->changeset; @@ -677,6 +712,25 @@ final class DifferentialChangesetParser { // requests. $this->isTopLevel = (($range_start === null) && ($range_len === null)); $this->highlightEngine = PhabricatorSyntaxHighlighter::newEngine(); + + $encoding = null; + if ($this->characterEncoding) { + // We are forcing this changeset to be interpreted with a specific + // character encoding, so force all the hunks into that encoding and + // propagate it to the renderer. + $encoding = $this->characterEncoding; + foreach ($this->changeset->getHunks() as $hunk) { + $hunk->forceEncoding($this->characterEncoding); + } + } else { + // We're just using the default, so tell the renderer what that is + // (by reading the encoding from the first hunk). + foreach ($this->changeset->getHunks() as $hunk) { + $encoding = $hunk->getDataEncoding(); + break; + } + } + $this->tryCacheStuff(); $render_pch = $this->shouldRenderPropertyChangeHeader($this->changeset); @@ -700,7 +754,8 @@ final class DifferentialChangesetParser { ->setMarkupEngine($this->markupEngine) ->setHandles($this->handles) ->setOldLines($this->old) - ->setNewLines($this->new); + ->setNewLines($this->new) + ->setOriginalCharacterEncoding($encoding); if ($this->user) { $renderer->setUser($this->user); @@ -1013,18 +1068,18 @@ final class DifferentialChangesetParser { private function isCommentVisibleOnRenderedDiff( PhabricatorInlineCommentInterface $comment) { - $changeset_id = $comment->getChangesetID(); - $is_new = $comment->getIsNewFile(); + $changeset_id = $comment->getChangesetID(); + $is_new = $comment->getIsNewFile(); - if ($changeset_id == $this->rightSideChangesetID && + if ($changeset_id == $this->rightSideChangesetID && $is_new == $this->rightSideAttachesToNewFile) { - return true; - } + return true; + } - if ($changeset_id == $this->leftSideChangesetID && + if ($changeset_id == $this->leftSideChangesetID && $is_new == $this->leftSideAttachesToNewFile) { - return true; - } + return true; + } return false; } diff --git a/src/applications/differential/render/DifferentialChangesetHTMLRenderer.php b/src/applications/differential/render/DifferentialChangesetHTMLRenderer.php index d37a2b9f06..9d46f170b6 100644 --- a/src/applications/differential/render/DifferentialChangesetHTMLRenderer.php +++ b/src/applications/differential/render/DifferentialChangesetHTMLRenderer.php @@ -9,248 +9,275 @@ abstract class DifferentialChangesetHTMLRenderer $change = $changeset->getChangeType(); $file = $changeset->getFileType(); - $message = null; - if ($change == DifferentialChangeType::TYPE_CHANGE && - $file == DifferentialChangeType::FILE_TEXT) { - if ($force) { - // We have to force something to render because there were no changes - // of other kinds. - $message = pht('This file was not modified.'); + $messages = array(); + $none = hsprintf(''); + switch ($change) { + + case DifferentialChangeType::TYPE_ADD: + switch ($file) { + case DifferentialChangeType::FILE_TEXT: + $messages[] = pht( + 'This file was added.', + $none); + break; + case DifferentialChangeType::FILE_IMAGE: + $messages[] = pht( + 'This image was added.', + $none); + break; + case DifferentialChangeType::FILE_DIRECTORY: + $messages[] = pht( + 'This directory was added.', + $none); + break; + case DifferentialChangeType::FILE_BINARY: + $messages[] = pht( + 'This binary file was added.', + $none); + break; + case DifferentialChangeType::FILE_SYMLINK: + $messages[] = pht( + 'This symlink was added.', + $none); + break; + case DifferentialChangeType::FILE_SUBMODULE: + $messages[] = pht( + 'This submodule was added.', + $none); + break; + } + break; + + case DifferentialChangeType::TYPE_DELETE: + switch ($file) { + case DifferentialChangeType::FILE_TEXT: + $messages[] = pht( + 'This file was deleted.', + $none); + break; + case DifferentialChangeType::FILE_IMAGE: + $messages[] = pht( + 'This image was deleted.', + $none); + break; + case DifferentialChangeType::FILE_DIRECTORY: + $messages[] = pht( + 'This directory was deleted.', + $none); + break; + case DifferentialChangeType::FILE_BINARY: + $messages[] = pht( + 'This binary file was deleted.', + $none); + break; + case DifferentialChangeType::FILE_SYMLINK: + $messages[] = pht( + 'This symlink was deleted.', + $none); + break; + case DifferentialChangeType::FILE_SUBMODULE: + $messages[] = pht( + 'This submodule was deleted.', + $none); + break; + } + break; + + case DifferentialChangeType::TYPE_MOVE_HERE: + $from = phutil_tag('strong', array(), $changeset->getOldFile()); + switch ($file) { + case DifferentialChangeType::FILE_TEXT: + $messages[] = pht('This file was moved from %s.', $from); + break; + case DifferentialChangeType::FILE_IMAGE: + $messages[] = pht('This image was moved from %s.', $from); + break; + case DifferentialChangeType::FILE_DIRECTORY: + $messages[] = pht('This directory was moved from %s.', $from); + break; + case DifferentialChangeType::FILE_BINARY: + $messages[] = pht('This binary file was moved from %s.', $from); + break; + case DifferentialChangeType::FILE_SYMLINK: + $messages[] = pht('This symlink was moved from %s.', $from); + break; + case DifferentialChangeType::FILE_SUBMODULE: + $messages[] = pht('This submodule was moved from %s.', $from); + break; + } + break; + + case DifferentialChangeType::TYPE_COPY_HERE: + $from = phutil_tag('strong', array(), $changeset->getOldFile()); + switch ($file) { + case DifferentialChangeType::FILE_TEXT: + $messages[] = pht('This file was copied from %s.', $from); + break; + case DifferentialChangeType::FILE_IMAGE: + $messages[] = pht('This image was copied from %s.', $from); + break; + case DifferentialChangeType::FILE_DIRECTORY: + $messages[] = pht('This directory was copied from %s.', $from); + break; + case DifferentialChangeType::FILE_BINARY: + $messages[] = pht('This binary file was copied from %s.', $from); + break; + case DifferentialChangeType::FILE_SYMLINK: + $messages[] = pht('This symlink was copied from %s.', $from); + break; + case DifferentialChangeType::FILE_SUBMODULE: + $messages[] = pht('This submodule was copied from %s.', $from); + break; + } + break; + + case DifferentialChangeType::TYPE_MOVE_AWAY: + $paths = phutil_tag( + 'strong', + array(), + implode(', ', $changeset->getAwayPaths())); + switch ($file) { + case DifferentialChangeType::FILE_TEXT: + $messages[] = pht('This file was moved to %s.', $paths); + break; + case DifferentialChangeType::FILE_IMAGE: + $messages[] = pht('This image was moved to %s.', $paths); + break; + case DifferentialChangeType::FILE_DIRECTORY: + $messages[] = pht('This directory was moved to %s.', $paths); + break; + case DifferentialChangeType::FILE_BINARY: + $messages[] = pht('This binary file was moved to %s.', $paths); + break; + case DifferentialChangeType::FILE_SYMLINK: + $messages[] = pht('This symlink was moved to %s.', $paths); + break; + case DifferentialChangeType::FILE_SUBMODULE: + $messages[] = pht('This submodule was moved to %s.', $paths); + break; + } + break; + + case DifferentialChangeType::TYPE_COPY_AWAY: + $paths = phutil_tag( + 'strong', + array(), + implode(', ', $changeset->getAwayPaths())); + switch ($file) { + case DifferentialChangeType::FILE_TEXT: + $messages[] = pht('This file was copied to %s.', $paths); + break; + case DifferentialChangeType::FILE_IMAGE: + $messages[] = pht('This image was copied to %s.', $paths); + break; + case DifferentialChangeType::FILE_DIRECTORY: + $messages[] = pht('This directory was copied to %s.', $paths); + break; + case DifferentialChangeType::FILE_BINARY: + $messages[] = pht('This binary file was copied to %s.', $paths); + break; + case DifferentialChangeType::FILE_SYMLINK: + $messages[] = pht('This symlink was copied to %s.', $paths); + break; + case DifferentialChangeType::FILE_SUBMODULE: + $messages[] = pht('This submodule was copied to %s.', $paths); + break; + } + break; + + case DifferentialChangeType::TYPE_MULTICOPY: + $paths = phutil_tag( + 'strong', + array(), + implode(', ', $changeset->getAwayPaths())); + switch ($file) { + case DifferentialChangeType::FILE_TEXT: + $messages[] = pht( + 'This file was deleted after being copied to %s.', + $paths); + break; + case DifferentialChangeType::FILE_IMAGE: + $messages[] = pht( + 'This image was deleted after being copied to %s.', + $paths); + break; + case DifferentialChangeType::FILE_DIRECTORY: + $messages[] = pht( + 'This directory was deleted after being copied to %s.', + $paths); + break; + case DifferentialChangeType::FILE_BINARY: + $messages[] = pht( + 'This binary file was deleted after being copied to %s.', + $paths); + break; + case DifferentialChangeType::FILE_SYMLINK: + $messages[] = pht( + 'This symlink was deleted after being copied to %s.', + $paths); + break; + case DifferentialChangeType::FILE_SUBMODULE: + $messages[] = pht( + 'This submodule was deleted after being copied to %s.', + $paths); + break; + } + break; + + default: + switch ($file) { + case DifferentialChangeType::FILE_TEXT: + // This is the default case, so we only render this header if + // forced to since it's not very useful. + if ($force) { + $messages[] = pht('This file was not modified.'); + } + break; + case DifferentialChangeType::FILE_IMAGE: + $messages[] = pht('This is an image.'); + break; + case DifferentialChangeType::FILE_DIRECTORY: + $messages[] = pht('This is a directory.'); + break; + case DifferentialChangeType::FILE_BINARY: + $messages[] = pht('This is a binary file.'); + break; + case DifferentialChangeType::FILE_SYMLINK: + $messages[] = pht('This is a symlink.'); + break; + case DifferentialChangeType::FILE_SUBMODULE: + $messages[] = pht('This is a submodule.'); + break; + } + break; + } + + $encoding = $this->getOriginalCharacterEncoding(); + if ($encoding != 'utf8' && ($file == DifferentialChangeType::FILE_TEXT)) { + if ($encoding) { + $messages[] = pht( + 'This file was converted from %s for display.', + phutil_tag('strong', array(), $encoding)); } else { - // Default case of changes to a text file, no metadata. - return null; - } - } else { - $none = hsprintf(''); - switch ($change) { - - case DifferentialChangeType::TYPE_ADD: - switch ($file) { - case DifferentialChangeType::FILE_TEXT: - $message = pht('This file was added.', $none); - break; - case DifferentialChangeType::FILE_IMAGE: - $message = pht('This image was added.', $none); - break; - case DifferentialChangeType::FILE_DIRECTORY: - $message = pht( - 'This directory was added.', - $none); - break; - case DifferentialChangeType::FILE_BINARY: - $message = pht( - 'This binary file was added.', - $none); - break; - case DifferentialChangeType::FILE_SYMLINK: - $message = pht('This symlink was added.', $none); - break; - case DifferentialChangeType::FILE_SUBMODULE: - $message = pht( - 'This submodule was added.', - $none); - break; - } - break; - - case DifferentialChangeType::TYPE_DELETE: - switch ($file) { - case DifferentialChangeType::FILE_TEXT: - $message = pht('This file was deleted.', $none); - break; - case DifferentialChangeType::FILE_IMAGE: - $message = pht('This image was deleted.', $none); - break; - case DifferentialChangeType::FILE_DIRECTORY: - $message = pht( - 'This directory was deleted.', - $none); - break; - case DifferentialChangeType::FILE_BINARY: - $message = pht( - 'This binary file was deleted.', - $none); - break; - case DifferentialChangeType::FILE_SYMLINK: - $message = pht( - 'This symlink was deleted.', - $none); - break; - case DifferentialChangeType::FILE_SUBMODULE: - $message = pht( - 'This submodule was deleted.', - $none); - break; - } - break; - - case DifferentialChangeType::TYPE_MOVE_HERE: - $from = phutil_tag('strong', array(), $changeset->getOldFile()); - switch ($file) { - case DifferentialChangeType::FILE_TEXT: - $message = pht('This file was moved from %s.', $from); - break; - case DifferentialChangeType::FILE_IMAGE: - $message = pht('This image was moved from %s.', $from); - break; - case DifferentialChangeType::FILE_DIRECTORY: - $message = pht('This directory was moved from %s.', $from); - break; - case DifferentialChangeType::FILE_BINARY: - $message = pht('This binary file was moved from %s.', $from); - break; - case DifferentialChangeType::FILE_SYMLINK: - $message = pht('This symlink was moved from %s.', $from); - break; - case DifferentialChangeType::FILE_SUBMODULE: - $message = pht('This submodule was moved from %s.', $from); - break; - } - break; - - case DifferentialChangeType::TYPE_COPY_HERE: - $from = phutil_tag('strong', array(), $changeset->getOldFile()); - switch ($file) { - case DifferentialChangeType::FILE_TEXT: - $message = pht('This file was copied from %s.', $from); - break; - case DifferentialChangeType::FILE_IMAGE: - $message = pht('This image was copied from %s.', $from); - break; - case DifferentialChangeType::FILE_DIRECTORY: - $message = pht('This directory was copied from %s.', $from); - break; - case DifferentialChangeType::FILE_BINARY: - $message = pht('This binary file was copied from %s.', $from); - break; - case DifferentialChangeType::FILE_SYMLINK: - $message = pht('This symlink was copied from %s.', $from); - break; - case DifferentialChangeType::FILE_SUBMODULE: - $message = pht('This submodule was copied from %s.', $from); - break; - } - break; - - case DifferentialChangeType::TYPE_MOVE_AWAY: - $paths = phutil_tag( - 'strong', - array(), - implode(', ', $changeset->getAwayPaths())); - switch ($file) { - case DifferentialChangeType::FILE_TEXT: - $message = pht('This file was moved to %s.', $paths); - break; - case DifferentialChangeType::FILE_IMAGE: - $message = pht('This image was moved to %s.', $paths); - break; - case DifferentialChangeType::FILE_DIRECTORY: - $message = pht('This directory was moved to %s.', $paths); - break; - case DifferentialChangeType::FILE_BINARY: - $message = pht('This binary file was moved to %s.', $paths); - break; - case DifferentialChangeType::FILE_SYMLINK: - $message = pht('This symlink was moved to %s.', $paths); - break; - case DifferentialChangeType::FILE_SUBMODULE: - $message = pht('This submodule was moved to %s.', $paths); - break; - } - break; - - case DifferentialChangeType::TYPE_COPY_AWAY: - $paths = phutil_tag( - 'strong', - array(), - implode(', ', $changeset->getAwayPaths())); - switch ($file) { - case DifferentialChangeType::FILE_TEXT: - $message = pht('This file was copied to %s.', $paths); - break; - case DifferentialChangeType::FILE_IMAGE: - $message = pht('This image was copied to %s.', $paths); - break; - case DifferentialChangeType::FILE_DIRECTORY: - $message = pht('This directory was copied to %s.', $paths); - break; - case DifferentialChangeType::FILE_BINARY: - $message = pht('This binary file was copied to %s.', $paths); - break; - case DifferentialChangeType::FILE_SYMLINK: - $message = pht('This symlink was copied to %s.', $paths); - break; - case DifferentialChangeType::FILE_SUBMODULE: - $message = pht('This submodule was copied to %s.', $paths); - break; - } - break; - - case DifferentialChangeType::TYPE_MULTICOPY: - $paths = phutil_tag( - 'strong', - array(), - implode(', ', $changeset->getAwayPaths())); - switch ($file) { - case DifferentialChangeType::FILE_TEXT: - $message = pht( - 'This file was deleted after being copied to %s.', - $paths); - break; - case DifferentialChangeType::FILE_IMAGE: - $message = pht( - 'This image was deleted after being copied to %s.', - $paths); - break; - case DifferentialChangeType::FILE_DIRECTORY: - $message = pht( - 'This directory was deleted after being copied to %s.', - $paths); - break; - case DifferentialChangeType::FILE_BINARY: - $message = pht( - 'This binary file was deleted after being copied to %s.', - $paths); - break; - case DifferentialChangeType::FILE_SYMLINK: - $message = pht( - 'This symlink was deleted after being copied to %s.', - $paths); - break; - case DifferentialChangeType::FILE_SUBMODULE: - $message = pht( - 'This submodule was deleted after being copied to %s.', - $paths); - break; - } - break; - - default: - switch ($file) { - case DifferentialChangeType::FILE_TEXT: - $message = pht('This is a file.'); - break; - case DifferentialChangeType::FILE_IMAGE: - $message = pht('This is an image.'); - break; - case DifferentialChangeType::FILE_DIRECTORY: - $message = pht('This is a directory.'); - break; - case DifferentialChangeType::FILE_BINARY: - $message = pht('This is a binary file.'); - break; - case DifferentialChangeType::FILE_SYMLINK: - $message = pht('This is a symlink.'); - break; - case DifferentialChangeType::FILE_SUBMODULE: - $message = pht('This is a submodule.'); - break; - } - break; + $messages[] = pht( + 'This file uses an unknown character encoding.'); } } - return phutil_tag_div('differential-meta-notice', $message); + if (!$messages) { + return null; + } + + foreach ($messages as $key => $message) { + $messages[$key] = phutil_tag('li', array(), $message); + } + + return phutil_tag( + 'ul', + array( + 'class' => 'differential-meta-notice', + ), + $messages); } protected function renderPropertyChangeHeader() { diff --git a/src/applications/differential/render/DifferentialChangesetRenderer.php b/src/applications/differential/render/DifferentialChangesetRenderer.php index 4f26319118..a7da1f80db 100644 --- a/src/applications/differential/render/DifferentialChangesetRenderer.php +++ b/src/applications/differential/render/DifferentialChangesetRenderer.php @@ -28,6 +28,16 @@ abstract class DifferentialChangesetRenderer { private $gaps; private $mask; private $depths; + private $originalCharacterEncoding; + + public function setOriginalCharacterEncoding($original_character_encoding) { + $this->originalCharacterEncoding = $original_character_encoding; + return $this; + } + + public function getOriginalCharacterEncoding() { + return $this->originalCharacterEncoding; + } public function setDepths($depths) { $this->depths = $depths; diff --git a/src/applications/differential/storage/DifferentialHunkLegacy.php b/src/applications/differential/storage/DifferentialHunkLegacy.php index d12219a8fd..7eedd770c1 100644 --- a/src/applications/differential/storage/DifferentialHunkLegacy.php +++ b/src/applications/differential/storage/DifferentialHunkLegacy.php @@ -8,4 +8,13 @@ final class DifferentialHunkLegacy extends DifferentialHunk { return 'differential_hunk'; } + public function getDataEncoding() { + return 'utf8'; + } + + public function forceEncoding($encoding) { + // Not supported, these are always utf8. + return $this; + } + } diff --git a/src/applications/differential/storage/DifferentialHunkModern.php b/src/applications/differential/storage/DifferentialHunkModern.php index 39168b3ebe..eae36a6f7f 100644 --- a/src/applications/differential/storage/DifferentialHunkModern.php +++ b/src/applications/differential/storage/DifferentialHunkModern.php @@ -14,6 +14,7 @@ final class DifferentialHunkModern extends DifferentialHunk { protected $data; private $rawData; + private $forcedEncoding; public function getTableName() { return 'differential_hunk_modern'; @@ -41,7 +42,12 @@ final class DifferentialHunkModern extends DifferentialHunk { public function getChanges() { return $this->getUTF8StringFromStorage( $this->getRawData(), - $this->getDataEncoding()); + nonempty($this->forcedEncoding, $this->getDataEncoding())); + } + + public function forceEncoding($encoding) { + $this->forcedEncoding = $encoding; + return $this; } public function save() { diff --git a/src/applications/differential/view/DifferentialChangesetListView.php b/src/applications/differential/view/DifferentialChangesetListView.php index 59f0fc108f..d16a2c1bd8 100644 --- a/src/applications/differential/view/DifferentialChangesetListView.php +++ b/src/applications/differential/view/DifferentialChangesetListView.php @@ -127,6 +127,11 @@ final class DifferentialChangesetListView extends AphrontView { 'Show Raw File (Left)' => pht('Show Raw File (Left)'), 'Show Raw File (Right)' => pht('Show Raw File (Right)'), 'Configure Editor' => pht('Configure Editor'), + 'Load Changes' => pht('Load Changes'), + 'View Side-by-Side' => pht('View Side-by-Side'), + 'View Unified' => pht('View Unified (Barely Works!)'), + 'Change Text Encoding...' => pht('Change Text Encoding...'), + 'Highlight As...' => pht('Highlight As...'), ), )); diff --git a/src/applications/diffusion/controller/DiffusionDiffController.php b/src/applications/diffusion/controller/DiffusionDiffController.php index 4541929f14..16e21ba09b 100644 --- a/src/applications/diffusion/controller/DiffusionDiffController.php +++ b/src/applications/diffusion/controller/DiffusionDiffController.php @@ -68,6 +68,9 @@ final class DiffusionDiffController extends DiffusionController { array( 'action' => 'rendering-ref'))); + $parser->setCharacterEncoding($request->getStr('encoding')); + $parser->setHighlightAs($request->getStr('highlight')); + $coverage = $drequest->loadCoverage(); if ($coverage) { $parser->setCoverage($coverage); diff --git a/src/applications/system/application/PhabricatorApplicationSystem.php b/src/applications/system/application/PhabricatorApplicationSystem.php index 0c642cc484..9d90f9667d 100644 --- a/src/applications/system/application/PhabricatorApplicationSystem.php +++ b/src/applications/system/application/PhabricatorApplicationSystem.php @@ -15,6 +15,10 @@ final class PhabricatorApplicationSystem extends PhabricatorApplication { '/status/' => 'PhabricatorStatusController', '/debug/' => 'PhabricatorDebugController', '/robots.txt' => 'PhabricatorRobotsController', + '/services/' => array( + 'encoding/' => 'PhabricatorSystemSelectEncodingController', + 'highlight/' => 'PhabricatorSystemSelectHighlightController', + ), ); } diff --git a/src/applications/system/controller/PhabricatorSystemSelectEncodingController.php b/src/applications/system/controller/PhabricatorSystemSelectEncodingController.php new file mode 100644 index 0000000000..4392f53ada --- /dev/null +++ b/src/applications/system/controller/PhabricatorSystemSelectEncodingController.php @@ -0,0 +1,55 @@ +getRequest(); + + if (!function_exists('mb_list_encodings')) { + return $this->newDialog() + ->setTitle(pht('No Encoding Support')) + ->appendParagraph( + pht( + 'This system does not have the "mbstring" extension installed, '. + 'so character encodings are not supported. Install "mbstring" to '. + 'enable support.')) + ->addCancelButton('/'); + } + + if ($request->isFormPost()) { + $result = array('encoding' => $request->getStr('encoding')); + return id(new AphrontAjaxResponse())->setContent($result); + } + + $encodings = mb_list_encodings(); + $encodings = array_fuse($encodings); + asort($encodings); + unset($encodings['pass']); + unset($encodings['auto']); + + $encodings = array( + '' => pht('(Use Default)'), + ) + $encodings; + + $form = id(new AphrontFormView()) + ->setUser($this->getRequest()->getUser()) + ->appendRemarkupInstructions(pht('Choose a text encoding to use.')) + ->appendChild( + id(new AphrontFormSelectControl()) + ->setLabel(pht('Encoding')) + ->setName('encoding') + ->setValue($request->getStr('encoding')) + ->setOptions($encodings)); + + return $this->newDialog() + ->setTitle(pht('Select Character Encoding')) + ->appendChild($form->buildLayoutView()) + ->addSubmitButton(pht('Choose Encoding')) + ->addCancelButton('/'); + } +} diff --git a/src/applications/system/controller/PhabricatorSystemSelectHighlightController.php b/src/applications/system/controller/PhabricatorSystemSelectHighlightController.php new file mode 100644 index 0000000000..631b6fd8f7 --- /dev/null +++ b/src/applications/system/controller/PhabricatorSystemSelectHighlightController.php @@ -0,0 +1,38 @@ +getRequest(); + + if ($request->isFormPost()) { + $result = array('highlight' => $request->getStr('highlight')); + return id(new AphrontAjaxResponse())->setContent($result); + } + + $languages = array( + '' => pht('(Use Default)'), + ) + PhabricatorEnv::getEnvConfig('pygments.dropdown-choices'); + + $form = id(new AphrontFormView()) + ->setUser($this->getRequest()->getUser()) + ->appendRemarkupInstructions(pht('Choose a syntax highlighting to use.')) + ->appendChild( + id(new AphrontFormSelectControl()) + ->setLabel(pht('Highlighting')) + ->setName('highlight') + ->setValue($request->getStr('highlight')) + ->setOptions($languages)); + + return $this->newDialog() + ->setTitle(pht('Select Syntax Highlighting')) + ->appendChild($form->buildLayoutView()) + ->addSubmitButton(pht('Choose Highlighting')) + ->addCancelButton('/'); + } +} diff --git a/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php b/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php index 0b96ba1682..8b286c5b3a 100644 --- a/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php +++ b/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php @@ -183,6 +183,29 @@ abstract class PhabricatorLiskDAO extends LiskDAO { if ($encoding == 'utf8') { return $string; } + + if (function_exists('mb_detect_encoding')) { + if (strlen($encoding)) { + $try_encodings = array( + $encoding, + ); + } else { + // TODO: This is pretty much a guess, and probably needs to be + // configurable in the long run. + $try_encodings = array( + 'JIS', + 'EUC-JP', + 'SJIS', + 'ISO-8859-1', + ); + } + + $guess = mb_detect_encoding($string, $try_encodings); + if ($guess) { + return mb_convert_encoding($string, 'UTF-8', $guess); + } + } + return phutil_utf8ize($string); } diff --git a/webroot/rsrc/js/application/differential/ChangesetViewManager.js b/webroot/rsrc/js/application/differential/ChangesetViewManager.js index 78aa0e25c4..3653de11be 100644 --- a/webroot/rsrc/js/application/differential/ChangesetViewManager.js +++ b/webroot/rsrc/js/application/differential/ChangesetViewManager.js @@ -22,6 +22,7 @@ JX.install('ChangesetViewManager', { this._whitespace = data.whitespace; this._renderer = data.renderer; this._highlight = data.highlight; + this._encoding = data.encoding; }, members: { @@ -35,6 +36,7 @@ JX.install('ChangesetViewManager', { _whitespace: null, _renderer: null, _highlight: null, + _encoding: null, /** @@ -120,8 +122,10 @@ JX.install('ChangesetViewManager', { var params = { ref: this._ref, - whitespace: this._whitespace, - renderer: this._getRenderer() + whitespace: this._whitespace || '', + renderer: this.getRenderer() || '', + highlight: this._highlight || '', + encoding: this._encoding || '' }; var workflow = new JX.Workflow(this._renderURI, params) @@ -160,8 +164,16 @@ JX.install('ChangesetViewManager', { return JX.Router.getInstance().getRoutableByKey(this._getRoutableKey()); }, + setRenderer: function(renderer) { + this._renderer = renderer; + return this; + }, + + getRenderer: function() { + if (this._renderer !== null) { + return this._renderer; + } - _getRenderer: function() { // TODO: This is a big pile of TODOs. // NOTE: If you load the page at one device resolution and then resize to @@ -176,6 +188,23 @@ JX.install('ChangesetViewManager', { return renderer; }, + setEncoding: function(encoding) { + this._encoding = encoding; + return this; + }, + + getEncoding: function() { + return this._encoding; + }, + + setHighlight: function(highlight) { + this._highlight = highlight; + return this; + }, + + getHighlight: function() { + return this._highlight; + }, _getNodeData: function() { return JX.Stratcom.getData(this._node); diff --git a/webroot/rsrc/js/application/differential/behavior-dropdown-menus.js b/webroot/rsrc/js/application/differential/behavior-dropdown-menus.js index 685cf1f92b..380c4d0ee9 100644 --- a/webroot/rsrc/js/application/differential/behavior-dropdown-menus.js +++ b/webroot/rsrc/js/application/differential/behavior-dropdown-menus.js @@ -4,6 +4,7 @@ * javelin-dom * javelin-util * javelin-stratcom + * javelin-workflow * phuix-dropdown-menu * phuix-action-list-view * phuix-action-view @@ -48,13 +49,18 @@ JX.behavior('differential-dropdown-menus', function(config) { var buildmenu = function(e) { var button = e.getNode('differential-view-options'); var data = JX.Stratcom.getData(button); - if (data.menu) { return; } e.prevent(); + var changeset = JX.DOM.findAbove( + button, + 'div', + 'differential-changeset'); + + var view = JX.ChangesetViewManager.getForNode(changeset); var menu = new JX.PHUIXDropdownMenu(button); var list = new JX.PHUIXActionListView(); @@ -103,12 +109,15 @@ JX.behavior('differential-dropdown-menus', function(config) { var up_item = new JX.PHUIXActionView() .setHandler(function(e) { - var changeset = JX.DOM.findAbove( - button, - 'div', - 'differential-changeset'); - - var view = JX.ChangesetViewManager.getForNode(changeset); + if (view.isLoaded()) { + var renderer = view.getRenderer(); + if (renderer == '1up') { + renderer = '2up'; + } else { + renderer = '1up'; + } + view.setRenderer(renderer); + } view.reload(); e.prevent(); @@ -116,22 +125,54 @@ JX.behavior('differential-dropdown-menus', function(config) { }); list.addItem(up_item); + var encoding_item = new JX.PHUIXActionView() + .setIcon('fa-font') + .setName(pht('Change Text Encoding...')) + .setHandler(function(e) { + var params = { + encoding: view.getEncoding() + }; + + new JX.Workflow('/services/encoding/', params) + .setHandler(function(r) { + view.setEncoding(r.encoding); + view.reload(); + }) + .start(); + + e.prevent(); + menu.close(); + }); + list.addItem(encoding_item); + + var highlight_item = new JX.PHUIXActionView() + .setIcon('fa-sun-o') + .setName(pht('Highlight As...')) + .setHandler(function(e) { + var params = { + highlight: view.getHighlight() + }; + + new JX.Workflow('/services/highlight/', params) + .setHandler(function(r) { + view.setHighlight(r.highlight); + view.reload(); + }) + .start(); + + e.prevent(); + menu.close(); + }); + list.addItem(highlight_item); + add_link('fa-arrow-left', pht('Show Raw File (Left)'), data.leftURI); add_link('fa-arrow-right', pht('Show Raw File (Right)'), data.rightURI); add_link('fa-pencil', pht('Open in Editor'), data.editor, true); add_link('fa-wrench', pht('Configure Editor'), data.editorConfigure); - menu.setContent(list.getNode()); menu.listen('open', function() { - var changeset = JX.DOM.findAbove( - button, - 'div', - 'differential-changeset'); - - var view = JX.ChangesetViewManager.getForNode(changeset); - // When the user opens the menu, check if there are any "Show More" // links in the changeset body. If there aren't, disable the "Show // Entire File" menu item since it won't change anything. @@ -155,16 +196,23 @@ JX.behavior('differential-dropdown-menus', function(config) { .setHandler(function(e) { e.prevent(); }); } - // TODO: This is temporary and just makes testing easier. It will do - // some mojo soon. + encoding_item.setDisabled(!view.isLoaded()); + highlight_item.setDisabled(!view.isLoaded()); + if (view.isLoaded()) { - up_item - .setIcon('fa-refresh') - .setName('Reload'); + if (view.getRenderer() == '2up') { + up_item + .setIcon('fa-list-alt') + .setName(pht('View Unified')); + } else { + up_item + .setIcon('fa-files-o') + .setName(pht('View Side-by-Side')); + } } else { up_item .setIcon('fa-refresh') - .setName('Load'); + .setName(pht('Load Changes')); } visible_item @@ -198,6 +246,7 @@ JX.behavior('differential-dropdown-menus', function(config) { } }); + data.menu = menu; menu.open(); };