##// END OF EJS Templates
after hooks cleanup we don't need to have ui injections into repo so we don't need to cache git repos...
marcink -
r3579:11feddcd beta
parent child Browse files
Show More
@@ -1,337 +1,336
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 from dulwich.web import LimitedInputFilter, GunzipFilter
33 from dulwich.web import LimitedInputFilter, GunzipFilter
34 from rhodecode.lib.exceptions import HTTPLockedRC
34 from rhodecode.lib.exceptions import HTTPLockedRC
35 from rhodecode.lib.hooks import pre_pull
35 from rhodecode.lib.hooks import pre_pull
36
36
37
37
38 class SimpleGitUploadPackHandler(dulserver.UploadPackHandler):
38 class SimpleGitUploadPackHandler(dulserver.UploadPackHandler):
39
39
40 def handle(self):
40 def handle(self):
41 write = lambda x: self.proto.write_sideband(1, x)
41 write = lambda x: self.proto.write_sideband(1, x)
42
42
43 graph_walker = dulserver.ProtocolGraphWalker(self,
43 graph_walker = dulserver.ProtocolGraphWalker(self,
44 self.repo.object_store,
44 self.repo.object_store,
45 self.repo.get_peeled)
45 self.repo.get_peeled)
46 objects_iter = self.repo.fetch_objects(
46 objects_iter = self.repo.fetch_objects(
47 graph_walker.determine_wants, graph_walker, self.progress,
47 graph_walker.determine_wants, graph_walker, self.progress,
48 get_tagged=self.get_tagged)
48 get_tagged=self.get_tagged)
49
49
50 # Did the process short-circuit (e.g. in a stateless RPC call)? Note
50 # Did the process short-circuit (e.g. in a stateless RPC call)? Note
51 # that the client still expects a 0-object pack in most cases.
51 # that the client still expects a 0-object pack in most cases.
52 if objects_iter is None:
52 if objects_iter is None:
53 return
53 return
54
54
55 self.progress("counting objects: %d, done.\n" % len(objects_iter))
55 self.progress("counting objects: %d, done.\n" % len(objects_iter))
56 dulserver.write_pack_objects(dulserver.ProtocolFile(None, write),
56 dulserver.write_pack_objects(dulserver.ProtocolFile(None, write),
57 objects_iter)
57 objects_iter)
58 messages = []
58 messages = []
59 messages.append('thank you for using rhodecode')
59 messages.append('thank you for using rhodecode')
60
60
61 for msg in messages:
61 for msg in messages:
62 self.progress(msg + "\n")
62 self.progress(msg + "\n")
63 # we are done
63 # we are done
64 self.proto.write("0000")
64 self.proto.write("0000")
65
65
66
66
67 dulserver.DEFAULT_HANDLERS = {
67 dulserver.DEFAULT_HANDLERS = {
68 #git-ls-remote, git-clone, git-fetch and git-pull
68 #git-ls-remote, git-clone, git-fetch and git-pull
69 'git-upload-pack': SimpleGitUploadPackHandler,
69 'git-upload-pack': SimpleGitUploadPackHandler,
70 #git-push
70 #git-push
71 'git-receive-pack': dulserver.ReceivePackHandler,
71 'git-receive-pack': dulserver.ReceivePackHandler,
72 }
72 }
73
73
74 # not used for now until dulwich get's fixed
74 # not used for now until dulwich get's fixed
75 #from dulwich.repo import Repo
75 #from dulwich.repo import Repo
76 #from dulwich.web import make_wsgi_chain
76 #from dulwich.web import make_wsgi_chain
77
77
78 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
78 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
79 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError, \
79 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError, \
80 HTTPBadRequest, HTTPNotAcceptable
80 HTTPBadRequest, HTTPNotAcceptable
81
81
82 from rhodecode.lib.utils2 import safe_str, fix_PATH, get_server_url,\
82 from rhodecode.lib.utils2 import safe_str, fix_PATH, get_server_url,\
83 _set_extras
83 _set_extras
84 from rhodecode.lib.base import BaseVCSController
84 from rhodecode.lib.base import BaseVCSController
85 from rhodecode.lib.auth import get_container_username
85 from rhodecode.lib.auth import get_container_username
86 from rhodecode.lib.utils import is_valid_repo, make_ui
86 from rhodecode.lib.utils import is_valid_repo, make_ui
87 from rhodecode.lib.compat import json
87 from rhodecode.lib.compat import json
88 from rhodecode.model.db import User, RhodeCodeUi
88 from rhodecode.model.db import User, RhodeCodeUi
89
89
90 log = logging.getLogger(__name__)
90 log = logging.getLogger(__name__)
91
91
92
92
93 GIT_PROTO_PAT = re.compile(r'^/(.+)/(info/refs|git-upload-pack|git-receive-pack)')
93 GIT_PROTO_PAT = re.compile(r'^/(.+)/(info/refs|git-upload-pack|git-receive-pack)')
94
94
95
95
96 def is_git(environ):
96 def is_git(environ):
97 path_info = environ['PATH_INFO']
97 path_info = environ['PATH_INFO']
98 isgit_path = GIT_PROTO_PAT.match(path_info)
98 isgit_path = GIT_PROTO_PAT.match(path_info)
99 log.debug('pathinfo: %s detected as GIT %s' % (
99 log.debug('pathinfo: %s detected as GIT %s' % (
100 path_info, isgit_path != None)
100 path_info, isgit_path != None)
101 )
101 )
102 return isgit_path
102 return isgit_path
103
103
104
104
105 class SimpleGit(BaseVCSController):
105 class SimpleGit(BaseVCSController):
106
106
107 def _handle_request(self, environ, start_response):
107 def _handle_request(self, environ, start_response):
108 if not is_git(environ):
108 if not is_git(environ):
109 return self.application(environ, start_response)
109 return self.application(environ, start_response)
110 if not self._check_ssl(environ, start_response):
110 if not self._check_ssl(environ, start_response):
111 return HTTPNotAcceptable('SSL REQUIRED !')(environ, start_response)
111 return HTTPNotAcceptable('SSL REQUIRED !')(environ, start_response)
112
112
113 ip_addr = self._get_ip_addr(environ)
113 ip_addr = self._get_ip_addr(environ)
114 username = None
114 username = None
115 self._git_first_op = False
115 self._git_first_op = False
116 # skip passing error to error controller
116 # skip passing error to error controller
117 environ['pylons.status_code_redirect'] = True
117 environ['pylons.status_code_redirect'] = True
118
118
119 #======================================================================
119 #======================================================================
120 # EXTRACT REPOSITORY NAME FROM ENV
120 # EXTRACT REPOSITORY NAME FROM ENV
121 #======================================================================
121 #======================================================================
122 try:
122 try:
123 repo_name = self.__get_repository(environ)
123 repo_name = self.__get_repository(environ)
124 log.debug('Extracted repo name is %s' % repo_name)
124 log.debug('Extracted repo name is %s' % repo_name)
125 except:
125 except:
126 return HTTPInternalServerError()(environ, start_response)
126 return HTTPInternalServerError()(environ, start_response)
127
127
128 # quick check if that dir exists...
128 # quick check if that dir exists...
129 if is_valid_repo(repo_name, self.basepath, 'git') is False:
129 if is_valid_repo(repo_name, self.basepath, 'git') is False:
130 return HTTPNotFound()(environ, start_response)
130 return HTTPNotFound()(environ, start_response)
131
131
132 #======================================================================
132 #======================================================================
133 # GET ACTION PULL or PUSH
133 # GET ACTION PULL or PUSH
134 #======================================================================
134 #======================================================================
135 action = self.__get_action(environ)
135 action = self.__get_action(environ)
136
136
137 #======================================================================
137 #======================================================================
138 # CHECK ANONYMOUS PERMISSION
138 # CHECK ANONYMOUS PERMISSION
139 #======================================================================
139 #======================================================================
140 if action in ['pull', 'push']:
140 if action in ['pull', 'push']:
141 anonymous_user = self.__get_user('default')
141 anonymous_user = self.__get_user('default')
142 username = anonymous_user.username
142 username = anonymous_user.username
143 anonymous_perm = self._check_permission(action, anonymous_user,
143 anonymous_perm = self._check_permission(action, anonymous_user,
144 repo_name, ip_addr)
144 repo_name, ip_addr)
145
145
146 if anonymous_perm is not True or anonymous_user.active is False:
146 if anonymous_perm is not True or anonymous_user.active is False:
147 if anonymous_perm is not True:
147 if anonymous_perm is not True:
148 log.debug('Not enough credentials to access this '
148 log.debug('Not enough credentials to access this '
149 'repository as anonymous user')
149 'repository as anonymous user')
150 if anonymous_user.active is False:
150 if anonymous_user.active is False:
151 log.debug('Anonymous access is disabled, running '
151 log.debug('Anonymous access is disabled, running '
152 'authentication')
152 'authentication')
153 #==============================================================
153 #==============================================================
154 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
154 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
155 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
155 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
156 #==============================================================
156 #==============================================================
157
157
158 # Attempting to retrieve username from the container
158 # Attempting to retrieve username from the container
159 username = get_container_username(environ, self.config)
159 username = get_container_username(environ, self.config)
160
160
161 # If not authenticated by the container, running basic auth
161 # If not authenticated by the container, running basic auth
162 if not username:
162 if not username:
163 self.authenticate.realm = \
163 self.authenticate.realm = \
164 safe_str(self.config['rhodecode_realm'])
164 safe_str(self.config['rhodecode_realm'])
165 result = self.authenticate(environ)
165 result = self.authenticate(environ)
166 if isinstance(result, str):
166 if isinstance(result, str):
167 AUTH_TYPE.update(environ, 'basic')
167 AUTH_TYPE.update(environ, 'basic')
168 REMOTE_USER.update(environ, result)
168 REMOTE_USER.update(environ, result)
169 username = result
169 username = result
170 else:
170 else:
171 return result.wsgi_application(environ, start_response)
171 return result.wsgi_application(environ, start_response)
172
172
173 #==============================================================
173 #==============================================================
174 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
174 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
175 #==============================================================
175 #==============================================================
176 try:
176 try:
177 user = self.__get_user(username)
177 user = self.__get_user(username)
178 if user is None or not user.active:
178 if user is None or not user.active:
179 return HTTPForbidden()(environ, start_response)
179 return HTTPForbidden()(environ, start_response)
180 username = user.username
180 username = user.username
181 except:
181 except:
182 log.error(traceback.format_exc())
182 log.error(traceback.format_exc())
183 return HTTPInternalServerError()(environ, start_response)
183 return HTTPInternalServerError()(environ, start_response)
184
184
185 #check permissions for this repository
185 #check permissions for this repository
186 perm = self._check_permission(action, user, repo_name, ip_addr)
186 perm = self._check_permission(action, user, repo_name, ip_addr)
187 if perm is not True:
187 if perm is not True:
188 return HTTPForbidden()(environ, start_response)
188 return HTTPForbidden()(environ, start_response)
189
189
190 # extras are injected into UI object and later available
190 # extras are injected into UI object and later available
191 # in hooks executed by rhodecode
191 # in hooks executed by rhodecode
192 from rhodecode import CONFIG
192 from rhodecode import CONFIG
193 server_url = get_server_url(environ)
193 server_url = get_server_url(environ)
194 extras = {
194 extras = {
195 'ip': ip_addr,
195 'ip': ip_addr,
196 'username': username,
196 'username': username,
197 'action': action,
197 'action': action,
198 'repository': repo_name,
198 'repository': repo_name,
199 'scm': 'git',
199 'scm': 'git',
200 'config': CONFIG['__file__'],
200 'config': CONFIG['__file__'],
201 'server_url': server_url,
201 'server_url': server_url,
202 'make_lock': None,
202 'make_lock': None,
203 'locked_by': [None, None]
203 'locked_by': [None, None]
204 }
204 }
205
205
206 #===================================================================
206 #===================================================================
207 # GIT REQUEST HANDLING
207 # GIT REQUEST HANDLING
208 #===================================================================
208 #===================================================================
209 repo_path = os.path.join(safe_str(self.basepath), safe_str(repo_name))
209 repo_path = os.path.join(safe_str(self.basepath), safe_str(repo_name))
210 log.debug('Repository path is %s' % repo_path)
210 log.debug('Repository path is %s' % repo_path)
211
211
212 # CHECK LOCKING only if it's not ANONYMOUS USER
212 # CHECK LOCKING only if it's not ANONYMOUS USER
213 if username != User.DEFAULT_USER:
213 if username != User.DEFAULT_USER:
214 log.debug('Checking locking on repository')
214 log.debug('Checking locking on repository')
215 (make_lock,
215 (make_lock,
216 locked,
216 locked,
217 locked_by) = self._check_locking_state(
217 locked_by) = self._check_locking_state(
218 environ=environ, action=action,
218 environ=environ, action=action,
219 repo=repo_name, user_id=user.user_id
219 repo=repo_name, user_id=user.user_id
220 )
220 )
221 # store the make_lock for later evaluation in hooks
221 # store the make_lock for later evaluation in hooks
222 extras.update({'make_lock': make_lock,
222 extras.update({'make_lock': make_lock,
223 'locked_by': locked_by})
223 'locked_by': locked_by})
224 # set the environ variables for this request
224 # set the environ variables for this request
225 os.environ['RC_SCM_DATA'] = json.dumps(extras)
225 os.environ['RC_SCM_DATA'] = json.dumps(extras)
226 fix_PATH()
226 fix_PATH()
227 log.debug('HOOKS extras is %s' % extras)
227 log.debug('HOOKS extras is %s' % extras)
228 baseui = make_ui('db')
228 baseui = make_ui('db')
229 self.__inject_extras(repo_path, baseui, extras)
229 self.__inject_extras(repo_path, baseui, extras)
230
230
231 try:
231 try:
232 self._handle_githooks(repo_name, action, baseui, environ)
232 self._handle_githooks(repo_name, action, baseui, environ)
233 log.info('%s action on GIT repo "%s" by "%s" from %s' %
233 log.info('%s action on GIT repo "%s" by "%s" from %s' %
234 (action, repo_name, username, ip_addr))
234 (action, repo_name, username, ip_addr))
235 app = self.__make_app(repo_name, repo_path, extras)
235 app = self.__make_app(repo_name, repo_path, extras)
236 return app(environ, start_response)
236 return app(environ, start_response)
237 except HTTPLockedRC, e:
237 except HTTPLockedRC, e:
238 _code = CONFIG.get('lock_ret_code')
238 _code = CONFIG.get('lock_ret_code')
239 log.debug('Repository LOCKED ret code %s!' % (_code))
239 log.debug('Repository LOCKED ret code %s!' % (_code))
240 return e(environ, start_response)
240 return e(environ, start_response)
241 except Exception:
241 except Exception:
242 log.error(traceback.format_exc())
242 log.error(traceback.format_exc())
243 return HTTPInternalServerError()(environ, start_response)
243 return HTTPInternalServerError()(environ, start_response)
244 finally:
244 finally:
245 # invalidate cache on push
245 # invalidate cache on push
246 if action == 'push':
246 if action == 'push':
247 self._invalidate_cache(repo_name)
247 self._invalidate_cache(repo_name)
248
248
249 def __make_app(self, repo_name, repo_path, extras):
249 def __make_app(self, repo_name, repo_path, extras):
250 """
250 """
251 Make an wsgi application using dulserver
251 Make an wsgi application using dulserver
252
252
253 :param repo_name: name of the repository
253 :param repo_name: name of the repository
254 :param repo_path: full path to the repository
254 :param repo_path: full path to the repository
255 """
255 """
256
256
257 from rhodecode.lib.middleware.pygrack import make_wsgi_app
257 from rhodecode.lib.middleware.pygrack import make_wsgi_app
258 app = make_wsgi_app(
258 app = make_wsgi_app(
259 repo_root=safe_str(self.basepath),
259 repo_root=safe_str(self.basepath),
260 repo_name=repo_name,
260 repo_name=repo_name,
261 extras=extras,
261 extras=extras,
262 )
262 )
263 app = GunzipFilter(LimitedInputFilter(app))
263 app = GunzipFilter(LimitedInputFilter(app))
264 return app
264 return app
265
265
266 def __get_repository(self, environ):
266 def __get_repository(self, environ):
267 """
267 """
268 Get's repository name out of PATH_INFO header
268 Get's repository name out of PATH_INFO header
269
269
270 :param environ: environ where PATH_INFO is stored
270 :param environ: environ where PATH_INFO is stored
271 """
271 """
272 try:
272 try:
273 environ['PATH_INFO'] = self._get_by_id(environ['PATH_INFO'])
273 environ['PATH_INFO'] = self._get_by_id(environ['PATH_INFO'])
274 repo_name = GIT_PROTO_PAT.match(environ['PATH_INFO']).group(1)
274 repo_name = GIT_PROTO_PAT.match(environ['PATH_INFO']).group(1)
275 except:
275 except:
276 log.error(traceback.format_exc())
276 log.error(traceback.format_exc())
277 raise
277 raise
278
278
279 return repo_name
279 return repo_name
280
280
281 def __get_user(self, username):
281 def __get_user(self, username):
282 return User.get_by_username(username)
282 return User.get_by_username(username)
283
283
284 def __get_action(self, environ):
284 def __get_action(self, environ):
285 """
285 """
286 Maps git request commands into a pull or push command.
286 Maps git request commands into a pull or push command.
287
287
288 :param environ:
288 :param environ:
289 """
289 """
290 service = environ['QUERY_STRING'].split('=')
290 service = environ['QUERY_STRING'].split('=')
291
291
292 if len(service) > 1:
292 if len(service) > 1:
293 service_cmd = service[1]
293 service_cmd = service[1]
294 mapping = {
294 mapping = {
295 'git-receive-pack': 'push',
295 'git-receive-pack': 'push',
296 'git-upload-pack': 'pull',
296 'git-upload-pack': 'pull',
297 }
297 }
298 op = mapping[service_cmd]
298 op = mapping[service_cmd]
299 self._git_stored_op = op
299 self._git_stored_op = op
300 return op
300 return op
301 else:
301 else:
302 # try to fallback to stored variable as we don't know if the last
302 # try to fallback to stored variable as we don't know if the last
303 # operation is pull/push
303 # operation is pull/push
304 op = getattr(self, '_git_stored_op', 'pull')
304 op = getattr(self, '_git_stored_op', 'pull')
305 return op
305 return op
306
306
307 def _handle_githooks(self, repo_name, action, baseui, environ):
307 def _handle_githooks(self, repo_name, action, baseui, environ):
308 """
308 """
309 Handles pull action, push is handled by post-receive hook
309 Handles pull action, push is handled by post-receive hook
310 """
310 """
311 from rhodecode.lib.hooks import log_pull_action
311 from rhodecode.lib.hooks import log_pull_action
312 service = environ['QUERY_STRING'].split('=')
312 service = environ['QUERY_STRING'].split('=')
313
313
314 if len(service) < 2:
314 if len(service) < 2:
315 return
315 return
316
316
317 from rhodecode.model.db import Repository
317 from rhodecode.model.db import Repository
318 _repo = Repository.get_by_repo_name(repo_name)
318 _repo = Repository.get_by_repo_name(repo_name)
319 _repo = _repo.scm_instance
319 _repo = _repo.scm_instance
320 _repo._repo.ui = baseui
321
320
322 _hooks = dict(baseui.configitems('hooks')) or {}
321 _hooks = dict(baseui.configitems('hooks')) or {}
323 if action == 'pull':
322 if action == 'pull':
324 # stupid git, emulate pre-pull hook !
323 # stupid git, emulate pre-pull hook !
325 pre_pull(ui=baseui, repo=_repo._repo)
324 pre_pull(ui=baseui, repo=_repo._repo)
326 if action == 'pull' and _hooks.get(RhodeCodeUi.HOOK_PULL):
325 if action == 'pull' and _hooks.get(RhodeCodeUi.HOOK_PULL):
327 log_pull_action(ui=baseui, repo=_repo._repo)
326 log_pull_action(ui=baseui, repo=_repo._repo)
328
327
329 def __inject_extras(self, repo_path, baseui, extras={}):
328 def __inject_extras(self, repo_path, baseui, extras={}):
330 """
329 """
331 Injects some extra params into baseui instance
330 Injects some extra params into baseui instance
332
331
333 :param baseui: baseui instance
332 :param baseui: baseui instance
334 :param extras: dict with extra params to put into baseui
333 :param extras: dict with extra params to put into baseui
335 """
334 """
336
335
337 _set_extras(extras)
336 _set_extras(extras)
@@ -1,691 +1,693
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 vcs.backends.git
3 vcs.backends.git
4 ~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~
5
5
6 Git backend implementation.
6 Git backend implementation.
7
7
8 :created_on: Apr 8, 2010
8 :created_on: Apr 8, 2010
9 :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
9 :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
10 """
10 """
11
11
12 import os
12 import os
13 import re
13 import re
14 import time
14 import time
15 import posixpath
15 import posixpath
16 import logging
16 import logging
17 import traceback
17 import traceback
18 import urllib
18 import urllib
19 import urllib2
19 import urllib2
20 from dulwich.repo import Repo, NotGitRepository
20 from dulwich.repo import Repo, NotGitRepository
21 from dulwich.objects import Tag
21 from dulwich.objects import Tag
22 from string import Template
22 from string import Template
23
23
24 import rhodecode
24 import rhodecode
25 from rhodecode.lib.vcs.backends.base import BaseRepository
25 from rhodecode.lib.vcs.backends.base import BaseRepository
26 from rhodecode.lib.vcs.exceptions import BranchDoesNotExistError
26 from rhodecode.lib.vcs.exceptions import BranchDoesNotExistError
27 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError
27 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError
28 from rhodecode.lib.vcs.exceptions import EmptyRepositoryError
28 from rhodecode.lib.vcs.exceptions import EmptyRepositoryError
29 from rhodecode.lib.vcs.exceptions import RepositoryError
29 from rhodecode.lib.vcs.exceptions import RepositoryError
30 from rhodecode.lib.vcs.exceptions import TagAlreadyExistError
30 from rhodecode.lib.vcs.exceptions import TagAlreadyExistError
31 from rhodecode.lib.vcs.exceptions import TagDoesNotExistError
31 from rhodecode.lib.vcs.exceptions import TagDoesNotExistError
32 from rhodecode.lib.vcs.utils import safe_unicode, makedate, date_fromtimestamp
32 from rhodecode.lib.vcs.utils import safe_unicode, makedate, date_fromtimestamp
33 from rhodecode.lib.vcs.utils.lazy import LazyProperty, ThreadLocalLazyProperty
33 from rhodecode.lib.vcs.utils.lazy import LazyProperty, ThreadLocalLazyProperty
34 from rhodecode.lib.vcs.utils.ordered_dict import OrderedDict
34 from rhodecode.lib.vcs.utils.ordered_dict import OrderedDict
35 from rhodecode.lib.vcs.utils.paths import abspath
35 from rhodecode.lib.vcs.utils.paths import abspath
36 from rhodecode.lib.vcs.utils.paths import get_user_home
36 from rhodecode.lib.vcs.utils.paths import get_user_home
37 from .workdir import GitWorkdir
37 from .workdir import GitWorkdir
38 from .changeset import GitChangeset
38 from .changeset import GitChangeset
39 from .inmemory import GitInMemoryChangeset
39 from .inmemory import GitInMemoryChangeset
40 from .config import ConfigFile
40 from .config import ConfigFile
41 from rhodecode.lib import subprocessio
41 from rhodecode.lib import subprocessio
42
42
43
43
44 log = logging.getLogger(__name__)
44 log = logging.getLogger(__name__)
45
45
46
46
47 class GitRepository(BaseRepository):
47 class GitRepository(BaseRepository):
48 """
48 """
49 Git repository backend.
49 Git repository backend.
50 """
50 """
51 DEFAULT_BRANCH_NAME = 'master'
51 DEFAULT_BRANCH_NAME = 'master'
52 scm = 'git'
52 scm = 'git'
53
53
54 def __init__(self, repo_path, create=False, src_url=None,
54 def __init__(self, repo_path, create=False, src_url=None,
55 update_after_clone=False, bare=False):
55 update_after_clone=False, bare=False):
56
56
57 self.path = abspath(repo_path)
57 self.path = abspath(repo_path)
58 repo = self._get_repo(create, src_url, update_after_clone, bare)
58 repo = self._get_repo(create, src_url, update_after_clone, bare)
59 self.bare = repo.bare
59 self.bare = repo.bare
60
60
61 self._config_files = [
61 self._config_files = [
62 bare and abspath(self.path, 'config')
62 bare and abspath(self.path, 'config')
63 or abspath(self.path, '.git', 'config'),
63 or abspath(self.path, '.git', 'config'),
64 abspath(get_user_home(), '.gitconfig'),
64 abspath(get_user_home(), '.gitconfig'),
65 ]
65 ]
66
66
67 @ThreadLocalLazyProperty
67 @property
68 def _repo(self):
68 def _repo(self):
69 return Repo(self.path)
69 return Repo(self.path)
70
70
71 @property
71 @property
72 def head(self):
72 def head(self):
73 try:
73 try:
74 return self._repo.head()
74 return self._repo.head()
75 except KeyError:
75 except KeyError:
76 return None
76 return None
77
77
78 @LazyProperty
78 @LazyProperty
79 def revisions(self):
79 def revisions(self):
80 """
80 """
81 Returns list of revisions' ids, in ascending order. Being lazy
81 Returns list of revisions' ids, in ascending order. Being lazy
82 attribute allows external tools to inject shas from cache.
82 attribute allows external tools to inject shas from cache.
83 """
83 """
84 return self._get_all_revisions()
84 return self._get_all_revisions()
85
85
86 @classmethod
86 @classmethod
87 def _run_git_command(cls, cmd, **opts):
87 def _run_git_command(cls, cmd, **opts):
88 """
88 """
89 Runs given ``cmd`` as git command and returns tuple
89 Runs given ``cmd`` as git command and returns tuple
90 (stdout, stderr).
90 (stdout, stderr).
91
91
92 :param cmd: git command to be executed
92 :param cmd: git command to be executed
93 :param opts: env options to pass into Subprocess command
93 :param opts: env options to pass into Subprocess command
94 """
94 """
95
95
96 if '_bare' in opts:
96 if '_bare' in opts:
97 _copts = []
97 _copts = []
98 del opts['_bare']
98 del opts['_bare']
99 else:
99 else:
100 _copts = ['-c', 'core.quotepath=false', ]
100 _copts = ['-c', 'core.quotepath=false', ]
101 safe_call = False
101 safe_call = False
102 if '_safe' in opts:
102 if '_safe' in opts:
103 #no exc on failure
103 #no exc on failure
104 del opts['_safe']
104 del opts['_safe']
105 safe_call = True
105 safe_call = True
106
106
107 _str_cmd = False
107 _str_cmd = False
108 if isinstance(cmd, basestring):
108 if isinstance(cmd, basestring):
109 cmd = [cmd]
109 cmd = [cmd]
110 _str_cmd = True
110 _str_cmd = True
111
111
112 gitenv = os.environ
112 gitenv = os.environ
113 # need to clean fix GIT_DIR !
113 # need to clean fix GIT_DIR !
114 if 'GIT_DIR' in gitenv:
114 if 'GIT_DIR' in gitenv:
115 del gitenv['GIT_DIR']
115 del gitenv['GIT_DIR']
116 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
116 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
117
117
118 _git_path = rhodecode.CONFIG.get('git_path', 'git')
118 _git_path = rhodecode.CONFIG.get('git_path', 'git')
119 cmd = [_git_path] + _copts + cmd
119 cmd = [_git_path] + _copts + cmd
120 if _str_cmd:
120 if _str_cmd:
121 cmd = ' '.join(cmd)
121 cmd = ' '.join(cmd)
122 try:
122 try:
123 _opts = dict(
123 _opts = dict(
124 env=gitenv,
124 env=gitenv,
125 shell=False,
125 shell=False,
126 )
126 )
127 _opts.update(opts)
127 _opts.update(opts)
128 p = subprocessio.SubprocessIOChunker(cmd, **_opts)
128 p = subprocessio.SubprocessIOChunker(cmd, **_opts)
129 except (EnvironmentError, OSError), err:
129 except (EnvironmentError, OSError), err:
130 tb_err = ("Couldn't run git command (%s).\n"
130 tb_err = ("Couldn't run git command (%s).\n"
131 "Original error was:%s\n" % (cmd, err))
131 "Original error was:%s\n" % (cmd, err))
132 log.error(tb_err)
132 log.error(tb_err)
133 if safe_call:
133 if safe_call:
134 return '', err
134 return '', err
135 else:
135 else:
136 raise RepositoryError(tb_err)
136 raise RepositoryError(tb_err)
137
137
138 return ''.join(p.output), ''.join(p.error)
138 return ''.join(p.output), ''.join(p.error)
139
139
140 def run_git_command(self, cmd):
140 def run_git_command(self, cmd):
141 opts = {}
141 opts = {}
142 if os.path.isdir(self.path):
142 if os.path.isdir(self.path):
143 opts['cwd'] = self.path
143 opts['cwd'] = self.path
144 return self._run_git_command(cmd, **opts)
144 return self._run_git_command(cmd, **opts)
145
145
146 @classmethod
146 @classmethod
147 def _check_url(cls, url):
147 def _check_url(cls, url):
148 """
148 """
149 Functon will check given url and try to verify if it's a valid
149 Functon will check given url and try to verify if it's a valid
150 link. Sometimes it may happened that mercurial will issue basic
150 link. Sometimes it may happened that mercurial will issue basic
151 auth request that can cause whole API to hang when used from python
151 auth request that can cause whole API to hang when used from python
152 or other external calls.
152 or other external calls.
153
153
154 On failures it'll raise urllib2.HTTPError
154 On failures it'll raise urllib2.HTTPError
155 """
155 """
156 from mercurial.util import url as Url
156 from mercurial.util import url as Url
157
157
158 # those authnadlers are patched for python 2.6.5 bug an
158 # those authnadlers are patched for python 2.6.5 bug an
159 # infinit looping when given invalid resources
159 # infinit looping when given invalid resources
160 from mercurial.url import httpbasicauthhandler, httpdigestauthhandler
160 from mercurial.url import httpbasicauthhandler, httpdigestauthhandler
161
161
162 # check first if it's not an local url
162 # check first if it's not an local url
163 if os.path.isdir(url) or url.startswith('file:'):
163 if os.path.isdir(url) or url.startswith('file:'):
164 return True
164 return True
165
165
166 if('+' in url[:url.find('://')]):
166 if('+' in url[:url.find('://')]):
167 url = url[url.find('+') + 1:]
167 url = url[url.find('+') + 1:]
168
168
169 handlers = []
169 handlers = []
170 test_uri, authinfo = Url(url).authinfo()
170 test_uri, authinfo = Url(url).authinfo()
171 if not test_uri.endswith('info/refs'):
171 if not test_uri.endswith('info/refs'):
172 test_uri = test_uri.rstrip('/') + '/info/refs'
172 test_uri = test_uri.rstrip('/') + '/info/refs'
173 if authinfo:
173 if authinfo:
174 #create a password manager
174 #create a password manager
175 passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
175 passmgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
176 passmgr.add_password(*authinfo)
176 passmgr.add_password(*authinfo)
177
177
178 handlers.extend((httpbasicauthhandler(passmgr),
178 handlers.extend((httpbasicauthhandler(passmgr),
179 httpdigestauthhandler(passmgr)))
179 httpdigestauthhandler(passmgr)))
180
180
181 o = urllib2.build_opener(*handlers)
181 o = urllib2.build_opener(*handlers)
182 o.addheaders = [('User-Agent', 'git/1.7.8.0')] # fake some git
182 o.addheaders = [('User-Agent', 'git/1.7.8.0')] # fake some git
183
183
184 q = {"service": 'git-upload-pack'}
184 q = {"service": 'git-upload-pack'}
185 qs = '?%s' % urllib.urlencode(q)
185 qs = '?%s' % urllib.urlencode(q)
186 cu = "%s%s" % (test_uri, qs)
186 cu = "%s%s" % (test_uri, qs)
187 req = urllib2.Request(cu, None, {})
187 req = urllib2.Request(cu, None, {})
188
188
189 try:
189 try:
190 resp = o.open(req)
190 resp = o.open(req)
191 return resp.code == 200
191 return resp.code == 200
192 except Exception, e:
192 except Exception, e:
193 # means it cannot be cloned
193 # means it cannot be cloned
194 raise urllib2.URLError("[%s] %s" % (url, e))
194 raise urllib2.URLError("[%s] %s" % (url, e))
195
195
196 def _get_repo(self, create, src_url=None, update_after_clone=False,
196 def _get_repo(self, create, src_url=None, update_after_clone=False,
197 bare=False):
197 bare=False):
198 if create and os.path.exists(self.path):
198 if create and os.path.exists(self.path):
199 raise RepositoryError("Location already exist")
199 raise RepositoryError("Location already exist")
200 if src_url and not create:
200 if src_url and not create:
201 raise RepositoryError("Create should be set to True if src_url is "
201 raise RepositoryError("Create should be set to True if src_url is "
202 "given (clone operation creates repository)")
202 "given (clone operation creates repository)")
203 try:
203 try:
204 if create and src_url:
204 if create and src_url:
205 GitRepository._check_url(src_url)
205 GitRepository._check_url(src_url)
206 self.clone(src_url, update_after_clone, bare)
206 self.clone(src_url, update_after_clone, bare)
207 return Repo(self.path)
207 return Repo(self.path)
208 elif create:
208 elif create:
209 os.mkdir(self.path)
209 os.mkdir(self.path)
210 if bare:
210 if bare:
211 return Repo.init_bare(self.path)
211 return Repo.init_bare(self.path)
212 else:
212 else:
213 return Repo.init(self.path)
213 return Repo.init(self.path)
214 else:
214 else:
215 return self._repo
215 return self._repo
216 except (NotGitRepository, OSError), err:
216 except (NotGitRepository, OSError), err:
217 raise RepositoryError(err)
217 raise RepositoryError(err)
218
218
219 def _get_all_revisions(self):
219 def _get_all_revisions(self):
220 # we must check if this repo is not empty, since later command
220 # we must check if this repo is not empty, since later command
221 # fails if it is. And it's cheaper to ask than throw the subprocess
221 # fails if it is. And it's cheaper to ask than throw the subprocess
222 # errors
222 # errors
223 try:
223 try:
224 self._repo.head()
224 self._repo.head()
225 except KeyError:
225 except KeyError:
226 return []
226 return []
227 rev_filter = _git_path = rhodecode.CONFIG.get('git_rev_filter',
227 rev_filter = _git_path = rhodecode.CONFIG.get('git_rev_filter',
228 '--all').strip()
228 '--all').strip()
229 cmd = 'rev-list %s --reverse --date-order' % (rev_filter)
229 cmd = 'rev-list %s --reverse --date-order' % (rev_filter)
230 try:
230 try:
231 so, se = self.run_git_command(cmd)
231 so, se = self.run_git_command(cmd)
232 except RepositoryError:
232 except RepositoryError:
233 # Can be raised for empty repositories
233 # Can be raised for empty repositories
234 return []
234 return []
235 return so.splitlines()
235 return so.splitlines()
236
236
237 def _get_all_revisions2(self):
237 def _get_all_revisions2(self):
238 #alternate implementation using dulwich
238 #alternate implementation using dulwich
239 includes = [x[1][0] for x in self._parsed_refs.iteritems()
239 includes = [x[1][0] for x in self._parsed_refs.iteritems()
240 if x[1][1] != 'T']
240 if x[1][1] != 'T']
241 return [c.commit.id for c in self._repo.get_walker(include=includes)]
241 return [c.commit.id for c in self._repo.get_walker(include=includes)]
242
242
243 def _get_revision(self, revision):
243 def _get_revision(self, revision):
244 """
244 """
245 For git backend we always return integer here. This way we ensure
245 For git backend we always return integer here. This way we ensure
246 that changset's revision attribute would become integer.
246 that changset's revision attribute would become integer.
247 """
247 """
248 pattern = re.compile(r'^[[0-9a-fA-F]{12}|[0-9a-fA-F]{40}]$')
248 pattern = re.compile(r'^[[0-9a-fA-F]{12}|[0-9a-fA-F]{40}]$')
249 is_bstr = lambda o: isinstance(o, (str, unicode))
249 is_bstr = lambda o: isinstance(o, (str, unicode))
250 is_null = lambda o: len(o) == revision.count('0')
250 is_null = lambda o: len(o) == revision.count('0')
251
251
252 if len(self.revisions) == 0:
252 if len(self.revisions) == 0:
253 raise EmptyRepositoryError("There are no changesets yet")
253 raise EmptyRepositoryError("There are no changesets yet")
254
254
255 if revision in (None, '', 'tip', 'HEAD', 'head', -1):
255 if revision in (None, '', 'tip', 'HEAD', 'head', -1):
256 revision = self.revisions[-1]
256 revision = self.revisions[-1]
257
257
258 if ((is_bstr(revision) and revision.isdigit() and len(revision) < 12)
258 if ((is_bstr(revision) and revision.isdigit() and len(revision) < 12)
259 or isinstance(revision, int) or is_null(revision)):
259 or isinstance(revision, int) or is_null(revision)):
260 try:
260 try:
261 revision = self.revisions[int(revision)]
261 revision = self.revisions[int(revision)]
262 except:
262 except:
263 raise ChangesetDoesNotExistError("Revision %s does not exist "
263 raise ChangesetDoesNotExistError("Revision %s does not exist "
264 "for this repository" % (revision))
264 "for this repository" % (revision))
265
265
266 elif is_bstr(revision):
266 elif is_bstr(revision):
267 # get by branch/tag name
267 # get by branch/tag name
268 _ref_revision = self._parsed_refs.get(revision)
268 _ref_revision = self._parsed_refs.get(revision)
269 _tags_shas = self.tags.values()
269 _tags_shas = self.tags.values()
270 if _ref_revision: # and _ref_revision[1] in ['H', 'RH', 'T']:
270 if _ref_revision: # and _ref_revision[1] in ['H', 'RH', 'T']:
271 return _ref_revision[0]
271 return _ref_revision[0]
272
272
273 # maybe it's a tag ? we don't have them in self.revisions
273 # maybe it's a tag ? we don't have them in self.revisions
274 elif revision in _tags_shas:
274 elif revision in _tags_shas:
275 return _tags_shas[_tags_shas.index(revision)]
275 return _tags_shas[_tags_shas.index(revision)]
276
276
277 elif not pattern.match(revision) or revision not in self.revisions:
277 elif not pattern.match(revision) or revision not in self.revisions:
278 raise ChangesetDoesNotExistError("Revision %s does not exist "
278 raise ChangesetDoesNotExistError("Revision %s does not exist "
279 "for this repository" % (revision))
279 "for this repository" % (revision))
280
280
281 # Ensure we return full id
281 # Ensure we return full id
282 if not pattern.match(str(revision)):
282 if not pattern.match(str(revision)):
283 raise ChangesetDoesNotExistError("Given revision %s not recognized"
283 raise ChangesetDoesNotExistError("Given revision %s not recognized"
284 % revision)
284 % revision)
285 return revision
285 return revision
286
286
287 def _get_archives(self, archive_name='tip'):
287 def _get_archives(self, archive_name='tip'):
288
288
289 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
289 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
290 yield {"type": i[0], "extension": i[1], "node": archive_name}
290 yield {"type": i[0], "extension": i[1], "node": archive_name}
291
291
292 def _get_url(self, url):
292 def _get_url(self, url):
293 """
293 """
294 Returns normalized url. If schema is not given, would fall to
294 Returns normalized url. If schema is not given, would fall to
295 filesystem (``file:///``) schema.
295 filesystem (``file:///``) schema.
296 """
296 """
297 url = str(url)
297 url = str(url)
298 if url != 'default' and not '://' in url:
298 if url != 'default' and not '://' in url:
299 url = ':///'.join(('file', url))
299 url = ':///'.join(('file', url))
300 return url
300 return url
301
301
302 def get_hook_location(self):
302 def get_hook_location(self):
303 """
303 """
304 returns absolute path to location where hooks are stored
304 returns absolute path to location where hooks are stored
305 """
305 """
306 loc = os.path.join(self.path, 'hooks')
306 loc = os.path.join(self.path, 'hooks')
307 if not self.bare:
307 if not self.bare:
308 loc = os.path.join(self.path, '.git', 'hooks')
308 loc = os.path.join(self.path, '.git', 'hooks')
309 return loc
309 return loc
310
310
311 @LazyProperty
311 @LazyProperty
312 def name(self):
312 def name(self):
313 return os.path.basename(self.path)
313 return os.path.basename(self.path)
314
314
315 @LazyProperty
315 @LazyProperty
316 def last_change(self):
316 def last_change(self):
317 """
317 """
318 Returns last change made on this repository as datetime object
318 Returns last change made on this repository as datetime object
319 """
319 """
320 return date_fromtimestamp(self._get_mtime(), makedate()[1])
320 return date_fromtimestamp(self._get_mtime(), makedate()[1])
321
321
322 def _get_mtime(self):
322 def _get_mtime(self):
323 try:
323 try:
324 return time.mktime(self.get_changeset().date.timetuple())
324 return time.mktime(self.get_changeset().date.timetuple())
325 except RepositoryError:
325 except RepositoryError:
326 idx_loc = '' if self.bare else '.git'
326 idx_loc = '' if self.bare else '.git'
327 # fallback to filesystem
327 # fallback to filesystem
328 in_path = os.path.join(self.path, idx_loc, "index")
328 in_path = os.path.join(self.path, idx_loc, "index")
329 he_path = os.path.join(self.path, idx_loc, "HEAD")
329 he_path = os.path.join(self.path, idx_loc, "HEAD")
330 if os.path.exists(in_path):
330 if os.path.exists(in_path):
331 return os.stat(in_path).st_mtime
331 return os.stat(in_path).st_mtime
332 else:
332 else:
333 return os.stat(he_path).st_mtime
333 return os.stat(he_path).st_mtime
334
334
335 @LazyProperty
335 @LazyProperty
336 def description(self):
336 def description(self):
337 idx_loc = '' if self.bare else '.git'
337 idx_loc = '' if self.bare else '.git'
338 undefined_description = u'unknown'
338 undefined_description = u'unknown'
339 description_path = os.path.join(self.path, idx_loc, 'description')
339 description_path = os.path.join(self.path, idx_loc, 'description')
340 if os.path.isfile(description_path):
340 if os.path.isfile(description_path):
341 return safe_unicode(open(description_path).read())
341 return safe_unicode(open(description_path).read())
342 else:
342 else:
343 return undefined_description
343 return undefined_description
344
344
345 @LazyProperty
345 @LazyProperty
346 def contact(self):
346 def contact(self):
347 undefined_contact = u'Unknown'
347 undefined_contact = u'Unknown'
348 return undefined_contact
348 return undefined_contact
349
349
350 @property
350 @property
351 def branches(self):
351 def branches(self):
352 if not self.revisions:
352 if not self.revisions:
353 return {}
353 return {}
354 sortkey = lambda ctx: ctx[0]
354 sortkey = lambda ctx: ctx[0]
355 _branches = [(x[0], x[1][0])
355 _branches = [(x[0], x[1][0])
356 for x in self._parsed_refs.iteritems() if x[1][1] == 'H']
356 for x in self._parsed_refs.iteritems() if x[1][1] == 'H']
357 return OrderedDict(sorted(_branches, key=sortkey, reverse=False))
357 return OrderedDict(sorted(_branches, key=sortkey, reverse=False))
358
358
359 @LazyProperty
359 @LazyProperty
360 def tags(self):
360 def tags(self):
361 return self._get_tags()
361 return self._get_tags()
362
362
363 def _get_tags(self):
363 def _get_tags(self):
364 if not self.revisions:
364 if not self.revisions:
365 return {}
365 return {}
366
366
367 sortkey = lambda ctx: ctx[0]
367 sortkey = lambda ctx: ctx[0]
368 _tags = [(x[0], x[1][0])
368 _tags = [(x[0], x[1][0])
369 for x in self._parsed_refs.iteritems() if x[1][1] == 'T']
369 for x in self._parsed_refs.iteritems() if x[1][1] == 'T']
370 return OrderedDict(sorted(_tags, key=sortkey, reverse=True))
370 return OrderedDict(sorted(_tags, key=sortkey, reverse=True))
371
371
372 def tag(self, name, user, revision=None, message=None, date=None,
372 def tag(self, name, user, revision=None, message=None, date=None,
373 **kwargs):
373 **kwargs):
374 """
374 """
375 Creates and returns a tag for the given ``revision``.
375 Creates and returns a tag for the given ``revision``.
376
376
377 :param name: name for new tag
377 :param name: name for new tag
378 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
378 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
379 :param revision: changeset id for which new tag would be created
379 :param revision: changeset id for which new tag would be created
380 :param message: message of the tag's commit
380 :param message: message of the tag's commit
381 :param date: date of tag's commit
381 :param date: date of tag's commit
382
382
383 :raises TagAlreadyExistError: if tag with same name already exists
383 :raises TagAlreadyExistError: if tag with same name already exists
384 """
384 """
385 if name in self.tags:
385 if name in self.tags:
386 raise TagAlreadyExistError("Tag %s already exists" % name)
386 raise TagAlreadyExistError("Tag %s already exists" % name)
387 changeset = self.get_changeset(revision)
387 changeset = self.get_changeset(revision)
388 message = message or "Added tag %s for commit %s" % (name,
388 message = message or "Added tag %s for commit %s" % (name,
389 changeset.raw_id)
389 changeset.raw_id)
390 self._repo.refs["refs/tags/%s" % name] = changeset._commit.id
390 self._repo.refs["refs/tags/%s" % name] = changeset._commit.id
391
391
392 self._parsed_refs = self._get_parsed_refs()
392 self._parsed_refs = self._get_parsed_refs()
393 self.tags = self._get_tags()
393 self.tags = self._get_tags()
394 return changeset
394 return changeset
395
395
396 def remove_tag(self, name, user, message=None, date=None):
396 def remove_tag(self, name, user, message=None, date=None):
397 """
397 """
398 Removes tag with the given ``name``.
398 Removes tag with the given ``name``.
399
399
400 :param name: name of the tag to be removed
400 :param name: name of the tag to be removed
401 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
401 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
402 :param message: message of the tag's removal commit
402 :param message: message of the tag's removal commit
403 :param date: date of tag's removal commit
403 :param date: date of tag's removal commit
404
404
405 :raises TagDoesNotExistError: if tag with given name does not exists
405 :raises TagDoesNotExistError: if tag with given name does not exists
406 """
406 """
407 if name not in self.tags:
407 if name not in self.tags:
408 raise TagDoesNotExistError("Tag %s does not exist" % name)
408 raise TagDoesNotExistError("Tag %s does not exist" % name)
409 tagpath = posixpath.join(self._repo.refs.path, 'refs', 'tags', name)
409 tagpath = posixpath.join(self._repo.refs.path, 'refs', 'tags', name)
410 try:
410 try:
411 os.remove(tagpath)
411 os.remove(tagpath)
412 self._parsed_refs = self._get_parsed_refs()
412 self._parsed_refs = self._get_parsed_refs()
413 self.tags = self._get_tags()
413 self.tags = self._get_tags()
414 except OSError, e:
414 except OSError, e:
415 raise RepositoryError(e.strerror)
415 raise RepositoryError(e.strerror)
416
416
417 @LazyProperty
417 @LazyProperty
418 def _parsed_refs(self):
418 def _parsed_refs(self):
419 return self._get_parsed_refs()
419 return self._get_parsed_refs()
420
420
421 def _get_parsed_refs(self):
421 def _get_parsed_refs(self):
422 refs = self._repo.get_refs()
422 # cache the property
423 _repo = self._repo
424 refs = _repo.get_refs()
423 keys = [('refs/heads/', 'H'),
425 keys = [('refs/heads/', 'H'),
424 ('refs/remotes/origin/', 'RH'),
426 ('refs/remotes/origin/', 'RH'),
425 ('refs/tags/', 'T')]
427 ('refs/tags/', 'T')]
426 _refs = {}
428 _refs = {}
427 for ref, sha in refs.iteritems():
429 for ref, sha in refs.iteritems():
428 for k, type_ in keys:
430 for k, type_ in keys:
429 if ref.startswith(k):
431 if ref.startswith(k):
430 _key = ref[len(k):]
432 _key = ref[len(k):]
431 if type_ == 'T':
433 if type_ == 'T':
432 obj = self._repo.get_object(sha)
434 obj = _repo.get_object(sha)
433 if isinstance(obj, Tag):
435 if isinstance(obj, Tag):
434 sha = self._repo.get_object(sha).object[1]
436 sha = _repo.get_object(sha).object[1]
435 _refs[_key] = [sha, type_]
437 _refs[_key] = [sha, type_]
436 break
438 break
437 return _refs
439 return _refs
438
440
439 def _heads(self, reverse=False):
441 def _heads(self, reverse=False):
440 refs = self._repo.get_refs()
442 refs = self._repo.get_refs()
441 heads = {}
443 heads = {}
442
444
443 for key, val in refs.items():
445 for key, val in refs.items():
444 for ref_key in ['refs/heads/', 'refs/remotes/origin/']:
446 for ref_key in ['refs/heads/', 'refs/remotes/origin/']:
445 if key.startswith(ref_key):
447 if key.startswith(ref_key):
446 n = key[len(ref_key):]
448 n = key[len(ref_key):]
447 if n not in ['HEAD']:
449 if n not in ['HEAD']:
448 heads[n] = val
450 heads[n] = val
449
451
450 return heads if reverse else dict((y, x) for x, y in heads.iteritems())
452 return heads if reverse else dict((y, x) for x, y in heads.iteritems())
451
453
452 def get_changeset(self, revision=None):
454 def get_changeset(self, revision=None):
453 """
455 """
454 Returns ``GitChangeset`` object representing commit from git repository
456 Returns ``GitChangeset`` object representing commit from git repository
455 at the given revision or head (most recent commit) if None given.
457 at the given revision or head (most recent commit) if None given.
456 """
458 """
457 if isinstance(revision, GitChangeset):
459 if isinstance(revision, GitChangeset):
458 return revision
460 return revision
459 revision = self._get_revision(revision)
461 revision = self._get_revision(revision)
460 changeset = GitChangeset(repository=self, revision=revision)
462 changeset = GitChangeset(repository=self, revision=revision)
461 return changeset
463 return changeset
462
464
463 def get_changesets(self, start=None, end=None, start_date=None,
465 def get_changesets(self, start=None, end=None, start_date=None,
464 end_date=None, branch_name=None, reverse=False):
466 end_date=None, branch_name=None, reverse=False):
465 """
467 """
466 Returns iterator of ``GitChangeset`` objects from start to end (both
468 Returns iterator of ``GitChangeset`` objects from start to end (both
467 are inclusive), in ascending date order (unless ``reverse`` is set).
469 are inclusive), in ascending date order (unless ``reverse`` is set).
468
470
469 :param start: changeset ID, as str; first returned changeset
471 :param start: changeset ID, as str; first returned changeset
470 :param end: changeset ID, as str; last returned changeset
472 :param end: changeset ID, as str; last returned changeset
471 :param start_date: if specified, changesets with commit date less than
473 :param start_date: if specified, changesets with commit date less than
472 ``start_date`` would be filtered out from returned set
474 ``start_date`` would be filtered out from returned set
473 :param end_date: if specified, changesets with commit date greater than
475 :param end_date: if specified, changesets with commit date greater than
474 ``end_date`` would be filtered out from returned set
476 ``end_date`` would be filtered out from returned set
475 :param branch_name: if specified, changesets not reachable from given
477 :param branch_name: if specified, changesets not reachable from given
476 branch would be filtered out from returned set
478 branch would be filtered out from returned set
477 :param reverse: if ``True``, returned generator would be reversed
479 :param reverse: if ``True``, returned generator would be reversed
478 (meaning that returned changesets would have descending date order)
480 (meaning that returned changesets would have descending date order)
479
481
480 :raise BranchDoesNotExistError: If given ``branch_name`` does not
482 :raise BranchDoesNotExistError: If given ``branch_name`` does not
481 exist.
483 exist.
482 :raise ChangesetDoesNotExistError: If changeset for given ``start`` or
484 :raise ChangesetDoesNotExistError: If changeset for given ``start`` or
483 ``end`` could not be found.
485 ``end`` could not be found.
484
486
485 """
487 """
486 if branch_name and branch_name not in self.branches:
488 if branch_name and branch_name not in self.branches:
487 raise BranchDoesNotExistError("Branch '%s' not found" \
489 raise BranchDoesNotExistError("Branch '%s' not found" \
488 % branch_name)
490 % branch_name)
489 # %H at format means (full) commit hash, initial hashes are retrieved
491 # %H at format means (full) commit hash, initial hashes are retrieved
490 # in ascending date order
492 # in ascending date order
491 cmd_template = 'log --date-order --reverse --pretty=format:"%H"'
493 cmd_template = 'log --date-order --reverse --pretty=format:"%H"'
492 cmd_params = {}
494 cmd_params = {}
493 if start_date:
495 if start_date:
494 cmd_template += ' --since "$since"'
496 cmd_template += ' --since "$since"'
495 cmd_params['since'] = start_date.strftime('%m/%d/%y %H:%M:%S')
497 cmd_params['since'] = start_date.strftime('%m/%d/%y %H:%M:%S')
496 if end_date:
498 if end_date:
497 cmd_template += ' --until "$until"'
499 cmd_template += ' --until "$until"'
498 cmd_params['until'] = end_date.strftime('%m/%d/%y %H:%M:%S')
500 cmd_params['until'] = end_date.strftime('%m/%d/%y %H:%M:%S')
499 if branch_name:
501 if branch_name:
500 cmd_template += ' $branch_name'
502 cmd_template += ' $branch_name'
501 cmd_params['branch_name'] = branch_name
503 cmd_params['branch_name'] = branch_name
502 else:
504 else:
503 rev_filter = _git_path = rhodecode.CONFIG.get('git_rev_filter',
505 rev_filter = _git_path = rhodecode.CONFIG.get('git_rev_filter',
504 '--all').strip()
506 '--all').strip()
505 cmd_template += ' %s' % (rev_filter)
507 cmd_template += ' %s' % (rev_filter)
506
508
507 cmd = Template(cmd_template).safe_substitute(**cmd_params)
509 cmd = Template(cmd_template).safe_substitute(**cmd_params)
508 revs = self.run_git_command(cmd)[0].splitlines()
510 revs = self.run_git_command(cmd)[0].splitlines()
509 start_pos = 0
511 start_pos = 0
510 end_pos = len(revs)
512 end_pos = len(revs)
511 if start:
513 if start:
512 _start = self._get_revision(start)
514 _start = self._get_revision(start)
513 try:
515 try:
514 start_pos = revs.index(_start)
516 start_pos = revs.index(_start)
515 except ValueError:
517 except ValueError:
516 pass
518 pass
517
519
518 if end is not None:
520 if end is not None:
519 _end = self._get_revision(end)
521 _end = self._get_revision(end)
520 try:
522 try:
521 end_pos = revs.index(_end)
523 end_pos = revs.index(_end)
522 except ValueError:
524 except ValueError:
523 pass
525 pass
524
526
525 if None not in [start, end] and start_pos > end_pos:
527 if None not in [start, end] and start_pos > end_pos:
526 raise RepositoryError('start cannot be after end')
528 raise RepositoryError('start cannot be after end')
527
529
528 if end_pos is not None:
530 if end_pos is not None:
529 end_pos += 1
531 end_pos += 1
530
532
531 revs = revs[start_pos:end_pos]
533 revs = revs[start_pos:end_pos]
532 if reverse:
534 if reverse:
533 revs = reversed(revs)
535 revs = reversed(revs)
534 for rev in revs:
536 for rev in revs:
535 yield self.get_changeset(rev)
537 yield self.get_changeset(rev)
536
538
537 def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
539 def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
538 context=3):
540 context=3):
539 """
541 """
540 Returns (git like) *diff*, as plain text. Shows changes introduced by
542 Returns (git like) *diff*, as plain text. Shows changes introduced by
541 ``rev2`` since ``rev1``.
543 ``rev2`` since ``rev1``.
542
544
543 :param rev1: Entry point from which diff is shown. Can be
545 :param rev1: Entry point from which diff is shown. Can be
544 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
546 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
545 the changes since empty state of the repository until ``rev2``
547 the changes since empty state of the repository until ``rev2``
546 :param rev2: Until which revision changes should be shown.
548 :param rev2: Until which revision changes should be shown.
547 :param ignore_whitespace: If set to ``True``, would not show whitespace
549 :param ignore_whitespace: If set to ``True``, would not show whitespace
548 changes. Defaults to ``False``.
550 changes. Defaults to ``False``.
549 :param context: How many lines before/after changed lines should be
551 :param context: How many lines before/after changed lines should be
550 shown. Defaults to ``3``.
552 shown. Defaults to ``3``.
551 """
553 """
552 flags = ['-U%s' % context, '--full-index', '--binary', '-p', '-M', '--abbrev=40']
554 flags = ['-U%s' % context, '--full-index', '--binary', '-p', '-M', '--abbrev=40']
553 if ignore_whitespace:
555 if ignore_whitespace:
554 flags.append('-w')
556 flags.append('-w')
555
557
556 if hasattr(rev1, 'raw_id'):
558 if hasattr(rev1, 'raw_id'):
557 rev1 = getattr(rev1, 'raw_id')
559 rev1 = getattr(rev1, 'raw_id')
558
560
559 if hasattr(rev2, 'raw_id'):
561 if hasattr(rev2, 'raw_id'):
560 rev2 = getattr(rev2, 'raw_id')
562 rev2 = getattr(rev2, 'raw_id')
561
563
562 if rev1 == self.EMPTY_CHANGESET:
564 if rev1 == self.EMPTY_CHANGESET:
563 rev2 = self.get_changeset(rev2).raw_id
565 rev2 = self.get_changeset(rev2).raw_id
564 cmd = ' '.join(['show'] + flags + [rev2])
566 cmd = ' '.join(['show'] + flags + [rev2])
565 else:
567 else:
566 rev1 = self.get_changeset(rev1).raw_id
568 rev1 = self.get_changeset(rev1).raw_id
567 rev2 = self.get_changeset(rev2).raw_id
569 rev2 = self.get_changeset(rev2).raw_id
568 cmd = ' '.join(['diff'] + flags + [rev1, rev2])
570 cmd = ' '.join(['diff'] + flags + [rev1, rev2])
569
571
570 if path:
572 if path:
571 cmd += ' -- "%s"' % path
573 cmd += ' -- "%s"' % path
572
574
573 stdout, stderr = self.run_git_command(cmd)
575 stdout, stderr = self.run_git_command(cmd)
574 # If we used 'show' command, strip first few lines (until actual diff
576 # If we used 'show' command, strip first few lines (until actual diff
575 # starts)
577 # starts)
576 if rev1 == self.EMPTY_CHANGESET:
578 if rev1 == self.EMPTY_CHANGESET:
577 lines = stdout.splitlines()
579 lines = stdout.splitlines()
578 x = 0
580 x = 0
579 for line in lines:
581 for line in lines:
580 if line.startswith('diff'):
582 if line.startswith('diff'):
581 break
583 break
582 x += 1
584 x += 1
583 # Append new line just like 'diff' command do
585 # Append new line just like 'diff' command do
584 stdout = '\n'.join(lines[x:]) + '\n'
586 stdout = '\n'.join(lines[x:]) + '\n'
585 return stdout
587 return stdout
586
588
587 @LazyProperty
589 @LazyProperty
588 def in_memory_changeset(self):
590 def in_memory_changeset(self):
589 """
591 """
590 Returns ``GitInMemoryChangeset`` object for this repository.
592 Returns ``GitInMemoryChangeset`` object for this repository.
591 """
593 """
592 return GitInMemoryChangeset(self)
594 return GitInMemoryChangeset(self)
593
595
594 def clone(self, url, update_after_clone=True, bare=False):
596 def clone(self, url, update_after_clone=True, bare=False):
595 """
597 """
596 Tries to clone changes from external location.
598 Tries to clone changes from external location.
597
599
598 :param update_after_clone: If set to ``False``, git won't checkout
600 :param update_after_clone: If set to ``False``, git won't checkout
599 working directory
601 working directory
600 :param bare: If set to ``True``, repository would be cloned into
602 :param bare: If set to ``True``, repository would be cloned into
601 *bare* git repository (no working directory at all).
603 *bare* git repository (no working directory at all).
602 """
604 """
603 url = self._get_url(url)
605 url = self._get_url(url)
604 cmd = ['clone']
606 cmd = ['clone']
605 if bare:
607 if bare:
606 cmd.append('--bare')
608 cmd.append('--bare')
607 elif not update_after_clone:
609 elif not update_after_clone:
608 cmd.append('--no-checkout')
610 cmd.append('--no-checkout')
609 cmd += ['--', '"%s"' % url, '"%s"' % self.path]
611 cmd += ['--', '"%s"' % url, '"%s"' % self.path]
610 cmd = ' '.join(cmd)
612 cmd = ' '.join(cmd)
611 # If error occurs run_git_command raises RepositoryError already
613 # If error occurs run_git_command raises RepositoryError already
612 self.run_git_command(cmd)
614 self.run_git_command(cmd)
613
615
614 def pull(self, url):
616 def pull(self, url):
615 """
617 """
616 Tries to pull changes from external location.
618 Tries to pull changes from external location.
617 """
619 """
618 url = self._get_url(url)
620 url = self._get_url(url)
619 cmd = ['pull']
621 cmd = ['pull']
620 cmd.append("--ff-only")
622 cmd.append("--ff-only")
621 cmd.append(url)
623 cmd.append(url)
622 cmd = ' '.join(cmd)
624 cmd = ' '.join(cmd)
623 # If error occurs run_git_command raises RepositoryError already
625 # If error occurs run_git_command raises RepositoryError already
624 self.run_git_command(cmd)
626 self.run_git_command(cmd)
625
627
626 def fetch(self, url):
628 def fetch(self, url):
627 """
629 """
628 Tries to pull changes from external location.
630 Tries to pull changes from external location.
629 """
631 """
630 url = self._get_url(url)
632 url = self._get_url(url)
631 so, se = self.run_git_command('ls-remote -h %s' % url)
633 so, se = self.run_git_command('ls-remote -h %s' % url)
632 refs = []
634 refs = []
633 for line in (x for x in so.splitlines()):
635 for line in (x for x in so.splitlines()):
634 sha, ref = line.split('\t')
636 sha, ref = line.split('\t')
635 refs.append(ref)
637 refs.append(ref)
636 refs = ' '.join(('+%s:%s' % (r, r) for r in refs))
638 refs = ' '.join(('+%s:%s' % (r, r) for r in refs))
637 cmd = '''fetch %s -- %s''' % (url, refs)
639 cmd = '''fetch %s -- %s''' % (url, refs)
638 self.run_git_command(cmd)
640 self.run_git_command(cmd)
639
641
640 @LazyProperty
642 @LazyProperty
641 def workdir(self):
643 def workdir(self):
642 """
644 """
643 Returns ``Workdir`` instance for this repository.
645 Returns ``Workdir`` instance for this repository.
644 """
646 """
645 return GitWorkdir(self)
647 return GitWorkdir(self)
646
648
647 def get_config_value(self, section, name, config_file=None):
649 def get_config_value(self, section, name, config_file=None):
648 """
650 """
649 Returns configuration value for a given [``section``] and ``name``.
651 Returns configuration value for a given [``section``] and ``name``.
650
652
651 :param section: Section we want to retrieve value from
653 :param section: Section we want to retrieve value from
652 :param name: Name of configuration we want to retrieve
654 :param name: Name of configuration we want to retrieve
653 :param config_file: A path to file which should be used to retrieve
655 :param config_file: A path to file which should be used to retrieve
654 configuration from (might also be a list of file paths)
656 configuration from (might also be a list of file paths)
655 """
657 """
656 if config_file is None:
658 if config_file is None:
657 config_file = []
659 config_file = []
658 elif isinstance(config_file, basestring):
660 elif isinstance(config_file, basestring):
659 config_file = [config_file]
661 config_file = [config_file]
660
662
661 def gen_configs():
663 def gen_configs():
662 for path in config_file + self._config_files:
664 for path in config_file + self._config_files:
663 try:
665 try:
664 yield ConfigFile.from_path(path)
666 yield ConfigFile.from_path(path)
665 except (IOError, OSError, ValueError):
667 except (IOError, OSError, ValueError):
666 continue
668 continue
667
669
668 for config in gen_configs():
670 for config in gen_configs():
669 try:
671 try:
670 return config.get(section, name)
672 return config.get(section, name)
671 except KeyError:
673 except KeyError:
672 continue
674 continue
673 return None
675 return None
674
676
675 def get_user_name(self, config_file=None):
677 def get_user_name(self, config_file=None):
676 """
678 """
677 Returns user's name from global configuration file.
679 Returns user's name from global configuration file.
678
680
679 :param config_file: A path to file which should be used to retrieve
681 :param config_file: A path to file which should be used to retrieve
680 configuration from (might also be a list of file paths)
682 configuration from (might also be a list of file paths)
681 """
683 """
682 return self.get_config_value('user', 'name', config_file)
684 return self.get_config_value('user', 'name', config_file)
683
685
684 def get_user_email(self, config_file=None):
686 def get_user_email(self, config_file=None):
685 """
687 """
686 Returns user's email from global configuration file.
688 Returns user's email from global configuration file.
687
689
688 :param config_file: A path to file which should be used to retrieve
690 :param config_file: A path to file which should be used to retrieve
689 configuration from (might also be a list of file paths)
691 configuration from (might also be a list of file paths)
690 """
692 """
691 return self.get_config_value('user', 'email', config_file)
693 return self.get_config_value('user', 'email', config_file)
General Comments 0
You need to be logged in to leave comments. Login now