Merge "Implementation of manifest defined githooks"
diff --git a/docs/manifest-format.txt b/docs/manifest-format.txt
index 1aa9396..4b979c7 100644
--- a/docs/manifest-format.txt
+++ b/docs/manifest-format.txt
@@ -31,7 +31,7 @@
 
     <!ELEMENT notice (#PCDATA)>
 
-    <!ELEMENT remote (EMPTY)>
+    <!ELEMENT remote (projecthook?)>
     <!ATTLIST remote name         ID    #REQUIRED>
     <!ATTLIST remote alias        CDATA #IMPLIED>
     <!ATTLIST remote fetch        CDATA #REQUIRED>
@@ -73,6 +73,10 @@
     <!ATTLIST extend-project path CDATA #IMPLIED>
     <!ATTLIST extend-project groups CDATA #IMPLIED>
 
+    <!ELEMENT projecthook (EMPTY)>
+    <!ATTLIST projecthook name CDATA #REQUIRED>
+    <!ATTLIST projecthook revision CDATA #REQUIRED>
+
     <!ELEMENT remove-project (EMPTY)>
     <!ATTLIST remove-project name  CDATA #REQUIRED>
 
@@ -306,6 +310,15 @@
 Attribute `name`: the manifest to include, specified relative to
 the manifest repository's root.
 
+Element projecthook
+-------------------
+
+This element is used to define a per-remote hook git that is
+fetched and applied to all projects using the remote. The project-
+hook functionality allows for company/team .git/hooks to be used.
+The hooks in the supplied project and revision are supplemented to
+the current repo stock hooks for each project. Supplemented hooks
+overrule any stock hooks.
 
 Local Manifests
 ===============
diff --git a/manifest_xml.py b/manifest_xml.py
index 890c954..9472a08 100644
--- a/manifest_xml.py
+++ b/manifest_xml.py
@@ -64,7 +64,9 @@
                fetch=None,
                manifestUrl=None,
                review=None,
-               revision=None):
+               revision=None,
+               projecthookName=None,
+               projecthookRevision=None):
     self.name = name
     self.fetchUrl = fetch
     self.manifestUrl = manifestUrl
@@ -72,6 +74,8 @@
     self.reviewUrl = review
     self.revision = revision
     self.resolvedFetchUrl = self._resolveFetchUrl()
+    self.projecthookName = projecthookName
+    self.projecthookRevision = projecthookRevision
 
   def __eq__(self, other):
     return self.__dict__ == other.__dict__
@@ -167,6 +171,11 @@
       e.setAttribute('review', r.reviewUrl)
     if r.revision is not None:
       e.setAttribute('revision', r.revision)
+    if r.projecthookName is not None:
+      ph = doc.createElement('projecthook')
+      ph.setAttribute('name', r.projecthookName)
+      ph.setAttribute('revision', r.projecthookRevision)
+      e.appendChild(ph)
 
   def _ParseGroups(self, groups):
     return [x for x in re.split(r'[,\s]+', groups) if x]
@@ -629,7 +638,13 @@
     if revision == '':
       revision = None
     manifestUrl = self.manifestProject.config.GetString('remote.origin.url')
