git_cl: add GitNumbererState in preparation to stop gnumbd service.

The new class is unused and doesn't change any existing functionality.

BUG=chromium:642493
R=machenbach@chromium.org,iannucci@chromium.org

Change-Id: Id3fe71b07b694339f0a620b427816e52560069d8
Reviewed-on: https://chromium-review.googlesource.com/416430
Reviewed-by: Michael Achenbach <machenbach@chromium.org>
Commit-Queue: Andrii Shyshkalov <tandrii@chromium.org>
diff --git a/git_cl.py b/git_cl.py
index 5a81169..c80184c 100755
--- a/git_cl.py
+++ b/git_cl.py
@@ -13,6 +13,7 @@
 from multiprocessing.pool import ThreadPool
 import base64
 import collections
+import fnmatch
 import httplib
 import json
 import logging
@@ -22,6 +23,7 @@
 import re
 import stat
 import sys
+import tempfile
 import textwrap
 import traceback
 import urllib
@@ -1039,6 +1041,129 @@
   return keyvals.get('GENERATE_GIT_NUMBER_FOOTERS', '').lower() == 'true'
 
 
+class _GitNumbererState(object):
+  KNOWN_PROJECTS_WHITELIST = [
+      'chromium/src',
+      'external/webrtc',
+      'v8/v8',
+  ]
+
+  @classmethod
+  def load(cls, remote_url, remote_ref):
+    """Figures out the state by fetching special refs from remote repo.
+    """
+    assert remote_ref and remote_ref.startswith('refs/'), remote_ref
+    url_parts = urlparse.urlparse(remote_url)
+    project_name = url_parts.path.lstrip('/').rstrip('git./')
+    for known in cls.KNOWN_PROJECTS_WHITELIST:
+      if project_name.endswith(known):
+        break
+    else:
+      # Early exit to avoid extra fetches for repos that aren't using gnumbd.
+      return cls(cls._get_pending_prefix_fallback(), None)
+
+    # This pollutes local ref space, but the amount of objects is neglible.
+    error, _ = cls._run_git_with_code([
+        'fetch', remote_url,
+        '+refs/meta/config:refs/git_cl/meta/config',
+        '+refs/gnumbd-config/main:refs/git_cl/gnumbd-config/main'])
+    if error:
+      # Some ref doesn't exist or isn't accessible to current user.
+      # This shouldn't happen on production KNOWN_PROJECTS_WHITELIST
+      # with git-numberer.
+      cls._warn('failed to fetch gnumbd and project config for %s: %s',
+                remote_url, error)
+      return cls(cls._get_pending_prefix_fallback(), None)
+    return cls(cls._get_pending_prefix(remote_ref),
+               cls._is_validator_enabled(remote_ref))
+
+  @classmethod
+  def _get_pending_prefix(cls, ref):
+    error, gnumbd_config_data = cls._run_git_with_code(
+        ['show', 'refs/git_cl/gnumbd-config/main:config.json'])
+    if error:
+      cls._warn('gnumbd config file not found')
+      return cls._get_pending_prefix_fallback()
+
+    try:
+      config = json.loads(gnumbd_config_data)
+      if cls.match_refglobs(ref, config['enabled_refglobs']):
+        return config['pending_ref_prefix']
+      return None
+    except KeyboardInterrupt:
+      raise
+    except Exception as e:
+      cls._warn('failed to parse gnumbd config: %s', e)
+      return cls._get_pending_prefix_fallback()
+
+  @staticmethod
+  def _get_pending_prefix_fallback():
+    global settings
+    if not settings:
+      settings = Settings()
+    return settings.GetPendingRefPrefix()
+
+  @classmethod
+  def _is_validator_enabled(cls, ref):
+    error, project_config_data = cls._run_git_with_code(
+        ['show', 'refs/git_cl/meta/config:project.config'])
+    if error:
+      cls._warn('project.config file not found')
+      return False
+    # Gerrit's project.config is really a git config file.
+    # So, parse it as such.
+    with tempfile.NamedTemporaryFile(prefix='git_cl_proj_config') as f:
+      f.write(project_config_data)
+      # Make sure OS sees this, but don't close the file just yet,
+      # as NamedTemporaryFile deletes it on closing.
+      f.flush()
+
+      def get_opts(x):
+        code, out = cls._run_git_with_code(
+            ['config', '-f', f.name, '--get-all',
+             'plugin.git-numberer.validate-%s-refglob' % x])
+        if code == 0:
+          return out.strip().splitlines()
+        return []
+      enabled, disabled = map(get_opts, ['enabled', 'disabled'])
+
+    if cls.match_refglobs(ref, disabled):
+      return False
+    return cls.match_refglobs(ref, enabled)
+
+  @staticmethod
+  def match_refglobs(ref, refglobs):
+    for refglob in refglobs:
+      if ref == refglob or fnmatch.fnmatch(ref, refglob):
+        return True
+    return False
+
+  @staticmethod
+  def _run_git_with_code(*args, **kwargs):
+    # The only reason for this wrapper is easy porting of this code to CQ
+    # codebase, which forked git_cl.py and checkouts.py long time ago.
+    return RunGitWithCode(*args, **kwargs)
+
+  @staticmethod
+  def _warn(msg, *args):
+    if args:
+      msg = msg % args
+    print('WARNING: %s' % msg)
+
+  def __init__(self, pending_prefix, validator_enabled):
+    # TODO(tandrii): remove pending_prefix after gnumbd is no more.
+    self._pending_prefix = pending_prefix or None
+    self._validator_enabled = validator_enabled or False
+
+  @property
+  def pending_prefix(self):
+    return self._pending_prefix
+
+  @property
+  def should_git_number(self):
+    return self._validator_enabled and self._pending_prefix is None
+
+
 def ShortBranchName(branch):
   """Convert a name like 'refs/heads/foo' to just 'foo'."""
   return branch.replace('refs/heads/', '', 1)