hooks.py 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. # hooks.py -- for dealing with git hooks
  2. # Copyright (C) 2012-2013 Jelmer Vernooij and others.
  3. #
  4. # Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
  5. # General Public License as public by the Free Software Foundation; version 2.0
  6. # or (at your option) any later version. You can redistribute it and/or
  7. # modify it under the terms of either of these two licenses.
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. #
  15. # You should have received a copy of the licenses; if not, see
  16. # <http://www.gnu.org/licenses/> for a copy of the GNU General Public License
  17. # and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
  18. # License, Version 2.0.
  19. #
  20. """Access to hooks."""
  21. import os
  22. import subprocess
  23. import tempfile
  24. from dulwich.errors import (
  25. HookError,
  26. )
  27. class Hook(object):
  28. """Generic hook object."""
  29. def execute(self, *args):
  30. """Execute the hook with the given args
  31. Args:
  32. args: argument list to hook
  33. Raises:
  34. HookError: hook execution failure
  35. Returns:
  36. a hook may return a useful value
  37. """
  38. raise NotImplementedError(self.execute)
  39. class ShellHook(Hook):
  40. """Hook by executable file
  41. Implements standard githooks(5) [0]:
  42. [0] http://www.kernel.org/pub/software/scm/git/docs/githooks.html
  43. """
  44. def __init__(self, name, path, numparam,
  45. pre_exec_callback=None, post_exec_callback=None,
  46. cwd=None):
  47. """Setup shell hook definition
  48. Args:
  49. name: name of hook for error messages
  50. path: absolute path to executable file
  51. numparam: number of requirements parameters
  52. pre_exec_callback: closure for setup before execution
  53. Defaults to None. Takes in the variable argument list from the
  54. execute functions and returns a modified argument list for the
  55. shell hook.
  56. post_exec_callback: closure for cleanup after execution
  57. Defaults to None. Takes in a boolean for hook success and the
  58. modified argument list and returns the final hook return value
  59. if applicable
  60. cwd: working directory to switch to when executing the hook
  61. """
  62. self.name = name
  63. self.filepath = path
  64. self.numparam = numparam
  65. self.pre_exec_callback = pre_exec_callback
  66. self.post_exec_callback = post_exec_callback
  67. self.cwd = cwd
  68. def execute(self, *args):
  69. """Execute the hook with given args"""
  70. if len(args) != self.numparam:
  71. raise HookError("Hook %s executed with wrong number of args. \
  72. Expected %d. Saw %d. args: %s"
  73. % (self.name, self.numparam, len(args), args))
  74. if (self.pre_exec_callback is not None):
  75. args = self.pre_exec_callback(*args)
  76. try:
  77. ret = subprocess.call([self.filepath] + list(args), cwd=self.cwd)
  78. if ret != 0:
  79. if (self.post_exec_callback is not None):
  80. self.post_exec_callback(0, *args)
  81. raise HookError("Hook %s exited with non-zero status %d"
  82. % (self.name, ret))
  83. if (self.post_exec_callback is not None):
  84. return self.post_exec_callback(1, *args)
  85. except OSError: # no file. silent failure.
  86. if (self.post_exec_callback is not None):
  87. self.post_exec_callback(0, *args)
  88. class PreCommitShellHook(ShellHook):
  89. """pre-commit shell hook"""
  90. def __init__(self, controldir):
  91. filepath = os.path.join(controldir, 'hooks', 'pre-commit')
  92. ShellHook.__init__(self, 'pre-commit', filepath, 0, cwd=controldir)
  93. class PostCommitShellHook(ShellHook):
  94. """post-commit shell hook"""
  95. def __init__(self, controldir):
  96. filepath = os.path.join(controldir, 'hooks', 'post-commit')
  97. ShellHook.__init__(self, 'post-commit', filepath, 0, cwd=controldir)
  98. class CommitMsgShellHook(ShellHook):
  99. """commit-msg shell hook
  100. Args:
  101. args[0]: commit message
  102. Returns:
  103. new commit message or None
  104. """
  105. def __init__(self, controldir):
  106. filepath = os.path.join(controldir, 'hooks', 'commit-msg')
  107. def prepare_msg(*args):
  108. (fd, path) = tempfile.mkstemp()
  109. with os.fdopen(fd, 'wb') as f:
  110. f.write(args[0])
  111. return (path,)
  112. def clean_msg(success, *args):
  113. if success:
  114. with open(args[0], 'rb') as f:
  115. new_msg = f.read()
  116. os.unlink(args[0])
  117. return new_msg
  118. os.unlink(args[0])
  119. ShellHook.__init__(self, 'commit-msg', filepath, 1,
  120. prepare_msg, clean_msg, controldir)
  121. class PostReceiveShellHook(ShellHook):
  122. """post-receive shell hook"""
  123. def __init__(self, controldir):
  124. self.controldir = controldir
  125. filepath = os.path.join(controldir, 'hooks', 'post-receive')
  126. ShellHook.__init__(self, 'post-receive', filepath, 0)
  127. def execute(self, client_refs):
  128. # do nothing if the script doesn't exist
  129. if not os.path.exists(self.filepath):
  130. return None
  131. try:
  132. env = os.environ.copy()
  133. env['GIT_DIR'] = self.controldir
  134. p = subprocess.Popen(
  135. self.filepath,
  136. stdin=subprocess.PIPE,
  137. stdout=subprocess.PIPE,
  138. stderr=subprocess.PIPE,
  139. env=env
  140. )
  141. # client_refs is a list of (oldsha, newsha, ref)
  142. in_data = '\n'.join([' '.join(ref) for ref in client_refs])
  143. out_data, err_data = p.communicate(in_data)
  144. if (p.returncode != 0) or err_data:
  145. err_fmt = "post-receive exit code: %d\n" \
  146. + "stdout:\n%s\nstderr:\n%s"
  147. err_msg = err_fmt % (p.returncode, out_data, err_data)
  148. raise HookError(err_msg)
  149. return out_data
  150. except OSError as err:
  151. raise HookError(repr(err))