hooks.py 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. # hooks.py -- for dealing with git hooks
  2. # Copyright (C) 2012-2013 Jelmer Vernooij and others.
  3. #
  4. # This program is free software; you can redistribute it and/or
  5. # modify it under the terms of the GNU General Public License
  6. # as published by the Free Software Foundation; version 2
  7. # of the License or (at your option) a later version of the License.
  8. #
  9. # This program is distributed in the hope that it will be useful,
  10. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. # GNU General Public License for more details.
  13. #
  14. # You should have received a copy of the GNU General Public License
  15. # along with this program; if not, write to the Free Software
  16. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
  17. # MA 02110-1301, USA.
  18. """Access to hooks."""
  19. import os
  20. import subprocess
  21. import sys
  22. import tempfile
  23. from dulwich.errors import (
  24. HookError,
  25. )
  26. class Hook(object):
  27. """Generic hook object."""
  28. def execute(self, *args):
  29. """Execute the hook with the given args
  30. :param args: argument list to hook
  31. :raise HookError: hook execution failure
  32. :return: a hook may return a useful value
  33. """
  34. raise NotImplementedError(self.execute)
  35. class ShellHook(Hook):
  36. """Hook by executable file
  37. Implements standard githooks(5) [0]:
  38. [0] http://www.kernel.org/pub/software/scm/git/docs/githooks.html
  39. """
  40. def __init__(self, name, path, numparam,
  41. pre_exec_callback=None, post_exec_callback=None):
  42. """Setup shell hook definition
  43. :param name: name of hook for error messages
  44. :param path: absolute path to executable file
  45. :param numparam: number of requirements parameters
  46. :param pre_exec_callback: closure for setup before execution
  47. Defaults to None. Takes in the variable argument list from the
  48. execute functions and returns a modified argument list for the
  49. shell hook.
  50. :param post_exec_callback: closure for cleanup after execution
  51. Defaults to None. Takes in a boolean for hook success and the
  52. modified argument list and returns the final hook return value
  53. if applicable
  54. """
  55. self.name = name
  56. self.filepath = path
  57. self.numparam = numparam
  58. self.pre_exec_callback = pre_exec_callback
  59. self.post_exec_callback = post_exec_callback
  60. if sys.version_info[0] == 2 and sys.platform == 'win32':
  61. # Python 2 on windows does not support unicode file paths
  62. # http://bugs.python.org/issue1759845
  63. self.filepath = self.filepath.encode(sys.getfilesystemencoding())
  64. def execute(self, *args):
  65. """Execute the hook with given args"""
  66. if len(args) != self.numparam:
  67. raise HookError("Hook %s executed with wrong number of args. \
  68. Expected %d. Saw %d. args: %s"
  69. % (self.name, self.numparam, len(args), args))
  70. if (self.pre_exec_callback is not None):
  71. args = self.pre_exec_callback(*args)
  72. try:
  73. ret = subprocess.call([self.filepath] + list(args))
  74. if ret != 0:
  75. if (self.post_exec_callback is not None):
  76. self.post_exec_callback(0, *args)
  77. raise HookError("Hook %s exited with non-zero status"
  78. % (self.name))
  79. if (self.post_exec_callback is not None):
  80. return self.post_exec_callback(1, *args)
  81. except OSError: # no file. silent failure.
  82. if (self.post_exec_callback is not None):
  83. self.post_exec_callback(0, *args)
  84. class PreCommitShellHook(ShellHook):
  85. """pre-commit shell hook"""
  86. def __init__(self, controldir):
  87. filepath = os.path.join(controldir, 'hooks', 'pre-commit')
  88. ShellHook.__init__(self, 'pre-commit', filepath, 0)
  89. class PostCommitShellHook(ShellHook):
  90. """post-commit shell hook"""
  91. def __init__(self, controldir):
  92. filepath = os.path.join(controldir, 'hooks', 'post-commit')
  93. ShellHook.__init__(self, 'post-commit', filepath, 0)
  94. class CommitMsgShellHook(ShellHook):
  95. """commit-msg shell hook
  96. :param args[0]: commit message
  97. :return: new commit message or None
  98. """
  99. def __init__(self, controldir):
  100. filepath = os.path.join(controldir, 'hooks', 'commit-msg')
  101. def prepare_msg(*args):
  102. (fd, path) = tempfile.mkstemp()
  103. with os.fdopen(fd, 'wb') as f:
  104. f.write(args[0])
  105. return (path,)
  106. def clean_msg(success, *args):
  107. if success:
  108. with open(args[0], 'rb') as f:
  109. new_msg = f.read()
  110. os.unlink(args[0])
  111. return new_msg
  112. os.unlink(args[0])
  113. ShellHook.__init__(self, 'commit-msg', filepath, 1,
  114. prepare_msg, clean_msg)