Kiln » Dependencies » Dulwich Read More
Clone URL:  
Pushed to one repository · View In Graph Contained in master-1, master-0, and 0.9.4

Basic Hook Framework

Implement local shell hooks -
pre-commit, commit-msg, post-commit
Add to hooks to Repo

Changeset c308a063178b

Parent 1d73469befa3

committed by milki

authored by milki

Changes to 3 files · Browse files at c308a063178b Showing diff from parent 1d73469befa3 Diff from another changeset...

Change 1 of 1 Show Entire File dulwich/​errors.py Stacked
 
171
172
173
 
 
 
 
 
171
172
173
174
175
176
177
@@ -171,3 +171,7 @@
   class RefFormatError(Exception):   """Indicates an invalid ref name.""" + + +class HookError(Exception): + """An error occurred while executing a hook."""
Change 1 of 1 Show Entire File dulwich/​hooks.py Stacked
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
@@ -1,0 +1,147 @@
+# hooks.py -- for dealing with git hooks +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; version 2 +# of the License or (at your option) a later version of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. + +"""Access to hooks.""" + +import os +import subprocess +import tempfile +import warnings + +from dulwich.errors import ( + HookError, +) + + +class Hook(object): + """Generic hook object.""" + + def execute(elf, *args): + """Execute the hook with the given args + + :param args: argument list to hook + :raise HookError: hook execution failure + :return: a hook may return a useful value + """ + raise NotImplementedError(self.execute) + + +class ShellHook(Hook): + """Hook by executable file + + Implements standard githooks(5) [0]: + + [0] http://www.kernel.org/pub/software/scm/git/docs/githooks.html + """ + + def __init__(self, name, path, numparam, + pre_exec_callback=None, post_exec_callback=None): + """Setup shell hook definition + + :param name: name of hook for error messages + :param path: absolute path to executable file + :param numparam: number of requirements parameters + :param pre_exec_callback: closure for setup before execution + Defaults to None. Takes in the variable argument list from the + execute functions and returns a modified argument list for the + shell hook. + :param post_exec_callback: closure for cleanup after execution + Defaults to None. Takes in a boolean for hook success and the + modified argument list and returns the final hook return value + if applicable + """ + self.name = name + self.filepath = path + self.numparam = numparam + + self.pre_exec_callback = pre_exec_callback + self.post_exec_callback = post_exec_callback + + def execute(self, *args): + """Execute the hook with given args""" + + if len(args) != self.numparam: + raise HookError("Hook %s executed with wrong number of args. \ + Expected %d. Saw %d. %s" + % (self.name, self.numparam, len(args))) + + if (self.pre_exec_callback is not None): + args = self.pre_exec_callback(*args) + + try: + ret = subprocess.call([self.filepath] + list(args)) + if ret != 0: + if (self.post_exec_callback is not None): + self.post_exec_callback(0, *args) + raise HookError("Hook %s exited with non-zero status" + % (self.name)) + if (self.post_exec_callback is not None): + return self.post_exec_callback(1, *args) + except OSError: # no file. silent failure. + if (self.post_exec_callback is not None): + self.post_exec_callback(0, *args) + + +class PreCommitShellHook(ShellHook): + """pre-commit shell hook""" + + def __init__(self, controldir): + filepath = os.path.join(controldir, 'hooks', 'pre-commit') + + ShellHook.__init__(self, 'pre-commit', filepath, 0) + + +class PostCommitShellHook(ShellHook): + """post-commit shell hook""" + + def __init__(self, controldir): + filepath = os.path.join(controldir, 'hooks', 'post-commit') + + ShellHook.__init__(self, 'post-commit', filepath, 0) + + +class CommitMsgShellHook(ShellHook): + """commit-msg shell hook + + :param args[0]: commit message + :return: new commit message or None + """ + + def __init__(self, controldir): + filepath = os.path.join(controldir, 'hooks', 'commit-msg') + + def prepare_msg(*args): + (fd, path) = tempfile.mkstemp() + + f = os.fdopen(fd, 'wb') + try: + f.write(args[0]) + finally: + f.close() + + return (path,) + + def clean_msg(success, *args): + if success: + with open(args[0], 'rb') as f: + new_msg = f.read() + os.unlink(args[0]) + return new_msg + os.unlink(args[0]) + + ShellHook.__init__(self, 'commit-msg', filepath, 1, + prepare_msg, clean_msg)
Change 1 of 7 Show Entire File dulwich/​repo.py Stacked
 
41
42
43
 
44
45
46
 
58
59
60
 
 
 
 
 
 
 
61
62
63
 
813
814
815
 
 
816
817
818
 
1179
1180
1181
 
 
 
 
 
 
 
 
1182
1183
1184
 
1206
1207
1208
1209
 
 
 
 
 
 
 
 
 
 
1210
1211
1212
 
1220
1221
1222
 
 
 
 
 
 
 
1223
1224
1225
 
1260
1261
1262
 
 
 
 
1263
1264
1265
 
41
42
43
44
45
46
47
 
59
60
61
62
63
64
65
66
67
68
69
70
71
 
821
822
823
824
825
826
827
828
 
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
 
1224
1225
1226
 
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
 
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
 
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
@@ -41,6 +41,7 @@
  PackedRefsException,   CommitError,   RefFormatError, + HookError,   )  from dulwich.file import (   ensure_dir_exists, @@ -58,6 +59,13 @@
  Tree,   hex_to_sha,   ) + +from dulwich.hooks import ( + PreCommitShellHook, + PostCommitShellHook, + CommitMsgShellHook, +) +  import warnings     @@ -813,6 +821,8 @@
  self.object_store = object_store   self.refs = refs   + self.hooks = {} +   def _init_files(self, bare):   """Initialize a default set of named files."""   from dulwich.config import ConfigFile @@ -1179,6 +1189,14 @@
  if len(tree) != 40:   raise ValueError("tree must be a 40-byte hex sha string")   c.tree = tree + + try: + self.hooks['pre-commit'].execute() + except HookError, e: + raise CommitError(e) + except KeyError: # no hook defined, silent fallthrough + pass +   if merge_heads is None:   # FIXME: Read merge heads from .git/MERGE_HEADS   merge_heads = [] @@ -1206,7 +1224,16 @@
  if message is None:   # FIXME: Try to read commit message from .git/MERGE_MSG   raise ValueError("No commit message specified") - c.message = message + + try: + c.message = self.hooks['commit-msg'].execute(message) + if c.message is None: + c.message = message + except HookError, e: + raise CommitError(e) + except KeyError: # no hook defined, message not modified + c.message = message +   try:   old_head = self.refs[ref]   c.parents = [old_head] + merge_heads @@ -1220,6 +1247,13 @@
  # Fail if the atomic compare-and-swap failed, leaving the commit and   # all its objects as garbage.   raise CommitError("%s changed during commit" % (ref,)) + + try: + self.hooks['post-commit'].execute() + except HookError, e: # silent failure + warnings.warn("post-commit hook failed: %s" % e, UserWarning) + except KeyError: # no hook defined, silent fallthrough + pass     return c.id   @@ -1260,6 +1294,10 @@
  refs = DiskRefsContainer(self.controldir())   BaseRepo.__init__(self, object_store, refs)   + self.hooks['pre-commit'] = PreCommitShellHook(self.controldir()) + self.hooks['commit-msg'] = CommitMsgShellHook(self.controldir()) + self.hooks['post-commit'] = PostCommitShellHook(self.controldir()) +   def controldir(self):   """Return the path of the control directory."""   return self._controldir