bisect-kit: output candidates information in json

diagnose_cros_autotest.py and bisectors can use 'view --json' subcommand
to dump candidates information in machine readable format.

BUG=b:117860228
TEST=unittest; run diagnose_cros_autotest.py end-to-end test manually

Change-Id: Iafcfc79f6331575b5cbca2fb0e98ac1d577a1b46
Reviewed-on: https://chromium-review.googlesource.com/1337352
Commit-Ready: Kuang-che Wu <kcwu@chromium.org>
Tested-by: Kuang-che Wu <kcwu@chromium.org>
Reviewed-by: Chung-yih Wang <cywang@chromium.org>
diff --git a/bisect_kit/codechange.py b/bisect_kit/codechange.py
index 30950df..15685ee 100644
--- a/bisect_kit/codechange.py
+++ b/bisect_kit/codechange.py
@@ -301,14 +301,13 @@
         actions=[a.serialize() for a in self.actions])
 
   def summary(self, code_storage):
+    result = {}
     if self.comment:
-      return self.comment
-    assert self.actions
-    if len(self.actions) > 1:
-      # TODO(kcwu): show details for multiple Actions
-      return '(%d actions)' % len(self.actions)
-    else:
-      return self.actions[0].summary(code_storage)
+      result['comment'] = self.comment
+    result['actions'] = [
+        action.summary(code_storage) for action in self.actions
+    ]
+    return result
 
   @staticmethod
   def unserialize(data):
@@ -346,12 +345,22 @@
   def summary(self, code_storage):
     git_root = code_storage.cached_git_root(self.repo_url)
     try:
-      summary = git_util.get_commit_log(git_root, self.rev).splitlines()[0]
+      commit_summary = git_util.get_commit_log(git_root,
+                                               self.rev).splitlines()[0]
     except subprocess.CalledProcessError:
       logger.warning('failed to get commit log of %s at %s', self.rev[:10],
                      git_root)
-      summary = '(unknown)'
-    return 'commit %s %s %r' % (self.rev[:10], self.path, summary)
+      commit_summary = '(unknown)'
+    text = 'commit %s %s %r' % (self.rev[:10], self.path, commit_summary)
+    return dict(
+        timestamp=self.timestamp,
+        action_type='commit',
+        path=self.path,
+        commit_summary=commit_summary,
+        repo_url=self.repo_url,
+        rev=self.rev,
+        text=text,
+    )
 
 
 class GitAddRepo(Action):
@@ -378,7 +387,13 @@
     code_storage.add_to_project_list(root_dir, self.path, self.repo_url)
 
   def summary(self, _code_storage):
-    return 'add repo %s from %s@%s' % (self.path, self.repo_url, self.rev[:10])
+    text = 'add repo %s from %s@%s' % (self.path, self.repo_url, self.rev[:10])
+    return dict(
+        timestamp=self.timestamp,
+        action_type='add_repo',
+        path=self.path,
+        text=text,
+    )
 
 
 class GitRemoveRepo(Action):
@@ -393,7 +408,12 @@
     code_storage.remove_from_project_list(root_dir, self.path)
 
   def summary(self, _code_storage):
-    return 'remove repo %s' % self.path
+    return dict(
+        timestamp=self.timestamp,
+        action_type='remove_repo',
+        path=self.path,
+        text='remove repo %s' % self.path,
+    )
 
 
 def apply_actions(code_storage, action_groups, root_dir):
@@ -892,26 +912,15 @@
 
     return result
 
-  def view_rev_diff(self, revlist, old, new):
-    assert old in revlist
-    assert new in revlist
-    loaded_action_groups = None
-    old_idx = revlist.index(old)
-    new_idx = revlist.index(new)
+  def get_rev_detail(self, rev):
+    rev_old, rev_new, index = parse_intra_rev(rev)
+    if rev_old == rev_new:
+      return {}
 
-    for i, rev in enumerate(revlist[old_idx:new_idx + 1], old_idx):
-      rev_old, rev_new, index = parse_intra_rev(rev)
-      if rev_old == rev_new:
-        logger.info('[%d] %s', i, rev)
-      else:
-        if loaded_action_groups != (rev_old, rev_new):
-          action_groups = self.load_action_groups_between_releases(
-              rev_old, rev_new)
-          loaded_action_groups = (rev_old, rev_new)
-        # Indexes inside intra_rev are 1 based.
-        action_group = action_groups[index - 1]
-        summary = action_group.summary(self.code_storage)
-        logger.info('[%d] %s %s', i, rev, summary)
+    action_groups = self.load_action_groups_between_releases(rev_old, rev_new)
+    # Indexes inside intra_rev are 1 based.
+    action_group = action_groups[index - 1]
+    return action_group.summary(self.code_storage)
 
   def switch(self, rev):
     # easy case