bisect-kit: Add ability to dump Chrome DEPS string

BUG=b:140720677
TEST=python3 -m unittest bisect_kit.gclient_util_test

Change-Id: I8c441f6696b0ef0a573e6ba652612c5eb9839a95
diff --git a/bisect_kit/gclient_util.py b/bisect_kit/gclient_util.py
index eb6dac3..4597bde 100644
--- a/bisect_kit/gclient_util.py
+++ b/bisect_kit/gclient_util.py
@@ -314,6 +314,39 @@
     # pylint: disable=eval-used
     return eval(self.condition, vars_dict)
 
+  def to_lines(self):
+    s = []
+    condition_part = (['    "condition": %r,' %
+                       self.condition] if self.condition else [])
+    if self.dep_type == 'cipd':
+      s.extend([
+          '  "%s": {' % (self.path.split(':')[0],),
+          '    "packages": [',
+      ])
+      for p in sorted(self.packages, key=lambda x: x['package']):
+        s.extend([
+            '      {',
+            '        "package": "%s",' % p['package'],
+            '        "version": "%s",' % p['version'],
+            '      },',
+        ])
+      s.extend([
+          '    ],',
+          '    "dep_type": "cipd",',
+      ] + condition_part + [
+          '  },',
+          '',
+      ])
+    else:
+      s.extend([
+          '  "%s": {' % (self.path,),
+          '    "url": "%s",' % (self.url,),
+      ] + condition_part + [
+          '  },',
+          '',
+      ])
+    return s
+
 
 class Deps:
   """DEPS parsed result.
@@ -328,7 +361,88 @@
   def __init__(self):
     self.variables = {}
     self.entries = {}
+    self.ignored_entries = {}
     self.recursedeps = []
+    self.allowed_hosts = set()
+    self.gn_args_file = None
+    self.gn_args = []
+    self.hooks = []
+    self.pre_deps_hooks = []
+
+  def _gn_settings_to_lines(self):
+    s = []
+    if self.gn_args_file:
+      s.extend([
+          'gclient_gn_args_file = "%s"' % self.gn_args_file,
+          'gclient_gn_args = %r' % self.gn_args,
+      ])
+    return s
+
+  def _allowed_hosts_to_lines(self):
+    """Converts |allowed_hosts| set to list of lines for output."""
+    if not self.allowed_hosts:
+      return []
+    s = ['allowed_hosts = [']
+    for h in sorted(self.allowed_hosts):
+      s.append('  "%s",' % h)
+    s.extend([']', ''])
+    return s
+
+  def _entries_to_lines(self):
+    """Converts |entries| dict to list of lines for output."""
+    entries = self.ignored_entries
+    entries.update(self.entries)
+    if not entries:
+      return []
+    s = ['deps = {']
+    for _, dep in sorted(entries.items()):
+      s.extend(dep.to_lines())
+    s.extend(['}', ''])
+    return s
+
+  def _vars_to_lines(self):
+    """Converts |variables| dict to list of lines for output."""
+    if not self.variables:
+      return []
+    s = ['vars = {']
+    for key, value in sorted(self.variables.items()):
+      s.extend([
+          '  "%s": %r,' % (key, value),
+          '',
+      ])
+    s.extend(['}', ''])
+    return s
+
+  def _hooks_to_lines(self, name, hooks):
+    """Converts |hooks| list to list of lines for output."""
+    if not hooks:
+      return []
+    s = ['%s = [' % name]
+    for hook in hooks:
+      s.extend([
+          '  {',
+      ])
+      if hook.get('name') is not None:
+        s.append('    "name": "%s",' % hook.get('name'))
+      if hook.get('pattern') is not None:
+        s.append('    "pattern": "%s",' % hook.get('pattern'))
+      if hook.get('condition') is not None:
+        s.append('    "condition": %r,' % hook.get('condition'))
+      # Flattened hooks need to be written relative to the root gclient dir
+      cwd = os.path.relpath(os.path.normpath(hook.get('cwd')))
+      s.extend(['    "cwd": "%s",' % cwd] + ['    "action": ['] +
+               ['        "%s",' % arg for arg in hook.get('action', [])] +
+               ['    ]', '  },', ''])
+    s.extend([']', ''])
+    return s
+
+  def to_string(self):
+    """Return flatten DEPS string."""
+    return '\n'.join(
+        self._gn_settings_to_lines() + self._allowed_hosts_to_lines() +
+        self._entries_to_lines() + self._hooks_to_lines('hooks', self.hooks) +
+        self._hooks_to_lines('pre_deps_hooks', self.pre_deps_hooks) +
+        self._vars_to_lines() + [''])  # Ensure newline at end of file.
 
 
 class TimeSeriesTree:
@@ -557,6 +671,15 @@
                        name)
     if 'deps_os' in local_scope:
       logger.warning('deps_os is no longer supported')
+    if local_scope.get('gclient_gn_args_from'):
+      logger.warning('gclient_gn_args_from is not supported')
+
+    if 'allowed_hosts' in local_scope:
+      deps.allowed_hosts = set(local_scope.get('allowed_hosts'))
+    deps.hooks = local_scope.get('hooks', [])
+    deps.pre_deps_hooks = local_scope.get('pre_deps_hooks', [])
+    deps.gn_args_file = local_scope.get('gclient_gn_args_file')
+    deps.gn_args = local_scope.get('gclient_gn_args', [])
 
     for path, dep_entry in local_scope['deps'].items():
       path = path.format(**deps.variables)
@@ -565,6 +688,7 @@
       path = os.path.normpath(path)
       dep = Dep(path, deps.variables, dep_entry)
       if not dep.eval_condition():
+        deps.ignored_entries[path] = dep
         continue
 
       # TODO(kcwu): support dep_type=cipd http://crbug.com/846564
@@ -574,6 +698,7 @@
           emitted_warnings.add(warning_key)
           logger.warning('dep_type=%s is not supported yet: %s', dep.dep_type,
                          path)
+        deps.ignored_entries[path] = dep
         continue
 
       deps.entries[path] = dep