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