You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@zeppelin.apache.org by mo...@apache.org on 2017/11/16 23:52:01 UTC

zeppelin git commit: [ZEPPELIN-2813] revisions comparator

Repository: zeppelin
Updated Branches:
  refs/heads/master 71a019e8b -> 7241348cf


[ZEPPELIN-2813] revisions comparator

### What is this PR for?
Sometimes need to see the difference between versions and to switch to another version and look for changes are not convenient (the page reloaded). This feature allows you to compare any two versions of the notebook.

### What type of PR is it?
Feature

### What is the Jira issue?
https://issues.apache.org/jira/browse/ZEPPELIN-2813

### How should this be tested?
1 make some commits. Сchange the contents of paragraphs (delete, add, edit)
2 open Revisions comparator
3 compare revisions and check diff

### Screenshots (if appropriate)
![comparator](https://user-images.githubusercontent.com/25951039/28702781-cf1cedce-7378-11e7-9034-7036f4440bf3.gif)

### Questions:
* Does the licenses files need update? yes (updated)
* Is there breaking changes for older versions? no
* Does this needs documentation? no

Author: tinkoff-dwh <ti...@gmail.com>
Author: Tinkoff DWH <ti...@gmail.com>

Closes #2506 from tinkoff-dwh/ZEPPELIN-2813 and squashes the following commits:

acc624f [tinkoff-dwh] Merge remote-tracking branch 'upstream/master' into ZEPPELIN-2813
2fd89a8 [tinkoff-dwh] Merge remote-tracking branch 'origin/master' into ZEPPELIN-2813
8b8afcc [tinkoff-dwh] Docs edit
efa7ce2 [tinkoff-dwh] Merge branch 'master' into ZEPPELIN-2813
f530524 [tinkoff-dwh] zep-2813 anim off
0e866b2 [tinkoff-dwh] zep_2813 color change
310760e [tinkoff-dwh] zep_2813 UI for REVISIONS COMPARATOR.
3d4f86c [tinkoff-dwh] Merge branch 'master' into ZEPPELIN-2813_refactoring_2
dc67f8f [tinkoff-dwh] [ZEPPELIN-2813] refactoring
514b3f5 [tinkoff-dwh] small fixes, added documentation
4ce5286 [tinkoff-dwh] Merge remote-tracking branch 'origin/master' into ZEPPELIN-2813
b949814 [Tinkoff DWH] [ZEPPELIN-2813] license
a192b95 [Tinkoff DWH] [ZEPPELIN-2813] revisions comparator for note


Project: http://git-wip-us.apache.org/repos/asf/zeppelin/repo
Commit: http://git-wip-us.apache.org/repos/asf/zeppelin/commit/7241348c
Tree: http://git-wip-us.apache.org/repos/asf/zeppelin/tree/7241348c
Diff: http://git-wip-us.apache.org/repos/asf/zeppelin/diff/7241348c

Branch: refs/heads/master
Commit: 7241348cff4ef061fbc716f836d2d2d503477008
Parents: 71a019e
Author: tinkoff-dwh <ti...@gmail.com>
Authored: Wed Nov 1 11:34:41 2017 +0300
Committer: Lee moon soo <mo...@apache.org>
Committed: Thu Nov 16 15:51:57 2017 -0800

----------------------------------------------------------------------
 LICENSE                                         |   2 +
 docs/_includes/themes/zeppelin/_navigation.html |   1 +
 .../revisions-comparator-comboboxes.png         | Bin 0 -> 2596 bytes
 .../img/docs-img/revisions-comparator-table.png | Bin 0 -> 16509 bytes
 .../docs-img/revisions_comparator_button.png    | Bin 0 -> 19092 bytes
 .../img/docs-img/revisions_comparator_diff.png  | Bin 0 -> 61883 bytes
 .../docs-img/revisions_comparator_paragraph.png | Bin 0 -> 66939 bytes
 docs/index.md                                   |   1 +
 docs/usage/other_features/notebook_actions.md   |  59 ++++
 licenses/LICENSE-jsdiff-3.3.0                   |  31 +++
 .../apache/zeppelin/socket/NotebookServer.java  |  28 +-
 zeppelin-web/bower.json                         |   3 +-
 zeppelin-web/karma.conf.js                      |   1 +
 .../src/app/notebook/notebook-actionBar.html    |   6 +
 .../src/app/notebook/notebook.controller.js     |  48 +++-
 zeppelin-web/src/app/notebook/notebook.html     |   8 +
 .../revisions-comparator.component.js           | 181 ++++++++++++
 .../revisions-comparator.css                    | 276 +++++++++++++++++++
 .../revisions-comparator.html                   | 124 +++++++++
 .../websocket/websocket-event.factory.js        |   2 +
 .../websocket/websocket-message.service.js      |  11 +
 zeppelin-web/src/index.html                     |   1 +
 zeppelin-web/src/index.js                       |   1 +
 .../zeppelin/notebook/socket/Message.java       |   4 +
 24 files changed, 778 insertions(+), 10 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/zeppelin/blob/7241348c/LICENSE
----------------------------------------------------------------------
diff --git a/LICENSE b/LICENSE
index a6c02de..142bd49 100644
--- a/LICENSE
+++ b/LICENSE
@@ -269,6 +269,8 @@ The following components are provided under the BSD 3-Clause license.  See file
   (BSD 3 Clause) portions of Scala (http://www.scala-lang.org/download) - http://www.scala-lang.org/download/#License
    r/src/main/scala/scala/Console.scala
 
+  (BSD 3 Clause) diff.js (https://github.com/kpdecker/jsdiff)
+
 ========================================================================
 BSD 2-Clause licenses
 ========================================================================

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/7241348c/docs/_includes/themes/zeppelin/_navigation.html
----------------------------------------------------------------------
diff --git a/docs/_includes/themes/zeppelin/_navigation.html b/docs/_includes/themes/zeppelin/_navigation.html
index 215c944..bccb5b4 100644
--- a/docs/_includes/themes/zeppelin/_navigation.html
+++ b/docs/_includes/themes/zeppelin/_navigation.html
@@ -61,6 +61,7 @@
                 <li><a href="{{BASE_PATH}}/usage/other_features/publishing_paragraphs.html">Publishing Paragraphs</a></li>
                 <li><a href="{{BASE_PATH}}/usage/other_features/personalized_mode.html">Personalized Mode</a></li>
                 <li><a href="{{BASE_PATH}}/usage/other_features/customizing_homepage.html">Customizing Zeppelin Homepage</a></li>
+                <li><a href="{{BASE_PATH}}/usage/other_features/notebook_actions.html">Notebook Actions</a></li>
                 <li role="separator" class="divider"></li>
                 <li class="title"><span>REST API</span></li>
                 <li><a href="{{BASE_PATH}}/usage/rest_api/interpreter.html">Interpreter API</a></li>

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/7241348c/docs/assets/themes/zeppelin/img/docs-img/revisions-comparator-comboboxes.png
----------------------------------------------------------------------
diff --git a/docs/assets/themes/zeppelin/img/docs-img/revisions-comparator-comboboxes.png b/docs/assets/themes/zeppelin/img/docs-img/revisions-comparator-comboboxes.png
new file mode 100644
index 0000000..7eae6a5
Binary files /dev/null and b/docs/assets/themes/zeppelin/img/docs-img/revisions-comparator-comboboxes.png differ

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/7241348c/docs/assets/themes/zeppelin/img/docs-img/revisions-comparator-table.png
----------------------------------------------------------------------
diff --git a/docs/assets/themes/zeppelin/img/docs-img/revisions-comparator-table.png b/docs/assets/themes/zeppelin/img/docs-img/revisions-comparator-table.png
new file mode 100644
index 0000000..6c7b8e6
Binary files /dev/null and b/docs/assets/themes/zeppelin/img/docs-img/revisions-comparator-table.png differ

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/7241348c/docs/assets/themes/zeppelin/img/docs-img/revisions_comparator_button.png
----------------------------------------------------------------------
diff --git a/docs/assets/themes/zeppelin/img/docs-img/revisions_comparator_button.png b/docs/assets/themes/zeppelin/img/docs-img/revisions_comparator_button.png
new file mode 100644
index 0000000..168809c
Binary files /dev/null and b/docs/assets/themes/zeppelin/img/docs-img/revisions_comparator_button.png differ

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/7241348c/docs/assets/themes/zeppelin/img/docs-img/revisions_comparator_diff.png
----------------------------------------------------------------------
diff --git a/docs/assets/themes/zeppelin/img/docs-img/revisions_comparator_diff.png b/docs/assets/themes/zeppelin/img/docs-img/revisions_comparator_diff.png
new file mode 100644
index 0000000..c1092e9
Binary files /dev/null and b/docs/assets/themes/zeppelin/img/docs-img/revisions_comparator_diff.png differ

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/7241348c/docs/assets/themes/zeppelin/img/docs-img/revisions_comparator_paragraph.png
----------------------------------------------------------------------
diff --git a/docs/assets/themes/zeppelin/img/docs-img/revisions_comparator_paragraph.png b/docs/assets/themes/zeppelin/img/docs-img/revisions_comparator_paragraph.png
new file mode 100644
index 0000000..c559c45
Binary files /dev/null and b/docs/assets/themes/zeppelin/img/docs-img/revisions_comparator_paragraph.png differ

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/7241348c/docs/index.md
----------------------------------------------------------------------
diff --git a/docs/index.md b/docs/index.md
index dbec040..8f3b551 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -72,6 +72,7 @@ limitations under the License.
   * [Publishing Paragraphs](./usage/other_features/publishing_paragraphs.html) results into your external website
   * [Personalized Mode](./usage/other_features/personalized_mode.html) 
   * [Customizing Zeppelin Homepage](./usage/other_features/customizing_homepage.html) with one of your notebooks
+  * [Notebook actions](./usage/other_features/notebook_actions.html)
 * REST API: available REST API list in Apache Zeppelin
   * [Interpreter API](./usage/rest_api/interpreter.html)
   * [Zeppelin Server API](./usage/rest_api/zeppelin_server.html)

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/7241348c/docs/usage/other_features/notebook_actions.md
----------------------------------------------------------------------
diff --git a/docs/usage/other_features/notebook_actions.md b/docs/usage/other_features/notebook_actions.md
new file mode 100644
index 0000000..36cbe9b
--- /dev/null
+++ b/docs/usage/other_features/notebook_actions.md
@@ -0,0 +1,59 @@
+---
+layout: page
+title: "Notebook Actions"
+description: "Description of some actions for notebooks"
+group: usage/other_features
+---
+<!--
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+{% include JB/setup %}
+
+# Revisions comparator
+
+<div id="toc"></div>
+
+Apache Zeppelin allows you to compare revisions of notebook.
+To see which paragraphs have been changed, removed or added.
+This action becomes available if your notebook has more than one revision.
+
+<center><img src="{{BASE_PATH}}/assets/themes/zeppelin/img/docs-img/revisions-comparator-button.png" height="90%" width="90%"></center>
+
+## How to compare two revisions
+
+For compare two revisions need open dialog of comparator (by click button) and click on any revision in the table.
+
+<center><img src="{{BASE_PATH}}/assets/themes/zeppelin/img/docs-img/revisions-comparator-table.png" height="90%" width="90%"></center>
+
+Or choose two revisions into comboboxes.
+
+<center><img src="{{BASE_PATH}}/assets/themes/zeppelin/img/docs-img/revisions-comparator-comboboxes.png" height="90%" width="90%"></center>
+
+After click on any revision in the table or selecting the second revision will see the result of the comparison.
+
+<center><img src="{{BASE_PATH}}/assets/themes/zeppelin/img/docs-img/revisions-comparator-diff.png" height="90%" width="90%"></center>
+
+## How to read the result of the comparison
+
+Result it is list of paragraphs which was in both revisions. If paragraph was added in second revision ("Head")
+then so it will be marked as <i style="color: green">added</i>, if was deleted then it will be marked as
+<i style="color: red">deleted</i>. If paragraph exists in both revisions then it marked as <i style="color: orange">there are differences</i>.
+To view the comparison click on the section.
+
+<center><img src="{{BASE_PATH}}/assets/themes/zeppelin/img/docs-img/revisions-comparator-paragraph.png" height="90%" width="90%"></center>
+
+Сhanges in the text of the paragraph are highlighted in green and red. Red it is line (block of lines) which was deleted, green it is line (block of lines) which was added).
+
+
+
+

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/7241348c/licenses/LICENSE-jsdiff-3.3.0
----------------------------------------------------------------------
diff --git a/licenses/LICENSE-jsdiff-3.3.0 b/licenses/LICENSE-jsdiff-3.3.0
new file mode 100644
index 0000000..4e7146e
--- /dev/null
+++ b/licenses/LICENSE-jsdiff-3.3.0
@@ -0,0 +1,31 @@
+Software License Agreement (BSD License)
+
+Copyright (c) 2009-2015, Kevin Decker <kp...@gmail.com>
+
+All rights reserved.
+
+Redistribution and use of this software in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above
+  copyright notice, this list of conditions and the
+  following disclaimer.
+
+* Redistributions in binary form must reproduce the above
+  copyright notice, this list of conditions and the
+  following disclaimer in the documentation and/or other
+  materials provided with the distribution.
+
+* Neither the name of Kevin Decker nor the names of its
+  contributors may be used to endorse or promote products
+  derived from this software without specific prior
+  written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
+IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
+FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
+IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
+OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/7241348c/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java
----------------------------------------------------------------------
diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java b/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java
index e3fb004..a3e8714 100644
--- a/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java
+++ b/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java
@@ -29,8 +29,6 @@ import java.util.regex.Pattern;
 
 import javax.servlet.http.HttpServletRequest;
 
-import com.google.common.base.Strings;
-import com.google.common.collect.Sets;
 import org.apache.commons.lang.StringUtils;
 import org.apache.commons.vfs2.FileSystemException;
 import org.apache.zeppelin.conf.ZeppelinConfiguration;
@@ -45,8 +43,8 @@ import org.apache.zeppelin.interpreter.*;
 import org.apache.zeppelin.interpreter.remote.RemoteAngularObjectRegistry;
 import org.apache.zeppelin.interpreter.remote.RemoteInterpreterProcessListener;
 import org.apache.zeppelin.interpreter.thrift.InterpreterCompletion;
-import org.apache.zeppelin.notebook.JobListenerFactory;
 import org.apache.zeppelin.notebook.Folder;
+import org.apache.zeppelin.notebook.JobListenerFactory;
 import org.apache.zeppelin.notebook.Note;
 import org.apache.zeppelin.notebook.Notebook;
 import org.apache.zeppelin.notebook.NotebookAuthorization;
@@ -77,7 +75,9 @@ import org.quartz.SchedulerException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.google.common.base.Strings;
 import com.google.common.collect.Queues;
+import com.google.common.collect.Sets;
 import com.google.gson.Gson;
 import com.google.gson.GsonBuilder;
 import com.google.gson.reflect.TypeToken;
@@ -332,6 +332,9 @@ public class NotebookServer extends WebSocketServlet
         case NOTE_REVISION:
           getNoteByRevision(conn, notebook, messagereceived);
           break;
+        case NOTE_REVISION_FOR_COMPARE:
+          getNoteByRevisionForCompare(conn, notebook, messagereceived);
+          break;
         case LIST_NOTE_JOBS:
           unicastNoteJobInfo(conn, messagereceived);
           break;
@@ -1943,6 +1946,25 @@ public class NotebookServer extends WebSocketServlet
             .put("note", revisionNote)));
   }
 
