##// END OF EJS Templates
Added handling of git hooks, extract pushed revisions and store them inside...
marcink -
r2402:2eeb2ed7 beta
parent child Browse files
Show More
@@ -0,0 +1,29 b''
1 #!/usr/bin/env python
2 import os
3 import sys
4
5 try:
6 import rhodecode
7 from rhodecode.lib.hooks import handle_git_post_receive
8 except ImportError:
9 rhodecode = None
10
11
12 def main():
13 if rhodecode is None:
14 # exit with success if we cannot import rhodecode !!
15 # this allows simply push to this repo even without
16 # rhodecode
17 sys.exit(0)
18
19 repo_path = os.path.abspath('.')
20 push_data = sys.stdin.read().strip().split(' ')
21 # os.environ is modified here by a subprocess call that
22 # runs git and later git executes this hook.
23 # Environ get's some additional info from rhodecode system
24 # like IP or username from basic-auth
25 handle_git_post_receive(repo_path, push_data, os.environ)
26 sys.exit(0)
27
28 if __name__ == '__main__':
29 main() No newline at end of file
@@ -1,192 +1,250 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.lib.hooks
3 rhodecode.lib.hooks
4 ~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~
5
5
6 Hooks runned by rhodecode
6 Hooks runned by rhodecode
7
7
8 :created_on: Aug 6, 2010
8 :created_on: Aug 6, 2010
9 :author: marcink
9 :author: marcink
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 :license: GPLv3, see COPYING for more details.
11 :license: GPLv3, see COPYING for more details.
12 """
12 """
13 # This program is free software: you can redistribute it and/or modify
13 # This program is free software: you can redistribute it and/or modify
14 # it under the terms of the GNU General Public License as published by
14 # it under the terms of the GNU General Public License as published by
15 # the Free Software Foundation, either version 3 of the License, or
15 # the Free Software Foundation, either version 3 of the License, or
16 # (at your option) any later version.
16 # (at your option) any later version.
17 #
17 #
18 # This program is distributed in the hope that it will be useful,
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # GNU General Public License for more details.
21 # GNU General Public License for more details.
22 #
22 #
23 # You should have received a copy of the GNU General Public License
23 # You should have received a copy of the GNU General Public License
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 import os
25 import os
26 import sys
26 import sys
27 import binascii
27 import binascii
28 from inspect import isfunction
28 from inspect import isfunction
29
29
30 from mercurial.scmutil import revrange
30 from mercurial.scmutil import revrange
31 from mercurial.node import nullrev
31 from mercurial.node import nullrev
32
32
33 from rhodecode import EXTENSIONS
33 from rhodecode import EXTENSIONS
34 from rhodecode.lib import helpers as h
34 from rhodecode.lib import helpers as h
35 from rhodecode.lib.utils import action_logger
35 from rhodecode.lib.utils import action_logger
36
36
37
37
38 def _get_scm_size(alias, root_path):
38 def _get_scm_size(alias, root_path):
39
39
40 if not alias.startswith('.'):
40 if not alias.startswith('.'):
41 alias += '.'
41 alias += '.'
42
42
43 size_scm, size_root = 0, 0
43 size_scm, size_root = 0, 0
44 for path, dirs, files in os.walk(root_path):
44 for path, dirs, files in os.walk(root_path):
45 if path.find(alias) != -1:
45 if path.find(alias) != -1:
46 for f in files:
46 for f in files:
47 try:
47 try:
48 size_scm += os.path.getsize(os.path.join(path, f))
48 size_scm += os.path.getsize(os.path.join(path, f))
49 except OSError:
49 except OSError:
50 pass
50 pass
51 else:
51 else:
52 for f in files:
52 for f in files:
53 try:
53 try:
54 size_root += os.path.getsize(os.path.join(path, f))
54 size_root += os.path.getsize(os.path.join(path, f))
55 except OSError:
55 except OSError:
56 pass
56 pass
57
57
58 size_scm_f = h.format_byte_size(size_scm)
58 size_scm_f = h.format_byte_size(size_scm)
59 size_root_f = h.format_byte_size(size_root)
59 size_root_f = h.format_byte_size(size_root)
60 size_total_f = h.format_byte_size(size_root + size_scm)
60 size_total_f = h.format_byte_size(size_root + size_scm)
61
61
62 return size_scm_f, size_root_f, size_total_f
62 return size_scm_f, size_root_f, size_total_f
63
63
64
64
65 def repo_size(ui, repo, hooktype=None, **kwargs):
65 def repo_size(ui, repo, hooktype=None, **kwargs):
66 """
66 """
67 Presents size of repository after push
67 Presents size of repository after push
68
68
69 :param ui:
69 :param ui:
70 :param repo:
70 :param repo:
71 :param hooktype:
71 :param hooktype:
72 """
72 """
73
73
74 size_hg_f, size_root_f, size_total_f = _get_scm_size('.hg', repo.root)
74 size_hg_f, size_root_f, size_total_f = _get_scm_size('.hg', repo.root)
75
75
76 last_cs = repo[len(repo) - 1]
76 last_cs = repo[len(repo) - 1]
77
77
78 msg = ('Repository size .hg:%s repo:%s total:%s\n'
78 msg = ('Repository size .hg:%s repo:%s total:%s\n'
79 'Last revision is now r%s:%s\n') % (
79 'Last revision is now r%s:%s\n') % (
80 size_hg_f, size_root_f, size_total_f, last_cs.rev(), last_cs.hex()[:12]
80 size_hg_f, size_root_f, size_total_f, last_cs.rev(), last_cs.hex()[:12]
81 )
81 )
82
82
83 sys.stdout.write(msg)
83 sys.stdout.write(msg)
84
84
85
85
86 def log_pull_action(ui, repo, **kwargs):
86 def log_pull_action(ui, repo, **kwargs):
87 """
87 """
88 Logs user last pull action
88 Logs user last pull action
89
89
90 :param ui:
90 :param ui:
91 :param repo:
91 :param repo:
92 """
92 """
93
93
94 extras = dict(repo.ui.configitems('rhodecode_extras'))
94 extras = dict(repo.ui.configitems('rhodecode_extras'))
95 username = extras['username']
95 username = extras['username']
96 repository = extras['repository']
96 repository = extras['repository']
97 scm = extras['scm']
97 scm = extras['scm']
98 action = 'pull'
98 action = 'pull'
99
99
100 action_logger(username, action, repository, extras['ip'], commit=True)
100 action_logger(username, action, repository, extras['ip'], commit=True)
101 # extension hook call
101 # extension hook call
102 callback = getattr(EXTENSIONS, 'PULL_HOOK', None)
102 callback = getattr(EXTENSIONS, 'PULL_HOOK', None)
103
103
104 if isfunction(callback):
104 if isfunction(callback):
105 kw = {}
105 kw = {}
106 kw.update(extras)
106 kw.update(extras)
107 callback(**kw)
107 callback(**kw)
108 return 0
108 return 0
109
109
110
110
111 def log_push_action(ui, repo, **kwargs):
111 def log_push_action(ui, repo, **kwargs):
112 """
112 """
113 Maps user last push action to new changeset id, from mercurial
113 Maps user last push action to new changeset id, from mercurial
114
114
115 :param ui:
115 :param ui:
116 :param repo: repo object containing the `ui` object
116 :param repo: repo object containing the `ui` object
117 """
117 """
118
118
119 extras = dict(repo.ui.configitems('rhodecode_extras'))
119 extras = dict(repo.ui.configitems('rhodecode_extras'))
120 username = extras['username']
120 username = extras['username']
121 repository = extras['repository']
121 repository = extras['repository']
122 action = extras['action'] + ':%s'
122 action = extras['action'] + ':%s'
123 scm = extras['scm']
123 scm = extras['scm']
124
124
125 if scm == 'hg':
125 if scm == 'hg':
126 node = kwargs['node']
126 node = kwargs['node']
127
127
128 def get_revs(repo, rev_opt):
128 def get_revs(repo, rev_opt):
129 if rev_opt:
129 if rev_opt:
130 revs = revrange(repo, rev_opt)
130 revs = revrange(repo, rev_opt)
131
131
132 if len(revs) == 0:
132 if len(revs) == 0:
133 return (nullrev, nullrev)
133 return (nullrev, nullrev)
134 return (max(revs), min(revs))
134 return (max(revs), min(revs))
135 else:
135 else:
136 return (len(repo) - 1, 0)
136 return (len(repo) - 1, 0)
137
137
138 stop, start = get_revs(repo, [node + ':'])
138 stop, start = get_revs(repo, [node + ':'])
139 h = binascii.hexlify
139 h = binascii.hexlify
140 revs = (h(repo[r].node()) for r in xrange(start, stop + 1))
140 revs = (h(repo[r].node()) for r in xrange(start, stop + 1))
141 elif scm == 'git':
141 elif scm == 'git':
142 revs = []
142 revs = kwargs.get('_git_revs', [])
143 if '_git_revs' in kwargs:
144 kwargs.pop('_git_revs')
143
145
144 action = action % ','.join(revs)
146 action = action % ','.join(revs)
145
147
146 action_logger(username, action, repository, extras['ip'], commit=True)
148 action_logger(username, action, repository, extras['ip'], commit=True)
147
149
148 # extension hook call
150 # extension hook call
149 callback = getattr(EXTENSIONS, 'PUSH_HOOK', None)
151 callback = getattr(EXTENSIONS, 'PUSH_HOOK', None)
150 if isfunction(callback):
152 if isfunction(callback):
151 kw = {'pushed_revs': revs}
153 kw = {'pushed_revs': revs}
152 kw.update(extras)
154 kw.update(extras)
153 callback(**kw)
155 callback(**kw)
154 return 0
156 return 0
155
157
156
158
157 def log_create_repository(repository_dict, created_by, **kwargs):
159 def log_create_repository(repository_dict, created_by, **kwargs):
158 """
160 """
159 Post create repository Hook. This is a dummy function for admins to re-use
161 Post create repository Hook. This is a dummy function for admins to re-use
160 if needed. It's taken from rhodecode-extensions module and executed
162 if needed. It's taken from rhodecode-extensions module and executed
161 if present
163 if present
162
164
163 :param repository: dict dump of repository object
165 :param repository: dict dump of repository object
164 :param created_by: username who created repository
166 :param created_by: username who created repository
165 :param created_date: date of creation
167 :param created_date: date of creation
166
168
167 available keys of repository_dict:
169 available keys of repository_dict:
168
170
169 'repo_type',
171 'repo_type',
170 'description',
172 'description',
171 'private',
173 'private',
172 'created_on',
174 'created_on',
173 'enable_downloads',
175 'enable_downloads',
174 'repo_id',
176 'repo_id',
175 'user_id',
177 'user_id',
176 'enable_statistics',
178 'enable_statistics',
177 'clone_uri',
179 'clone_uri',
178 'fork_id',
180 'fork_id',
179 'group_id',
181 'group_id',
180 'repo_name'
182 'repo_name'
181
183
182 """
184 """
183
185
184 callback = getattr(EXTENSIONS, 'CREATE_REPO_HOOK', None)
186 callback = getattr(EXTENSIONS, 'CREATE_REPO_HOOK', None)
185 if isfunction(callback):
187 if isfunction(callback):
186 kw = {}
188 kw = {}
187 kw.update(repository_dict)
189 kw.update(repository_dict)
188 kw.update({'created_by': created_by})
190 kw.update({'created_by': created_by})
189 kw.update(kwargs)
191 kw.update(kwargs)
190 return callback(**kw)
192 return callback(**kw)
191
193
192 return 0
194 return 0
195
196
197 def handle_git_post_receive(repo_path, revs, env):
198 """
199 A really hacky method that is runned by git pre-receive hook and logs
200 an push action together with pushed revisions. It's runned by subprocess
201 thus needs all info to be able to create a temp pylons enviroment, connect
202 to database and run the logging code. Hacky as sh**t but works. ps.
203 GIT SUCKS
204
205 :param repo_path:
206 :type repo_path:
207 :param revs:
208 :type revs:
209 :param env:
210 :type env:
211 """
212 from paste.deploy import appconfig
213 from sqlalchemy import engine_from_config
214 from rhodecode.config.environment import load_environment
215 from rhodecode.model import init_model
216 from rhodecode.model.db import RhodeCodeUi
217 from rhodecode.lib.utils import make_ui
218 from rhodecode.model.db import Repository
219
220 path, ini_name = os.path.split(env['RHODECODE_CONFIG_FILE'])
221 conf = appconfig('config:%s' % ini_name, relative_to=path)
222 load_environment(conf.global_conf, conf.local_conf)
223
224 engine = engine_from_config(conf, 'sqlalchemy.db1.')
225 init_model(engine)
226
227 baseui = make_ui('db')
228 repo = Repository.get_by_full_path(repo_path)
229
230 _hooks = dict(baseui.configitems('hooks')) or {}
231 # if push hook is enabled via web interface
232 if _hooks.get(RhodeCodeUi.HOOK_PUSH):
233
234 extras = {
235 'username': env['RHODECODE_USER'],
236 'repository': repo.repo_name,
237 'scm': 'git',
238 'action': 'push',
239 'ip': env['RHODECODE_CONFIG_IP'],
240 }
241 for k, v in extras.items():
242 baseui.setconfig('rhodecode_extras', k, v)
243 repo = repo.scm_instance
244 repo.ui = baseui
245 old_rev, new_rev = revs[0:-1]
246
247 cmd = 'log ' + old_rev + '..' + new_rev + ' --reverse --pretty=format:"%H"'
248 git_revs = repo.run_git_command(cmd)[0].splitlines()
249
250 log_push_action(baseui, repo, _git_revs=git_revs)
@@ -1,182 +1,198 b''
1 import os
1 import os
2 import socket
2 import socket
3 import logging
3 import logging
4 import subprocess
4 import subprocess
5
5
6 from webob import Request, Response, exc
6 from webob import Request, Response, exc
7
7
8 from rhodecode.lib import subprocessio
8 from rhodecode.lib import subprocessio
9
9
10 log = logging.getLogger(__name__)
10 log = logging.getLogger(__name__)
11
11
12
12
13 class FileWrapper(object):
13 class FileWrapper(object):
14
14
15 def __init__(self, fd, content_length):
15 def __init__(self, fd, content_length):
16 self.fd = fd
16 self.fd = fd
17 self.content_length = content_length
17 self.content_length = content_length
18 self.remain = content_length
18 self.remain = content_length
19
19
20 def read(self, size):
20 def read(self, size):
21 if size <= self.remain:
21 if size <= self.remain:
22 try:
22 try:
23 data = self.fd.read(size)
23 data = self.fd.read(size)
24 except socket.error:
24 except socket.error:
25 raise IOError(self)
25 raise IOError(self)
26 self.remain -= size
26 self.remain -= size
27 elif self.remain:
27 elif self.remain:
28 data = self.fd.read(self.remain)
28 data = self.fd.read(self.remain)
29 self.remain = 0
29 self.remain = 0
30 else:
30 else:
31 data = None
31 data = None
32 return data
32 return data
33
33
34 def __repr__(self):
34 def __repr__(self):
35 return '<FileWrapper %s len: %s, read: %s>' % (
35 return '<FileWrapper %s len: %s, read: %s>' % (
36 self.fd, self.content_length, self.content_length - self.remain
36 self.fd, self.content_length, self.content_length - self.remain
37 )
37 )
38
38
39
39
40 class GitRepository(object):
40 class GitRepository(object):
41 git_folder_signature = set(['config', 'head', 'info', 'objects', 'refs'])
41 git_folder_signature = set(['config', 'head', 'info', 'objects', 'refs'])
42 commands = ['git-upload-pack', 'git-receive-pack']
42 commands = ['git-upload-pack', 'git-receive-pack']
43
43
44 def __init__(self, repo_name, content_path):
44 def __init__(self, repo_name, content_path, username):
45 files = set([f.lower() for f in os.listdir(content_path)])
45 files = set([f.lower() for f in os.listdir(content_path)])
46 if not (self.git_folder_signature.intersection(files)
46 if not (self.git_folder_signature.intersection(files)
47 == self.git_folder_signature):
47 == self.git_folder_signature):
48 raise OSError('%s missing git signature' % content_path)
48 raise OSError('%s missing git signature' % content_path)
49 self.content_path = content_path
49 self.content_path = content_path
50 self.valid_accepts = ['application/x-%s-result' %
50 self.valid_accepts = ['application/x-%s-result' %
51 c for c in self.commands]
51 c for c in self.commands]
52 self.repo_name = repo_name
52 self.repo_name = repo_name
53 self.username = username
53
54
54 def _get_fixedpath(self, path):
55 def _get_fixedpath(self, path):
55 """
56 """
56 Small fix for repo_path
57 Small fix for repo_path
57
58
58 :param path:
59 :param path:
59 :type path:
60 :type path:
60 """
61 """
61 return path.split(self.repo_name, 1)[-1].strip('/')
62 return path.split(self.repo_name, 1)[-1].strip('/')
62
63
63 def inforefs(self, request, environ):
64 def inforefs(self, request, environ):
64 """
65 """
65 WSGI Response producer for HTTP GET Git Smart
66 WSGI Response producer for HTTP GET Git Smart
66 HTTP /info/refs request.
67 HTTP /info/refs request.
67 """
68 """
68
69
69 git_command = request.GET['service']
70 git_command = request.GET['service']
70 if git_command not in self.commands:
71 if git_command not in self.commands:
71 log.debug('command %s not allowed' % git_command)
72 log.debug('command %s not allowed' % git_command)
72 return exc.HTTPMethodNotAllowed()
73 return exc.HTTPMethodNotAllowed()
73
74
74 # note to self:
75 # note to self:
75 # please, resist the urge to add '\n' to git capture and increment
76 # please, resist the urge to add '\n' to git capture and increment
76 # line count by 1.
77 # line count by 1.
77 # The code in Git client not only does NOT need '\n', but actually
78 # The code in Git client not only does NOT need '\n', but actually
78 # blows up if you sprinkle "flush" (0000) as "0001\n".
79 # blows up if you sprinkle "flush" (0000) as "0001\n".
79 # It reads binary, per number of bytes specified.
80 # It reads binary, per number of bytes specified.
80 # if you do add '\n' as part of data, count it.
81 # if you do add '\n' as part of data, count it.
81 smart_server_advert = '# service=%s' % git_command
82 smart_server_advert = '# service=%s' % git_command
82 try:
83 try:
83 out = subprocessio.SubprocessIOChunker(
84 out = subprocessio.SubprocessIOChunker(
84 r'git %s --stateless-rpc --advertise-refs "%s"' % (
85 r'git %s --stateless-rpc --advertise-refs "%s"' % (
85 git_command[4:], self.content_path),
86 git_command[4:], self.content_path),
86 starting_values=[
87 starting_values=[
87 str(hex(len(smart_server_advert) + 4)[2:]
88 str(hex(len(smart_server_advert) + 4)[2:]
88 .rjust(4, '0') + smart_server_advert + '0000')
89 .rjust(4, '0') + smart_server_advert + '0000')
89 ]
90 ]
90 )
91 )
91 except EnvironmentError, e:
92 except EnvironmentError, e:
92 log.exception(e)
93 log.exception(e)
93 raise exc.HTTPExpectationFailed()
94 raise exc.HTTPExpectationFailed()
94 resp = Response()
95 resp = Response()
95 resp.content_type = 'application/x-%s-advertisement' % str(git_command)
96 resp.content_type = 'application/x-%s-advertisement' % str(git_command)
96 resp.app_iter = out
97 resp.app_iter = out
97 return resp
98 return resp
98
99
99 def backend(self, request, environ):
100 def backend(self, request, environ):
100 """
101 """
101 WSGI Response producer for HTTP POST Git Smart HTTP requests.
102 WSGI Response producer for HTTP POST Git Smart HTTP requests.
102 Reads commands and data from HTTP POST's body.
103 Reads commands and data from HTTP POST's body.
103 returns an iterator obj with contents of git command's
104 returns an iterator obj with contents of git command's
104 response to stdout
105 response to stdout
105 """
106 """
106 git_command = self._get_fixedpath(request.path_info)
107 git_command = self._get_fixedpath(request.path_info)
107 if git_command not in self.commands:
108 if git_command not in self.commands:
108 log.debug('command %s not allowed' % git_command)
109 log.debug('command %s not allowed' % git_command)
109 return exc.HTTPMethodNotAllowed()
110 return exc.HTTPMethodNotAllowed()
110
111
111 if 'CONTENT_LENGTH' in environ:
112 if 'CONTENT_LENGTH' in environ:
112 inputstream = FileWrapper(environ['wsgi.input'],
113 inputstream = FileWrapper(environ['wsgi.input'],
113 request.content_length)
114 request.content_length)
114 else:
115 else:
115 inputstream = environ['wsgi.input']
116 inputstream = environ['wsgi.input']
116
117
117 try:
118 try:
119 gitenv = os.environ
120 from rhodecode import CONFIG
121 from rhodecode.lib.base import _get_ip_addr
122 gitenv['RHODECODE_USER'] = self.username
123 gitenv['RHODECODE_CONFIG_IP'] = _get_ip_addr(environ)
124 # forget all configs
125 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
126 # we need current .ini file used to later initialize rhodecode
127 # env and connect to db
128 gitenv['RHODECODE_CONFIG_FILE'] = CONFIG['__file__']
129 opts = dict(
130 env=gitenv
131 )
118 out = subprocessio.SubprocessIOChunker(
132 out = subprocessio.SubprocessIOChunker(
119 r'git %s --stateless-rpc "%s"' % (git_command[4:],
133 r'git %s --stateless-rpc "%s"' % (git_command[4:],
120 self.content_path),
134 self.content_path),
121 inputstream=inputstream
135 inputstream=inputstream,
122 )
136 **opts
137 )
123 except EnvironmentError, e:
138 except EnvironmentError, e:
124 log.exception(e)
139 log.exception(e)
125 raise exc.HTTPExpectationFailed()
140 raise exc.HTTPExpectationFailed()
126
141
127 if git_command in [u'git-receive-pack']:
142 if git_command in [u'git-receive-pack']:
128 # updating refs manually after each push.
143 # updating refs manually after each push.
129 # Needed for pre-1.7.0.4 git clients using regular HTTP mode.
144 # Needed for pre-1.7.0.4 git clients using regular HTTP mode.
130 subprocess.call(u'git --git-dir "%s" '
145 subprocess.call(u'git --git-dir "%s" '
131 'update-server-info' % self.content_path,
146 'update-server-info' % self.content_path,
132 shell=True)
147 shell=True)
133
148
134 resp = Response()
149 resp = Response()
135 resp.content_type = 'application/x-%s-result' % git_command.encode('utf8')
150 resp.content_type = 'application/x-%s-result' % git_command.encode('utf8')
136 resp.app_iter = out
151 resp.app_iter = out
137 return resp
152 return resp
138
153
139 def __call__(self, environ, start_response):
154 def __call__(self, environ, start_response):
140 request = Request(environ)
155 request = Request(environ)
141 _path = self._get_fixedpath(request.path_info)
156 _path = self._get_fixedpath(request.path_info)
142 if _path.startswith('info/refs'):
157 if _path.startswith('info/refs'):
143 app = self.inforefs
158 app = self.inforefs
144 elif [a for a in self.valid_accepts if a in request.accept]:
159 elif [a for a in self.valid_accepts if a in request.accept]:
145 app = self.backend
160 app = self.backend
146 try:
161 try:
147 resp = app(request, environ)
162 resp = app(request, environ)
148 except exc.HTTPException, e:
163 except exc.HTTPException, e:
149 resp = e
164 resp = e
150 log.exception(e)
165 log.exception(e)
151 except Exception, e:
166 except Exception, e:
152 log.exception(e)
167 log.exception(e)
153 resp = exc.HTTPInternalServerError()
168 resp = exc.HTTPInternalServerError()
154 return resp(environ, start_response)
169 return resp(environ, start_response)
155
170
156
171
157 class GitDirectory(object):
172 class GitDirectory(object):
158
173
159 def __init__(self, repo_root, repo_name):
174 def __init__(self, repo_root, repo_name, username):
160 repo_location = os.path.join(repo_root, repo_name)
175 repo_location = os.path.join(repo_root, repo_name)
161 if not os.path.isdir(repo_location):
176 if not os.path.isdir(repo_location):
162 raise OSError(repo_location)
177 raise OSError(repo_location)
163
178
164 self.content_path = repo_location
179 self.content_path = repo_location
165 self.repo_name = repo_name
180 self.repo_name = repo_name
166 self.repo_location = repo_location
181 self.repo_location = repo_location
182 self.username = username
167
183
168 def __call__(self, environ, start_response):
184 def __call__(self, environ, start_response):
169 content_path = self.content_path
185 content_path = self.content_path
170 try:
186 try:
171 app = GitRepository(self.repo_name, content_path)
187 app = GitRepository(self.repo_name, content_path, self.username)
172 except (AssertionError, OSError):
188 except (AssertionError, OSError):
173 if os.path.isdir(os.path.join(content_path, '.git')):
189 if os.path.isdir(os.path.join(content_path, '.git')):
174 app = GitRepository(self.repo_name,
190 app = GitRepository(self.repo_name,
175 os.path.join(content_path, '.git'))
191 os.path.join(content_path, '.git'))
176 else:
192 else:
177 return exc.HTTPNotFound()(environ, start_response)
193 return exc.HTTPNotFound()(environ, start_response, self.username)
178 return app(environ, start_response)
194 return app(environ, start_response)
179
195
180
196
181 def make_wsgi_app(repo_name, repo_root):
197 def make_wsgi_app(repo_name, repo_root, username):
182 return GitDirectory(repo_root, repo_name)
198 return GitDirectory(repo_root, repo_name, username)
@@ -1,303 +1,304 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.lib.middleware.simplegit
3 rhodecode.lib.middleware.simplegit
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5
5
6 SimpleGit middleware for handling git protocol request (push/clone etc.)
6 SimpleGit middleware for handling git protocol request (push/clone etc.)
7 It's implemented with basic auth function
7 It's implemented with basic auth function
8
8
9 :created_on: Apr 28, 2010
9 :created_on: Apr 28, 2010
10 :author: marcink
10 :author: marcink
11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 :license: GPLv3, see COPYING for more details.
12 :license: GPLv3, see COPYING for more details.
13 """
13 """
14 # This program is free software: you can redistribute it and/or modify
14 # This program is free software: you can redistribute it and/or modify
15 # it under the terms of the GNU General Public License as published by
15 # it under the terms of the GNU General Public License as published by
16 # the Free Software Foundation, either version 3 of the License, or
16 # the Free Software Foundation, either version 3 of the License, or
17 # (at your option) any later version.
17 # (at your option) any later version.
18 #
18 #
19 # This program is distributed in the hope that it will be useful,
19 # This program is distributed in the hope that it will be useful,
20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 # GNU General Public License for more details.
22 # GNU General Public License for more details.
23 #
23 #
24 # You should have received a copy of the GNU General Public License
24 # You should have received a copy of the GNU General Public License
25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26
26
27 import os
27 import os
28 import re
28 import re
29 import logging
29 import logging
30 import traceback
30 import traceback
31
31
32 from dulwich import server as dulserver
32 from dulwich import server as dulserver
33
33
34
34
35 class SimpleGitUploadPackHandler(dulserver.UploadPackHandler):
35 class SimpleGitUploadPackHandler(dulserver.UploadPackHandler):
36
36
37 def handle(self):
37 def handle(self):
38 write = lambda x: self.proto.write_sideband(1, x)
38 write = lambda x: self.proto.write_sideband(1, x)
39
39
40 graph_walker = dulserver.ProtocolGraphWalker(self,
40 graph_walker = dulserver.ProtocolGraphWalker(self,
41 self.repo.object_store,
41 self.repo.object_store,
42 self.repo.get_peeled)
42 self.repo.get_peeled)
43 objects_iter = self.repo.fetch_objects(
43 objects_iter = self.repo.fetch_objects(
44 graph_walker.determine_wants, graph_walker, self.progress,
44 graph_walker.determine_wants, graph_walker, self.progress,
45 get_tagged=self.get_tagged)
45 get_tagged=self.get_tagged)
46
46
47 # Did the process short-circuit (e.g. in a stateless RPC call)? Note
47 # Did the process short-circuit (e.g. in a stateless RPC call)? Note
48 # that the client still expects a 0-object pack in most cases.
48 # that the client still expects a 0-object pack in most cases.
49 if objects_iter is None:
49 if objects_iter is None:
50 return
50 return
51
51
52 self.progress("counting objects: %d, done.\n" % len(objects_iter))
52 self.progress("counting objects: %d, done.\n" % len(objects_iter))
53 dulserver.write_pack_objects(dulserver.ProtocolFile(None, write),
53 dulserver.write_pack_objects(dulserver.ProtocolFile(None, write),
54 objects_iter)
54 objects_iter)
55 messages = []
55 messages = []
56 messages.append('thank you for using rhodecode')
56 messages.append('thank you for using rhodecode')
57
57
58 for msg in messages:
58 for msg in messages:
59 self.progress(msg + "\n")
59 self.progress(msg + "\n")
60 # we are done
60 # we are done
61 self.proto.write("0000")
61 self.proto.write("0000")
62
62
63
63
64 dulserver.DEFAULT_HANDLERS = {
64 dulserver.DEFAULT_HANDLERS = {
65 #git-ls-remote, git-clone, git-fetch and git-pull
65 #git-ls-remote, git-clone, git-fetch and git-pull
66 'git-upload-pack': SimpleGitUploadPackHandler,
66 'git-upload-pack': SimpleGitUploadPackHandler,
67 #git-push
67 #git-push
68 'git-receive-pack': dulserver.ReceivePackHandler,
68 'git-receive-pack': dulserver.ReceivePackHandler,
69 }
69 }
70
70
71 from dulwich.repo import Repo
71 # not used for now until dulwich get's fixed
72 from dulwich.web import make_wsgi_chain
72 #from dulwich.repo import Repo
73 #from dulwich.web import make_wsgi_chain
73
74
74 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
75 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
75
76
76 from rhodecode.lib.utils2 import safe_str
77 from rhodecode.lib.utils2 import safe_str
77 from rhodecode.lib.base import BaseVCSController
78 from rhodecode.lib.base import BaseVCSController
78 from rhodecode.lib.auth import get_container_username
79 from rhodecode.lib.auth import get_container_username
79 from rhodecode.lib.utils import is_valid_repo, make_ui
80 from rhodecode.lib.utils import is_valid_repo, make_ui
80 from rhodecode.model.db import User
81 from rhodecode.model.db import User, RhodeCodeUi
81
82
82 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError
83 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError
83
84
84 log = logging.getLogger(__name__)
85 log = logging.getLogger(__name__)
85
86
86
87
87 GIT_PROTO_PAT = re.compile(r'^/(.+)/(info/refs|git-upload-pack|git-receive-pack)')
88 GIT_PROTO_PAT = re.compile(r'^/(.+)/(info/refs|git-upload-pack|git-receive-pack)')
88
89
89
90
90 def is_git(environ):
91 def is_git(environ):
91 path_info = environ['PATH_INFO']
92 path_info = environ['PATH_INFO']
92 isgit_path = GIT_PROTO_PAT.match(path_info)
93 isgit_path = GIT_PROTO_PAT.match(path_info)
93 log.debug('pathinfo: %s detected as GIT %s' % (
94 log.debug('pathinfo: %s detected as GIT %s' % (
94 path_info, isgit_path != None)
95 path_info, isgit_path != None)
95 )
96 )
96 return isgit_path
97 return isgit_path
97
98
98
99
99 class SimpleGit(BaseVCSController):
100 class SimpleGit(BaseVCSController):
100
101
101 def _handle_request(self, environ, start_response):
102 def _handle_request(self, environ, start_response):
102
103
103 if not is_git(environ):
104 if not is_git(environ):
104 return self.application(environ, start_response)
105 return self.application(environ, start_response)
105
106
106 ipaddr = self._get_ip_addr(environ)
107 ipaddr = self._get_ip_addr(environ)
107 username = None
108 username = None
108 self._git_first_op = False
109 self._git_first_op = False
109 # skip passing error to error controller
110 # skip passing error to error controller
110 environ['pylons.status_code_redirect'] = True
111 environ['pylons.status_code_redirect'] = True
111
112
112 #======================================================================
113 #======================================================================
113 # EXTRACT REPOSITORY NAME FROM ENV
114 # EXTRACT REPOSITORY NAME FROM ENV
114 #======================================================================
115 #======================================================================
115 try:
116 try:
116 repo_name = self.__get_repository(environ)
117 repo_name = self.__get_repository(environ)
117 log.debug('Extracted repo name is %s' % repo_name)
118 log.debug('Extracted repo name is %s' % repo_name)
118 except:
119 except:
119 return HTTPInternalServerError()(environ, start_response)
120 return HTTPInternalServerError()(environ, start_response)
120
121
121 # quick check if that dir exists...
122 # quick check if that dir exists...
122 if is_valid_repo(repo_name, self.basepath) is False:
123 if is_valid_repo(repo_name, self.basepath) is False:
123 return HTTPNotFound()(environ, start_response)
124 return HTTPNotFound()(environ, start_response)
124
125
125 #======================================================================
126 #======================================================================
126 # GET ACTION PULL or PUSH
127 # GET ACTION PULL or PUSH
127 #======================================================================
128 #======================================================================
128 action = self.__get_action(environ)
129 action = self.__get_action(environ)
129
130
130 #======================================================================
131 #======================================================================
131 # CHECK ANONYMOUS PERMISSION
132 # CHECK ANONYMOUS PERMISSION
132 #======================================================================
133 #======================================================================
133 if action in ['pull', 'push']:
134 if action in ['pull', 'push']:
134 anonymous_user = self.__get_user('default')
135 anonymous_user = self.__get_user('default')
135 username = anonymous_user.username
136 username = anonymous_user.username
136 anonymous_perm = self._check_permission(action, anonymous_user,
137 anonymous_perm = self._check_permission(action, anonymous_user,
137 repo_name)
138 repo_name)
138
139
139 if anonymous_perm is not True or anonymous_user.active is False:
140 if anonymous_perm is not True or anonymous_user.active is False:
140 if anonymous_perm is not True:
141 if anonymous_perm is not True:
141 log.debug('Not enough credentials to access this '
142 log.debug('Not enough credentials to access this '
142 'repository as anonymous user')
143 'repository as anonymous user')
143 if anonymous_user.active is False:
144 if anonymous_user.active is False:
144 log.debug('Anonymous access is disabled, running '
145 log.debug('Anonymous access is disabled, running '
145 'authentication')
146 'authentication')
146 #==============================================================
147 #==============================================================
147 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
148 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
148 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
149 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
149 #==============================================================
150 #==============================================================
150
151
151 # Attempting to retrieve username from the container
152 # Attempting to retrieve username from the container
152 username = get_container_username(environ, self.config)
153 username = get_container_username(environ, self.config)
153
154
154 # If not authenticated by the container, running basic auth
155 # If not authenticated by the container, running basic auth
155 if not username:
156 if not username:
156 self.authenticate.realm = \
157 self.authenticate.realm = \
157 safe_str(self.config['rhodecode_realm'])
158 safe_str(self.config['rhodecode_realm'])
158 result = self.authenticate(environ)
159 result = self.authenticate(environ)
159 if isinstance(result, str):
160 if isinstance(result, str):
160 AUTH_TYPE.update(environ, 'basic')
161 AUTH_TYPE.update(environ, 'basic')
161 REMOTE_USER.update(environ, result)
162 REMOTE_USER.update(environ, result)
162 username = result
163 username = result
163 else:
164 else:
164 return result.wsgi_application(environ, start_response)
165 return result.wsgi_application(environ, start_response)
165
166
166 #==============================================================
167 #==============================================================
167 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
168 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
168 #==============================================================
169 #==============================================================
169 if action in ['pull', 'push']:
170 if action in ['pull', 'push']:
170 try:
171 try:
171 user = self.__get_user(username)
172 user = self.__get_user(username)
172 if user is None or not user.active:
173 if user is None or not user.active:
173 return HTTPForbidden()(environ, start_response)
174 return HTTPForbidden()(environ, start_response)
174 username = user.username
175 username = user.username
175 except:
176 except:
176 log.error(traceback.format_exc())
177 log.error(traceback.format_exc())
177 return HTTPInternalServerError()(environ,
178 return HTTPInternalServerError()(environ,
178 start_response)
179 start_response)
179
180
180 #check permissions for this repository
181 #check permissions for this repository
181 perm = self._check_permission(action, user, repo_name)
182 perm = self._check_permission(action, user, repo_name)
182 if perm is not True:
183 if perm is not True:
183 return HTTPForbidden()(environ, start_response)
184 return HTTPForbidden()(environ, start_response)
184 extras = {
185 extras = {
185 'ip': ipaddr,
186 'ip': ipaddr,
186 'username': username,
187 'username': username,
187 'action': action,
188 'action': action,
188 'repository': repo_name,
189 'repository': repo_name,
189 'scm': 'git',
190 'scm': 'git',
190 }
191 }
191
192
192 #===================================================================
193 #===================================================================
193 # GIT REQUEST HANDLING
194 # GIT REQUEST HANDLING
194 #===================================================================
195 #===================================================================
195 repo_path = os.path.join(safe_str(self.basepath), safe_str(repo_name))
196 repo_path = os.path.join(safe_str(self.basepath), safe_str(repo_name))
196 log.debug('Repository path is %s' % repo_path)
197 log.debug('Repository path is %s' % repo_path)
197
198
198 baseui = make_ui('db')
199 baseui = make_ui('db')
199 self.__inject_extras(repo_path, baseui, extras)
200 self.__inject_extras(repo_path, baseui, extras)
200
201
201 try:
202 try:
202 # invalidate cache on push
203 # invalidate cache on push
203 if action == 'push':
204 if action == 'push':
204 self._invalidate_cache(repo_name)
205 self._invalidate_cache(repo_name)
205 self._handle_githooks(repo_name, action, baseui, environ)
206 self._handle_githooks(repo_name, action, baseui, environ)
206
207
207 log.info('%s action on GIT repo "%s"' % (action, repo_name))
208 log.info('%s action on GIT repo "%s"' % (action, repo_name))
208 app = self.__make_app(repo_name, repo_path)
209 app = self.__make_app(repo_name, repo_path, username)
209 return app(environ, start_response)
210 return app(environ, start_response)
210 except Exception:
211 except Exception:
211 log.error(traceback.format_exc())
212 log.error(traceback.format_exc())
212 return HTTPInternalServerError()(environ, start_response)
213 return HTTPInternalServerError()(environ, start_response)
213
214
214 def __make_app(self, repo_name, repo_path):
215 def __make_app(self, repo_name, repo_path, username):
215 """
216 """
216 Make an wsgi application using dulserver
217 Make an wsgi application using dulserver
217
218
218 :param repo_name: name of the repository
219 :param repo_name: name of the repository
219 :param repo_path: full path to the repository
220 :param repo_path: full path to the repository
220 """
221 """
221
222
222 from rhodecode.lib.middleware.pygrack import make_wsgi_app
223 from rhodecode.lib.middleware.pygrack import make_wsgi_app
223 app = make_wsgi_app(
224 app = make_wsgi_app(
224 repo_root=os.path.dirname(repo_path),
225 repo_root=os.path.dirname(repo_path),
225 repo_name=repo_name,
226 repo_name=repo_name,
227 username=username,
226 )
228 )
227 return app
229 return app
228
230
229 def __get_repository(self, environ):
231 def __get_repository(self, environ):
230 """
232 """
231 Get's repository name out of PATH_INFO header
233 Get's repository name out of PATH_INFO header
232
234
233 :param environ: environ where PATH_INFO is stored
235 :param environ: environ where PATH_INFO is stored
234 """
236 """
235 try:
237 try:
236 environ['PATH_INFO'] = self._get_by_id(environ['PATH_INFO'])
238 environ['PATH_INFO'] = self._get_by_id(environ['PATH_INFO'])
237 repo_name = GIT_PROTO_PAT.match(environ['PATH_INFO']).group(1)
239 repo_name = GIT_PROTO_PAT.match(environ['PATH_INFO']).group(1)
238 except:
240 except:
239 log.error(traceback.format_exc())
241 log.error(traceback.format_exc())
240 raise
242 raise
241
243
242 return repo_name
244 return repo_name
243
245
244 def __get_user(self, username):
246 def __get_user(self, username):
245 return User.get_by_username(username)
247 return User.get_by_username(username)
246
248
247 def __get_action(self, environ):
249 def __get_action(self, environ):
248 """
250 """
249 Maps git request commands into a pull or push command.
251 Maps git request commands into a pull or push command.
250
252
251 :param environ:
253 :param environ:
252 """
254 """
253 service = environ['QUERY_STRING'].split('=')
255 service = environ['QUERY_STRING'].split('=')
254
256
255 if len(service) > 1:
257 if len(service) > 1:
256 service_cmd = service[1]
258 service_cmd = service[1]
257 mapping = {
259 mapping = {
258 'git-receive-pack': 'push',
260 'git-receive-pack': 'push',
259 'git-upload-pack': 'pull',
261 'git-upload-pack': 'pull',
260 }
262 }
261 op = mapping[service_cmd]
263 op = mapping[service_cmd]
262 self._git_stored_op = op
264 self._git_stored_op = op
263 return op
265 return op
264 else:
266 else:
265 # try to fallback to stored variable as we don't know if the last
267 # try to fallback to stored variable as we don't know if the last
266 # operation is pull/push
268 # operation is pull/push
267 op = getattr(self, '_git_stored_op', 'pull')
269 op = getattr(self, '_git_stored_op', 'pull')
268 return op
270 return op
269
271
270 def _handle_githooks(self, repo_name, action, baseui, environ):
272 def _handle_githooks(self, repo_name, action, baseui, environ):
271 from rhodecode.lib.hooks import log_pull_action, log_push_action
273 """
274 Handles pull action, push is handled by pre-receive hook
275 """
276 from rhodecode.lib.hooks import log_pull_action
272 service = environ['QUERY_STRING'].split('=')
277 service = environ['QUERY_STRING'].split('=')
273 if len(service) < 2:
278 if len(service) < 2:
274 return
279 return
275
280
276 from rhodecode.model.db import Repository
281 from rhodecode.model.db import Repository
277 _repo = Repository.get_by_repo_name(repo_name)
282 _repo = Repository.get_by_repo_name(repo_name)
278 _repo = _repo.scm_instance
283 _repo = _repo.scm_instance
279 _repo._repo.ui = baseui
284 _repo._repo.ui = baseui
280
285
281 push_hook = 'pretxnchangegroup.push_logger'
282 pull_hook = 'preoutgoing.pull_logger'
283 _hooks = dict(baseui.configitems('hooks')) or {}
286 _hooks = dict(baseui.configitems('hooks')) or {}
284 if action == 'push' and _hooks.get(push_hook):
287 if action == 'pull' and _hooks.get(RhodeCodeUi.HOOK_PULL):
285 log_push_action(ui=baseui, repo=_repo._repo)
286 elif action == 'pull' and _hooks.get(pull_hook):
287 log_pull_action(ui=baseui, repo=_repo._repo)
288 log_pull_action(ui=baseui, repo=_repo._repo)
288
289
289 def __inject_extras(self, repo_path, baseui, extras={}):
290 def __inject_extras(self, repo_path, baseui, extras={}):
290 """
291 """
291 Injects some extra params into baseui instance
292 Injects some extra params into baseui instance
292
293
293 :param baseui: baseui instance
294 :param baseui: baseui instance
294 :param extras: dict with extra params to put into baseui
295 :param extras: dict with extra params to put into baseui
295 """
296 """
296
297
297 # make our hgweb quiet so it doesn't print output
298 # make our hgweb quiet so it doesn't print output
298 baseui.setconfig('ui', 'quiet', 'true')
299 baseui.setconfig('ui', 'quiet', 'true')
299
300
300 #inject some additional parameters that will be available in ui
301 #inject some additional parameters that will be available in ui
301 #for hooks
302 #for hooks
302 for k, v in extras.items():
303 for k, v in extras.items():
303 baseui.setconfig('rhodecode_extras', k, v)
304 baseui.setconfig('rhodecode_extras', k, v)
General Comments 0
You need to be logged in to leave comments. Login now