-    return _XmlRemote(name, alias, fetch, manifestUrl, review, revision)
+    projecthookName = None
+    projecthookRevision = None
+    for n in node.childNodes:
+      if n.nodeName == 'projecthook':
+        projecthookName, projecthookRevision = self._ParseProjectHooks(n)
+        break
+    return _XmlRemote(name, alias, fetch, manifestUrl, review, revision, projecthookName, projecthookRevision)
 
   def _ParseDefault(self, node):
     """
@@ -933,3 +948,8 @@
       diff['added'].append(toProjects[proj])
 
     return diff
+
+  def _ParseProjectHooks(self, node):
+    name = self._reqatt(node, 'name')
+    revision = self._reqatt(node, 'revision')
+    return name, revision
diff --git a/project.py b/project.py
index 5f47899..68d7421 100644
--- a/project.py
+++ b/project.py
@@ -69,27 +69,6 @@
 def sq(r):
   return "'" + r.replace("'", "'\''") + "'"
 
-_project_hook_list = None
-def _ProjectHooks():
-  """List the hooks present in the 'hooks' directory.
-
-  These hooks are project hooks and are copied to the '.git/hooks' directory
-  of all subprojects.
-
-  This function caches the list of hooks (based on the contents of the
-  'repo/hooks' directory) on the first call.
-
-  Returns:
-    A list of absolute paths to all of the files in the hooks directory.
-  """
-  global _project_hook_list
-  if _project_hook_list is None:
-    d = os.path.realpath(os.path.abspath(os.path.dirname(__file__)))
-    d = os.path.join(d, 'hooks')
-    _project_hook_list = [os.path.join(d, x) for x in os.listdir(d)]
-  return _project_hook_list
-
-
 class DownloadedChange(object):
   _commit_cache = None
 
@@ -2106,7 +2085,7 @@
     if GitCommand(self, cmd).Wait() != 0:
       raise GitError('%s merge %s ' % (self.name, head))
 
-  def _InitGitDir(self, mirror_git=None):
+  def _InitGitDir(self, mirror_git=None, MirrorOverride=False):
     if not os.path.exists(self.gitdir):
 
       # Initialize the bare repository, which contains all of the objects.
@@ -2148,11 +2127,38 @@
       for key in ['user.name', 'user.email']:
         if m.Has(key, include_defaults=False):
           self.config.SetString(key, m.GetString(key))
-      if self.manifest.IsMirror:
+      if self.manifest.IsMirror and not MirrorOverride:
         self.config.SetString('core.bare', 'true')
       else:
         self.config.SetString('core.bare', None)
 
+  def _ProjectHooks(self, remote, repodir):
+    """List the hooks present in the 'hooks' directory.
+
+    These hooks are project hooks and are copied to the '.git/hooks' directory
+    of all subprojects.
+
+    The remote projecthooks supplement/overrule any stockhook making it possible to
+    have a combination of hooks both from the remote projecthook and
+    .repo/hooks directories.
+
+    Returns:
+      A list of absolute paths to all of the files in the hooks directory and
+      projecthooks files, excluding the .git folder.
+    """
+    hooks = {}
+    d = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'hooks')
+    hooks = dict([(x, os.path.join(d, x)) for x in os.listdir(d)])
+    if remote is not None:
+      if remote.projecthookName is not None:
+        d = os.path.abspath('%s/projecthooks/%s/%s' % (repodir, remote.name, remote.projecthookName))
+        if os.path.isdir(d):
+          hooks.update(dict([(x, os.path.join(d, x)) for x in os.listdir(d)]))
+
+    if hooks.has_key('.git'):
+      del hooks['.git']
+    return hooks.values()
+
   def _UpdateHooks(self):
     if os.path.exists(self.gitdir):
       self._InitHooks()
@@ -2161,7 +2167,10 @@
     hooks = os.path.realpath(self._gitdir_path('hooks'))
     if not os.path.exists(hooks):
       os.makedirs(hooks)
-    for stock_hook in _ProjectHooks():
+    pr = None
+    if self is not self.manifest.manifestProject:
+      pr = self.manifest.remotes.get(self.remote.name)
+    for stock_hook in self._ProjectHooks(pr, self.manifest.repodir):
       name = os.path.basename(stock_hook)
 
       if name in ('commit-msg',) and not self.remote.review \
diff --git a/subcmds/init.py b/subcmds/init.py
index b73de71..c5bf282 100644
--- a/subcmds/init.py
+++ b/subcmds/init.py
@@ -32,7 +32,7 @@
 from color import Coloring
 from command import InteractiveCommand, MirrorSafeCommand
 from error import ManifestParseError
-from project import SyncBuffer
+from project import SyncBuffer, MetaProject
 from git_config import GitConfig
 from git_command import git_require, MIN_GIT_VERSION
 
@@ -374,6 +374,52 @@
       print('   rm -r %s/.repo' % self.manifest.topdir)
       print('and try again.')
 
+  def _SyncProjectHooks(self, opt, repodir):
+    """Downloads the defined hooks supplied in the projecthooks element
+
+    """
+    # Always delete projecthooks and re-download for every new init.
+    projecthooksdir = os.path.join(repodir, 'projecthooks')
+    if os.path.exists(projecthooksdir):
+      shutil.rmtree(projecthooksdir)
+    for remotename in self.manifest.remotes:
+      r = self.manifest.remotes.get(remotename)
+      if r.projecthookName is not None and r.projecthookRevision is not None:
+        projecthookurl = r.resolvedFetchUrl.rstrip('/') + '/' + r.projecthookName
+
+        ph = MetaProject(manifest = self.manifest,
+        name = r.projecthookName,
+        gitdir   = os.path.join(projecthooksdir,'%s/%s.git' % (remotename, r.projecthookName)),
+        worktree = os.path.join(projecthooksdir,'%s/%s' % (remotename, r.projecthookName)))
+
+        ph.revisionExpr = r.projecthookRevision
+        is_new = not ph.Exists
+
+        if is_new:
+          if not opt.quiet:
+            print('Get projecthook %s' % \
+              GitConfig.ForUser().UrlInsteadOf(projecthookurl), file=sys.stderr)
+          ph._InitGitDir(MirrorOverride=True)
+
+        phr = ph.GetRemote(remotename)
+        phr.name = 'origin'
+        phr.url = projecthookurl
+        phr.ResetFetch()
+        phr.Save()
+
+        if not ph.Sync_NetworkHalf(quiet=opt.quiet, is_new=is_new, clone_bundle=False):
+          print('fatal: cannot obtain projecthook %s' % phr.url, file=sys.stderr)
+
+          # Better delete the git dir if we created it; otherwise next
+          # time (when user fixes problems) we won't go through the "is_new" logic.
+          if is_new:
+            shutil.rmtree(ph.gitdir)
+          sys.exit(1)
+
+        syncbuf = SyncBuffer(ph.config)
+        ph.Sync_LocalHalf(syncbuf)
+        syncbuf.Finish()
+
   def Execute(self, opt, args):
     git_require(MIN_GIT_VERSION, fail=True)
 
@@ -389,6 +435,7 @@
 
     self._SyncManifest(opt)
     self._LinkManifest(opt.manifest_name)
+    self._SyncProjectHooks(opt, self.manifest.repodir)
 
     if os.isatty(0) and os.isatty(1) and not self.manifest.IsMirror:
       if opt.config_name or self._ShouldConfigureUser():