+  private void getNoteByRevisionForCompare(NotebookSocket conn, Notebook notebook,
+      Message fromMessage) throws IOException {
+    String noteId = (String) fromMessage.get("noteId");
+    String revisionId = (String) fromMessage.get("revisionId");
+
+    String position = (String) fromMessage.get("position");
+    AuthenticationInfo subject = new AuthenticationInfo(fromMessage.principal);
+    Note revisionNote;
+    if (revisionId.equals("Head")) {
+      revisionNote = notebook.getNote(noteId);
+    } else {
+      revisionNote = notebook.getNoteByRevision(noteId, revisionId, subject);
+    }
+
+    conn.send(serializeMessage(
+        new Message(OP.NOTE_REVISION_FOR_COMPARE).put("noteId", noteId)
+            .put("revisionId", revisionId).put("position", position).put("note", revisionNote)));
+  }
+
   /**
    * This callback is for the paragraph that runs on ZeppelinServer
    *

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/7241348c/zeppelin-web/bower.json
----------------------------------------------------------------------
diff --git a/zeppelin-web/bower.json b/zeppelin-web/bower.json
index a68c2e9..2b5135f 100644
--- a/zeppelin-web/bower.json
+++ b/zeppelin-web/bower.json
@@ -32,7 +32,8 @@
     "bootstrap3-dialog": "bootstrap-dialog#~1.34.7",
     "select2": "^4.0.3",
     "MathJax": "2.7.0",
-    "ngclipboard": "^1.1.1"
+    "ngclipboard": "^1.1.1",
+    "jsdiff": "3.3.0"
   },
   "devDependencies": {
     "angular-mocks": "1.5.7"

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/7241348c/zeppelin-web/karma.conf.js
----------------------------------------------------------------------
diff --git a/zeppelin-web/karma.conf.js b/zeppelin-web/karma.conf.js
index 1c79346..8a03bec 100644
--- a/zeppelin-web/karma.conf.js
+++ b/zeppelin-web/karma.conf.js
@@ -86,6 +86,7 @@ module.exports = function(config) {
       'bower_components/MathJax/MathJax.js',
       'bower_components/clipboard/dist/clipboard.js',
       'bower_components/ngclipboard/dist/ngclipboard.js',
+      'bower_components/jsdiff/diff.js',
       'bower_components/angular-mocks/angular-mocks.js',
       // endbower
 

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/7241348c/zeppelin-web/src/app/notebook/notebook-actionBar.html
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/app/notebook/notebook-actionBar.html b/zeppelin-web/src/app/notebook/notebook-actionBar.html
index b121fee..0db4ff0 100644
--- a/zeppelin-web/src/app/notebook/notebook-actionBar.html
+++ b/zeppelin-web/src/app/notebook/notebook-actionBar.html
@@ -143,6 +143,12 @@ limitations under the License.
             </div>
           </li>
         </ul>
+        <button type="button"
+                class="btn btn-default btn-xs"
+                ng-click="toggleRevisionsComparator()"
+                tooltip-placement="bottom" uib-tooltip="Compare revisions">
+          <i class="fa fa-exchange"></i>
+        </button>
       </div>
       <div class="btn-group" role="group">
         <button type="button" class="btn btn-default btn-xs revisionName" title="{{currentRevision}}">

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/7241348c/zeppelin-web/src/app/notebook/notebook.controller.js
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/app/notebook/notebook.controller.js b/zeppelin-web/src/app/notebook/notebook.controller.js
index bdd47c2..456e463 100644
--- a/zeppelin-web/src/app/notebook/notebook.controller.js
+++ b/zeppelin-web/src/app/notebook/notebook.controller.js
@@ -31,6 +31,7 @@ function NotebookCtrl ($scope, $route, $routeParams, $location, $rootScope,
   $scope.tableToggled = false
   $scope.viewOnly = false
   $scope.showSetting = false
+  $scope.showRevisionsComparator = false
   $scope.looknfeelOption = ['default', 'simple', 'report']
   $scope.cronOption = [
     {name: 'None', value: undefined},
@@ -247,13 +248,24 @@ function NotebookCtrl ($scope, $route, $routeParams, $location, $rootScope,
     })
   }
 
+  $scope.preVisibleRevisionsComparator = function() {
+    $scope.mergeNoteRevisionsForCompare = null
+    $scope.firstNoteRevisionForCompare = null
+    $scope.secondNoteRevisionForCompare = null
+    $scope.currentFirstRevisionForCompare = 'Choose...'
+    $scope.currentSecondRevisionForCompare = 'Choose...'
+    $scope.$apply()
+  }
+
   $scope.$on('listRevisionHistory', function (event, data) {
     console.debug('received list of revisions %o', data)
     $scope.noteRevisions = data.revisionList
-    $scope.noteRevisions.splice(0, 0, {
-      id: 'Head',
-      message: 'Head'
-    })
+    if ($scope.noteRevisions.length === 0 || $scope.noteRevisions[0].id !== 'Head') {
+      $scope.noteRevisions.splice(0, 0, {
+        id: 'Head',
+        message: 'Head'
+      })
+    }
     if ($routeParams.revisionId) {
       let index = _.findIndex($scope.noteRevisions, {'id': $routeParams.revisionId})
       if (index > -1) {
@@ -579,6 +591,12 @@ function NotebookCtrl ($scope, $route, $routeParams, $location, $rootScope,
     orderChanged: function (event) {}
   }
 
+  $scope.closeAdditionalBoards = function() {
+    $scope.closeSetting()
+    $scope.closePermissions()
+    $scope.closeRevisionsComparator()
+  }
+
   $scope.openSetting = function () {
     $scope.showSetting = true
     getInterpreterBindings()
@@ -628,8 +646,26 @@ function NotebookCtrl ($scope, $route, $routeParams, $location, $rootScope,
     if ($scope.showSetting) {
       $scope.closeSetting()
     } else {
+      $scope.closeAdditionalBoards()
       $scope.openSetting()
-      $scope.closePermissions()
+      angular.element('html, body').animate({ scrollTop: 0 }, 'slow')
+    }
+  }
+
+  $scope.openRevisionsComparator = function () {
+    $scope.showRevisionsComparator = true
+  }
+
+  $scope.closeRevisionsComparator = function () {
+    $scope.showRevisionsComparator = false
+  }
+
+  $scope.toggleRevisionsComparator = function () {
+    if ($scope.showRevisionsComparator) {
+      $scope.closeRevisionsComparator()
+    } else {
+      $scope.closeAdditionalBoards()
+      $scope.openRevisionsComparator()
       angular.element('html, body').animate({ scrollTop: 0 }, 'slow')
     }
   }
@@ -1064,8 +1100,8 @@ function NotebookCtrl ($scope, $route, $routeParams, $location, $rootScope,
         angular.element('#selectRunners').select2({})
         angular.element('#selectWriters').select2({})
       } else {
+        $scope.closeAdditionalBoards()
         $scope.openPermissions()
-        $scope.closeSetting()
       }
     }
   }

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/7241348c/zeppelin-web/src/app/notebook/notebook.html
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/app/notebook/notebook.html b/zeppelin-web/src/app/notebook/notebook.html
index 4b1c0b9..9441f6e 100644
--- a/zeppelin-web/src/app/notebook/notebook.html
+++ b/zeppelin-web/src/app/notebook/notebook.html
@@ -14,6 +14,14 @@ limitations under the License.
 <!-- Here the controller <NotebookCtrl> is not needed because explicitly set in the app.js (route) -->
 <div id="actionbar" ng-include src="'app/notebook/notebook-actionBar.html'"></div>
 <div id="content" class="notebookContent">
+  <!-- revisions comparator-->
+  <div ng-if="showRevisionsComparator" class="revisions-comparator">
+    <div>
+      <h4>Revisions comparator</h4>
+    </div>
+    <hr />
+    <revisions-comparator note-revisions="noteRevisions"></revisions-comparator>
+  </div>
   <!-- settings -->
   <div ng-if="showSetting" class="setting">
     <div>

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/7241348c/zeppelin-web/src/app/notebook/revisions-comparator/revisions-comparator.component.js
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/app/notebook/revisions-comparator/revisions-comparator.component.js b/zeppelin-web/src/app/notebook/revisions-comparator/revisions-comparator.component.js
new file mode 100644
index 0000000..45db38a
--- /dev/null
+++ b/zeppelin-web/src/app/notebook/revisions-comparator/revisions-comparator.component.js
@@ -0,0 +1,181 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import revisionsComparatorTemplate from './revisions-comparator.html'
+import './revisions-comparator.css'
+import moment from 'moment'
+
+function RevisionsComparatorController($scope, websocketMsgSrv, $routeParams) {
+  'ngInject'
+
+  $scope.firstNoteRevisionForCompare = null
+  $scope.secondNoteRevisionForCompare = null
+  $scope.mergeNoteRevisionsForCompare = null
+  $scope.currentParagraphDiffDisplay = null
+  $scope.currentFirstRevisionForCompare = 'Choose...'
+  $scope.currentSecondRevisionForCompare = 'Choose...'
+
+  $scope.getNoteRevisionForReview = function (revision, position) {
+    if (position) {
+      if (position === 'first') {
+        $scope.currentFirstRevisionForCompare = revision.message
+      } else {
+        $scope.currentSecondRevisionForCompare = revision.message
+      }
+      websocketMsgSrv.getNoteByRevisionForCompare($routeParams.noteId, revision.id, position)
+    }
+  }
+
+  // compare revisions
+  $scope.compareRevisions = function () {
+    if ($scope.firstNoteRevisionForCompare && $scope.secondNoteRevisionForCompare) {
+      let paragraphs1 = $scope.firstNoteRevisionForCompare.note.paragraphs
+      let paragraphs2 = $scope.secondNoteRevisionForCompare.note.paragraphs
+      let added = 'added'
+      let deleted = 'deleted'
+      let compared = 'compared'
+      let merge = []
+      for (let p1 of paragraphs1) {
+        let p2 = null
+        for (let p of paragraphs2) {
+          if (p1.id === p.id) {
+            p2 = p
+            break
+          }
+        }
+        if (p2 === null) {
+          merge.push({paragraph: p1, firstString: (p1.text || '').split('\n')[0], type: deleted})
+        } else {
+          let colorClass = ''
+          let span = null
+          let text1 = p1.text || ''
+          let text2 = p2.text || ''
+
+          let diff = window.JsDiff.diffLines(text1, text2)
+          let diffHtml = document.createDocumentFragment()
+          let identical = true
+          let identicalClass = 'color-black'
+
+          diff.forEach(function (part) {
+            colorClass = part.added ? 'color-green-row' : part.removed ? 'color-red-row' : identicalClass
+            span = document.createElement('span')
+            span.className = colorClass
+            if (identical && colorClass !== identicalClass) {
+              identical = false
+            }
+
+            let str = part.value
+
+            if (str[str.length - 1] !== '\n') {
+              str = str + '\n'
+            }
+
+            span.appendChild(document.createTextNode(str))
+            diffHtml.appendChild(span)
+          })
+
+          let pre = document.createElement('pre')
+          pre.appendChild(diffHtml)
+
+          merge.push(
+            {
+              paragraph: p1,
+              diff: pre.innerHTML,
+              identical: identical,
+              firstString: (p1.text || '').split('\n')[0],
+              type: compared
+            })
+        }
+      }
+
+      for (let p2 of paragraphs2) {
+        let p1 = null
+        for (let p of paragraphs1) {
+          if (p2.id === p.id) {
+            p1 = p
+            break
+          }
+        }
+        if (p1 === null) {
+          merge.push({paragraph: p2, firstString: (p2.text || '').split('\n')[0], type: added})
+        }
+      }
+
+      merge.sort(function (a, b) {
+        if (a.type === added) {
+          return -1
+        }
+        if (a.type === compared) {
+          return 1
+        }
+        if (a.type === deleted) {
+          if (b.type === compared) {
+            return -1
+          } else {
+            return 1
+          }
+        }
+      })
+
+      $scope.mergeNoteRevisionsForCompare = merge
+
+      if ($scope.currentParagraphDiffDisplay !== null) {
+        $scope.changeCurrentParagraphDiffDisplay($scope.currentParagraphDiffDisplay.paragraph.id)
+      }
+    }
+  }
+
+  $scope.$on('noteRevisionForCompare', function (event, data) {
+    console.debug('received note revision for compare %o', data)
+    if (data.note && data.position) {
+      if (data.position === 'first') {
+        $scope.firstNoteRevisionForCompare = data
+      } else {
+        $scope.secondNoteRevisionForCompare = data
+      }
+
+      if ($scope.firstNoteRevisionForCompare !== null && $scope.secondNoteRevisionForCompare !== null &&
+        $scope.firstNoteRevisionForCompare.revisionId !== $scope.secondNoteRevisionForCompare.revisionId) {
+        $scope.compareRevisions()
+      }
+    }
+  })
+
+  $scope.formatRevisionDate = function (date) {
+    return moment.unix(date).format('MMMM Do YYYY, h:mm:ss a')
+  }
+
+  $scope.changeCurrentParagraphDiffDisplay = function (paragraphId) {
+    for (let p of $scope.mergeNoteRevisionsForCompare) {
+      if (p.paragraph.id === paragraphId) {
+        $scope.currentParagraphDiffDisplay = p
+        return
+      }
+    }
+    $scope.currentParagraphDiffDisplay = null
+  }
+}
+
+export const RevisionsComparatorComponent = {
+  template: revisionsComparatorTemplate,
+  controller: RevisionsComparatorController,
+  bindings: {
+    noteRevisions: '<'
+  }
+}
+
+export const RevisionsComparatorModule = angular
+  .module('zeppelinWebApp')
+  .component('revisionsComparator', RevisionsComparatorComponent)
+  .name

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/7241348c/zeppelin-web/src/app/notebook/revisions-comparator/revisions-comparator.css
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/app/notebook/revisions-comparator/revisions-comparator.css b/zeppelin-web/src/app/notebook/revisions-comparator/revisions-comparator.css
new file mode 100644
index 0000000..3ec60c4
--- /dev/null
+++ b/zeppelin-web/src/app/notebook/revisions-comparator/revisions-comparator.css
@@ -0,0 +1,276 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+.revisions-comparator {
+  background: white;
+  padding: 10px 15px 15px 15px;
+  margin-left: -10px;
+  margin-right: -10px;
+  font-family: 'Roboto', sans-serif;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
+  border-bottom: 1px solid #E5E5E5;
+}
+
+.revisions-comparator-panel {
+  transition-property: border, left, background-color;
+  transition-duration: 250ms, 500ms, 200ms;
+  transition-timing-function: ease-out;
+  position: relative;
+  left: 0;
+  width: 95%;
+  background-color: rgba(255, 255, 0, 0.10);
+  border: 1px solid rgba(120, 129, 82, 0.08);
+  margin-right: 15px;
+  min-width: 270px;
+}
+
+.revisions-comparator-panel:first-child {
+  margin-top: 5px;
+}
+
+.revisions-comparator-panel:last-child {
+  margin-bottom: 5px;
+}
+
+.revisions-comparator-panel:hover {
+  border: 1px solid rgba(55, 54, 35, 0.35);
+  background-color: rgba(255, 204, 0, 0.15);
+}
+
+.revisions-comparator-panel-selected {
+  background-color: rgba(252, 255, 0, 0.53) !important;
+  border: 1px solid rgba(55, 54, 35, 0.49)
+}
+
+.revisions-comparator-panel-heading {
+  padding: 10px;
+  cursor: pointer;
+}
+
+.revisions-comparator-panel.ng-enter,
+.revisions-comparator-panel.ng-enter.ng-enter-active ,
+.revisions-comparator-panel.ng-leave,
+.revisions-comparator-panel.ng-leave-active {
+  transition-duration: 0s !important;
+}
+
+.cursor-hand {
+  cursor: pointer;
+}
+
+.paragraphs-div {
+  overflow: auto;
+  height: 35vh;
+}
+
+.paragraphs-div-border {
+  border: 1px solid black;
+  border-radius: 10px;
+  padding: 10px;
+}
+
+.commit-tree {
+  width: 100%;
+  margin-right: 0;
+  margin-bottom: 10px;
+  border: 2px solid grey;
+  border-radius: 5px !important;
+}
+
+.commit-rows {
+  height: 30vh;
+  overflow: auto;
+  display: block;
+  width: 100%;
+}
+
+.commit-rows tr:nth-child(even) {
+  background-color: rgba(128, 128, 128, 0.06);
+}
+
+.commit-tree tr:hover {
+  background-color: rgba(48, 113, 169, 0.21);
+}
+
+.selected-revision {
+  background-color: rgba(48, 113, 169, 0.47) !important;
+}
+
+.commit-tree table {
+  border-collapse: collapse;
+  table-layout: fixed;
+  padding: 2px;
+  margin-bottom: 0px;
+}
+
+.commit-tree tr {
+  width: 100%;
+}
+
+.commit-tree td:nth-child(1),
+.commit-tree th:nth-child(1) {
+  width: 10%;
+}
+
+.commit-tree td:nth-child(2),
+.commit-tree th:nth-child(2) {
+  width: 20%;
+}
+
+.commit-tree th{
+  font-weight: normal;
+  font-size: 1.2em;
+  background-color: #317bb4;
+  color: rgb(255, 255, 255);
+  text-align: center;
+}
+
+.commit-tree .commit-rows td{
+  padding-left: 15px;
+}
+
+.commit-tree thead tr {
+  display: block;
+  position: relative;
+}
+
+.empty-code-panel {
+  text-align: center;
+  padding-top: 25%;
+  font-size: 30px;
+  color: grey;
+}
+
+.empty-paragraph-message {
+  font-size: 2em;
+  color: grey;
+  margin: 0 auto;
+  text-align: center;
+  display: table-cell;
+  vertical-align: middle;
+}
+
+.revisions-comparator-bar {
+  margin-left: 25px;
+
+}
+
+.revisions-comparator-bar .btn-group {
+  margin: 0 20px;
+}
+
+.revisions-comparator-code-panel {
+  display: block;
+  clear: both;
+  width: 95%;
+  float: left;
+  height: 70vh;
+  overflow-y: auto;
+}
+
+.revisions-comparator-code-panel-title {
+  width: 50%;
+  float: left;
+  font-size: 14px;
+  padding: 5px;
+}
+
+.revisions-comparator-bar {
+  width: 400px;
+  padding-bottom: 10px;
+}
+
+.revisions-comparator-status {
+  font-size: 12px;
+  padding-left: 10px;
+}
+
+#diffPanel {
+  height: 100%;
+  padding-left: 10px;
+  border: 2px solid grey;
+  border-radius: 5px !important;
+}
+
+#diffPanel .panel-group {
+  height: inherit;
+  overflow: auto;
+}
+
+.revision-name-for-compare {
+  cursor: default;
+  overflow: hidden;
+  vertical-align: bottom;
+  display: inline-block;
+  max-width: 100px;
+  padding: 1px 5px;
+}
+
+.revisions-comparator-caret {
+  padding-bottom: 5px;
+}
+
+.revisions-comparator-link, .revisions-comparator-link:hover,
+.revisions-comparator-link:visited, .revisions-comparator-link:focus {
+  text-decoration: none;
+  outline: none;
+  color: #000;
+}
+
+.revisions-comparator-first-string {
+  display: block;
+  height: 2em;
+  overflow: hidden;
+  padding-top: 6px;
+  padding-left: 5px;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+  font-size: 12px;
+  color: grey;
+}
+
+.revisions-comparator-dropdown {
+  padding: 5px;
+  font-size: 12px;
+  border-radius: 3px;
+}
+
+.color-green-row {
+  background-color: rgba(0, 226, 0, 0.22);
+  display: block;
+  color: green;
+}
+
+.color-red-row {
+  background-color: rgba(226, 0, 0, 0.22);
+  display: block;
+  color: red;
+}
+
+.color-green {
+  color: green;
+}
+
+.color-red {
+  color: red;
+}
+
+.color-black {
+  color: black;
+}
+
+.color-orange {
+  color: orange;
+}
+

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/7241348c/zeppelin-web/src/app/notebook/revisions-comparator/revisions-comparator.html
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/app/notebook/revisions-comparator/revisions-comparator.html b/zeppelin-web/src/app/notebook/revisions-comparator/revisions-comparator.html
new file mode 100644
index 0000000..37cdc5f
--- /dev/null
+++ b/zeppelin-web/src/app/notebook/revisions-comparator/revisions-comparator.html
@@ -0,0 +1,124 @@
+<!--
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<div class="col-md-4">
+
+  <div class="commit-tree">
+    <table class="table">
+      <thead>
+      <tr>
+        <th>Revision name</th>
+        <th>Date</th>
+      </tr>
+      </thead>
+      <tbody class="commit-rows">
+      <tr ng-repeat="revision in $ctrl.noteRevisions | orderBy:'time':true"
+          ng-class="{'cursor-hand': !$last, 'selected-revision': revision.message === currentSecondRevisionForCompare}"
+          ng-click="getNoteRevisionForReview($ctrl.noteRevisions[$index + 1], 'first'); getNoteRevisionForReview(revision, 'second')">
+        <td>{{revision.message}}</td>
+        <td>{{formatRevisionDate(revision.time)}}</td>
+      </tr>
+      </tbody>
+    </table>
+  </div>
+
+  <div class="revisions-comparator-bar">
+    <div class="btn-group">
+      <button type="button" ng-if="$ctrl.noteRevisions.length > 0"
+              class="btn btn-default revisions-comparator-dropdown dropdown-toggle"
+              data-toggle="dropdown" id="firstRevisionDropdown" title="{{currentFirstRevisionForCompare}}">
+        <div class="revision-name-for-compare">{{currentFirstRevisionForCompare}}</div>
+        <span class="caret revisions-comparator-caret"></span>
+      </button>
+      <ul class="dropdown-menu dropdown-menu-left" aria-labelledby="firstRevisionDropdown">
+        <li></li>
+        <li ng-repeat="revision in $ctrl.noteRevisions | orderBy:'time':true" class="revision">
+          <a style="cursor:pointer" ng-click="getNoteRevisionForReview(revision, 'first')">
+          <span style="display: block;">
+            <strong>{{revision.message}}</strong>
+          </span>
+            <span class="revisionDate">
+            <em>{{formatRevisionDate(revision.time)}}</em>
+          </span>
+          </a>
+        </li>
+      </ul>
+    </div>
+    <span>compare with</span>
+    <div class="btn-group">
+      <button type="button" ng-if="$ctrl.noteRevisions.length > 0"
+              class="btn btn-default revisions-comparator-dropdown dropdown-toggle"
+              ng-disabled="firstNoteRevisionForCompare === null"
+              data-toggle="dropdown" id="secondRevisionDropdown" title="{{currentSecondRevisionForCompare}}">
+        <div class="revision-name-for-compare">{{currentSecondRevisionForCompare}}</div>
+        <span class="caret revisions-comparator-caret"></span>
+      </button>
+      <ul class="dropdown-menu dropdown-menu-left" aria-labelledby="secondRevisionDropdown">
+        <li ng-repeat="revision in $ctrl.noteRevisions | orderBy:'time':true" class="revision">
+          <a style="cursor:pointer" ng-click="getNoteRevisionForReview(revision, 'second')">
+          <span style="display: block;">
+            <strong>{{revision.message}}</strong>
+          </span>
+            <span class="revisionDate">
+            <em>{{formatRevisionDate(revision.time)}}</em>
+          </span>
+          </a>
+        </li>
+      </ul>
+    </div>
+  </div>
+
+  <div id="diffPanel">
+    <div class="panel-group" style="margin-bottom: 0px">
+      <div class="paragraphs-div">
+        <div class="panel revisions-comparator-panel" data-ng-repeat="p in mergeNoteRevisionsForCompare | orderBy:'time':true"
+             ng-class="{'revisions-comparator-panel-selected' : currentParagraphDiffDisplay.paragraph.id === p.paragraph.id}">
+          <div class="revisions-comparator-panel-heading"
+               ng-click="changeCurrentParagraphDiffDisplay(p.paragraph.id)">
+            <h4 class="panel-title">
+              {{p.paragraph.id}}<strong style="padding: 5px;" ng-if="p.paragraph.title">({{p.paragraph.title}})</strong>
+              <i ng-if="p.type === 'added'" class="revisions-comparator-status color-green">added</i>
+              <i ng-if="p.type === 'deleted'" class="revisions-comparator-status color-red">deleted</i>
+              <i ng-if="p.type === 'compared' && !(p.identical)" class="revisions-comparator-status color-orange">there
+                are differences</i>
+              <i ng-if="p.type === 'compared' && p.identical" class="revisions-comparator-status">contents are
+                identical</i>
+              <i class="revisions-comparator-first-string">{{p.firstString}}</i>
+            </h4>
+          </div>
+        </div>
+        <div style="display: table; width: 100%; height: 100%"
+             ng-if="currentSecondRevisionForCompare === 'Choose...'">
+          <div class="empty-paragraph-message">
+            Please select a revision
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+
+<div id="revisions-comparator-code-panel-id center-block" class="panel col-md-8">
+  <span
+    class="revisions-comparator-code-panel-title">Revision: <strong>{{currentFirstRevisionForCompare}} --> {{currentSecondRevisionForCompare}}</strong></span>
+  <pre ng-if="currentParagraphDiffDisplay.type === 'added'"
+       class="revisions-comparator-code-panel color-green-row">{{currentParagraphDiffDisplay.paragraph.text}}</pre>
+  <pre ng-if="currentParagraphDiffDisplay.type === 'deleted'"
+       class="revisions-comparator-code-panel color-red-row">{{currentParagraphDiffDisplay.paragraph.text}}</pre>
+  <pre ng-if="currentParagraphDiffDisplay.type === 'compared'"
+       class="revisions-comparator-code-panel" ng-bind-html="currentParagraphDiffDisplay.diff"></pre>
+  <pre ng-if="currentParagraphDiffDisplay === null"
+       class="revisions-comparator-code-panel empty-code-panel"><div>Nothing to display</div></pre>
+</div>
+
+<div class="clearfix"></div>

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/7241348c/zeppelin-web/src/components/websocket/websocket-event.factory.js
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/components/websocket/websocket-event.factory.js b/zeppelin-web/src/components/websocket/websocket-event.factory.js
index 10cfd9c..d4bfadf 100644
--- a/zeppelin-web/src/components/websocket/websocket-event.factory.js
+++ b/zeppelin-web/src/components/websocket/websocket-event.factory.js
@@ -138,6 +138,8 @@ function WebsocketEventFactory ($rootScope, $websocket, $location, baseUrlSrv) {
       $rootScope.$broadcast('listRevisionHistory', data)
     } else if (op === 'NOTE_REVISION') {
       $rootScope.$broadcast('noteRevision', data)
+    } else if (op === 'NOTE_REVISION_FOR_COMPARE') {
+      $rootScope.$broadcast('noteRevisionForCompare', data)
     } else if (op === 'INTERPRETER_BINDINGS') {
       $rootScope.$broadcast('interpreterBindings', data)
     } else if (op === 'ERROR_INFO') {

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/7241348c/zeppelin-web/src/components/websocket/websocket-message.service.js
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/components/websocket/websocket-message.service.js b/zeppelin-web/src/components/websocket/websocket-message.service.js
index cafc61b..ab97fa8 100644
--- a/zeppelin-web/src/components/websocket/websocket-message.service.js
+++ b/zeppelin-web/src/components/websocket/websocket-message.service.js
@@ -295,6 +295,17 @@ function WebsocketMessageService ($rootScope, websocketEvents) {
       })
     },
 
+    getNoteByRevisionForCompare: function (noteId, revisionId, position) {
+      websocketEvents.sendNewEvent({
+        op: 'NOTE_REVISION_FOR_COMPARE',
+        data: {
+          noteId: noteId,
+          revisionId: revisionId,
+          position: position
+        }
+      })
+    },
+
     getEditorSetting: function (paragraphId, replName) {
       websocketEvents.sendNewEvent({
         op: 'EDITOR_SETTING',

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/7241348c/zeppelin-web/src/index.html
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/index.html b/zeppelin-web/src/index.html
index 4b43179..9a126f1 100644
--- a/zeppelin-web/src/index.html
+++ b/zeppelin-web/src/index.html
@@ -165,6 +165,7 @@ limitations under the License.
     <script src="bower_components/MathJax/MathJax.js"></script>
     <script src="bower_components/clipboard/dist/clipboard.js"></script>
     <script src="bower_components/ngclipboard/dist/ngclipboard.js"></script>
+    <script src="bower_components/jsdiff/diff.js"></script>
     <!-- endbower -->
     <!-- endbuild -->
   </body>

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/7241348c/zeppelin-web/src/index.js
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/index.js b/zeppelin-web/src/index.js
index 3cf052b..ed8f1d8 100644
--- a/zeppelin-web/src/index.js
+++ b/zeppelin-web/src/index.js
@@ -42,6 +42,7 @@ import './app/interpreter/interpreter-item.directive.js'
 import './app/interpreter/widget/number-widget.directive.js'
 import './app/credential/credential.controller.js'
 import './app/configuration/configuration.controller.js'
+import './app/notebook/revisions-comparator/revisions-comparator.component.js'
 import './app/notebook/paragraph/paragraph.controller.js'
 import './app/notebook/paragraph/clipboard.controller.js'
 import './app/notebook/paragraph/resizable.directive.js'

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/7241348c/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/socket/Message.java
----------------------------------------------------------------------
diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/socket/Message.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/socket/Message.java
index 06c83e1..d99bd59 100644
--- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/socket/Message.java
+++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/socket/Message.java
@@ -152,6 +152,10 @@ public class Message implements JsonSerializable {
     SET_NOTE_REVISION,            // [c-s] set current notebook head to this revision
                                   // @param noteId
                                   // @param revisionId
+    NOTE_REVISION_FOR_COMPARE,    // [c-s] get certain revision of note for compare
+                                  // @param noteId
+                                  // @param revisionId
+                                  // @param position
     APP_APPEND_OUTPUT,            // [s-c] append output
     APP_UPDATE_OUTPUT,            // [s-c] update (replace) output
     APP_LOAD,                     // [s-c] on app load