##// END OF EJS Templates
Implemented pull command for remote repos for git...
marcink -
r2209:19a6c23a beta
parent child Browse files
Show More
@@ -1,286 +1,300 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.lib.middleware.simplegit
3 rhodecode.lib.middleware.simplegit
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5
5
6 SimpleGit middleware for handling git protocol request (push/clone etc.)
6 SimpleGit middleware for handling git protocol request (push/clone etc.)
7 It's implemented with basic auth function
7 It's implemented with basic auth function
8
8
9 :created_on: Apr 28, 2010
9 :created_on: Apr 28, 2010
10 :author: marcink
10 :author: marcink
11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 :license: GPLv3, see COPYING for more details.
12 :license: GPLv3, see COPYING for more details.
13 """
13 """
14 # This program is free software: you can redistribute it and/or modify
14 # This program is free software: you can redistribute it and/or modify
15 # it under the terms of the GNU General Public License as published by
15 # it under the terms of the GNU General Public License as published by
16 # the Free Software Foundation, either version 3 of the License, or
16 # the Free Software Foundation, either version 3 of the License, or
17 # (at your option) any later version.
17 # (at your option) any later version.
18 #
18 #
19 # This program is distributed in the hope that it will be useful,
19 # This program is distributed in the hope that it will be useful,
20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 # GNU General Public License for more details.
22 # GNU General Public License for more details.
23 #
23 #
24 # You should have received a copy of the GNU General Public License
24 # You should have received a copy of the GNU General Public License
25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26
26
27 import os
27 import os
28 import re
28 import re
29 import logging
29 import logging
30 import traceback
30 import traceback
31
31
32 from dulwich import server as dulserver
32 from dulwich import server as dulserver
33
33
34
34
35 class SimpleGitUploadPackHandler(dulserver.UploadPackHandler):
35 class SimpleGitUploadPackHandler(dulserver.UploadPackHandler):
36
36
37 def handle(self):
37 def handle(self):
38 write = lambda x: self.proto.write_sideband(1, x)
38 write = lambda x: self.proto.write_sideband(1, x)
39
39
40 graph_walker = dulserver.ProtocolGraphWalker(self,
40 graph_walker = dulserver.ProtocolGraphWalker(self,
41 self.repo.object_store,
41 self.repo.object_store,
42 self.repo.get_peeled)
42 self.repo.get_peeled)
43 objects_iter = self.repo.fetch_objects(
43 objects_iter = self.repo.fetch_objects(
44 graph_walker.determine_wants, graph_walker, self.progress,
44 graph_walker.determine_wants, graph_walker, self.progress,
45 get_tagged=self.get_tagged)
45 get_tagged=self.get_tagged)
46
46
47 # Did the process short-circuit (e.g. in a stateless RPC call)? Note
47 # Did the process short-circuit (e.g. in a stateless RPC call)? Note
48 # that the client still expects a 0-object pack in most cases.
48 # that the client still expects a 0-object pack in most cases.
49 if objects_iter is None:
49 if objects_iter is None:
50 return
50 return
51
51
52 self.progress("counting objects: %d, done.\n" % len(objects_iter))
52 self.progress("counting objects: %d, done.\n" % len(objects_iter))
53 dulserver.write_pack_objects(dulserver.ProtocolFile(None, write),
53 dulserver.write_pack_objects(dulserver.ProtocolFile(None, write),
54 objects_iter)
54 objects_iter)
55 messages = []
55 messages = []
56 messages.append('thank you for using rhodecode')
56 messages.append('thank you for using rhodecode')
57
57
58 for msg in messages:
58 for msg in messages:
59 self.progress(msg + "\n")
59 self.progress(msg + "\n")
60 # we are done
60 # we are done
61 self.proto.write("0000")
61 self.proto.write("0000")
62
62
63
63
64 dulserver.DEFAULT_HANDLERS = {
64 dulserver.DEFAULT_HANDLERS = {
65 'git-upload-pack': SimpleGitUploadPackHandler,
65 'git-upload-pack': SimpleGitUploadPackHandler,
66 'git-receive-pack': dulserver.ReceivePackHandler,
66 'git-receive-pack': dulserver.ReceivePackHandler,
67 }
67 }
68
68
69 from dulwich.repo import Repo
69 from dulwich.repo import Repo
70 from dulwich.web import make_wsgi_chain
70 from dulwich.web import make_wsgi_chain
71
71
72 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
72 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
73
73
74 from rhodecode.lib.utils2 import safe_str
74 from rhodecode.lib.utils2 import safe_str
75 from rhodecode.lib.base import BaseVCSController
75 from rhodecode.lib.base import BaseVCSController
76 from rhodecode.lib.auth import get_container_username
76 from rhodecode.lib.auth import get_container_username
77 from rhodecode.lib.utils import is_valid_repo, make_ui
77 from rhodecode.lib.utils import is_valid_repo, make_ui
78 from rhodecode.model.db import User
78 from rhodecode.model.db import User
79
79
80 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError
80 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError
81
81
82 log = logging.getLogger(__name__)
82 log = logging.getLogger(__name__)
83
83
84
84
85 GIT_PROTO_PAT = re.compile(r'^/(.+)/(info/refs|git-upload-pack|git-receive-pack)')
85 GIT_PROTO_PAT = re.compile(r'^/(.+)/(info/refs|git-upload-pack|git-receive-pack)')
86
86
87
87
88 def is_git(environ):
88 def is_git(environ):
89 path_info = environ['PATH_INFO']
89 path_info = environ['PATH_INFO']
90 isgit_path = GIT_PROTO_PAT.match(path_info)
90 isgit_path = GIT_PROTO_PAT.match(path_info)
91 log.debug('pathinfo: %s detected as GIT %s' % (
91 log.debug('pathinfo: %s detected as GIT %s' % (
92 path_info, isgit_path != None)
92 path_info, isgit_path != None)
93 )
93 )
94 return isgit_path
94 return isgit_path
95
95
96
96
97 class SimpleGit(BaseVCSController):
97 class SimpleGit(BaseVCSController):
98
98
99 def _handle_request(self, environ, start_response):
99 def _handle_request(self, environ, start_response):
100
100
101 if not is_git(environ):
101 if not is_git(environ):
102 return self.application(environ, start_response)
102 return self.application(environ, start_response)
103
103
104 ipaddr = self._get_ip_addr(environ)
104 ipaddr = self._get_ip_addr(environ)
105 username = None
105 username = None
106 self._git_first_op = False
106 self._git_first_op = False
107 # skip passing error to error controller
107 # skip passing error to error controller
108 environ['pylons.status_code_redirect'] = True
108 environ['pylons.status_code_redirect'] = True
109
109
110 #======================================================================
110 #======================================================================
111 # EXTRACT REPOSITORY NAME FROM ENV
111 # EXTRACT REPOSITORY NAME FROM ENV
112 #======================================================================
112 #======================================================================
113 try:
113 try:
114 repo_name = self.__get_repository(environ)
114 repo_name = self.__get_repository(environ)
115 log.debug('Extracted repo name is %s' % repo_name)
115 log.debug('Extracted repo name is %s' % repo_name)
116 except:
116 except:
117 return HTTPInternalServerError()(environ, start_response)
117 return HTTPInternalServerError()(environ, start_response)
118
118
119 # quick check if that dir exists...
119 # quick check if that dir exists...
120 if is_valid_repo(repo_name, self.basepath) is False:
120 if is_valid_repo(repo_name, self.basepath) is False:
121 return HTTPNotFound()(environ, start_response)
121 return HTTPNotFound()(environ, start_response)
122
122
123 #======================================================================
123 #======================================================================
124 # GET ACTION PULL or PUSH
124 # GET ACTION PULL or PUSH
125 #======================================================================
125 #======================================================================
126 action = self.__get_action(environ)
126 action = self.__get_action(environ)
127
127
128 #======================================================================
128 #======================================================================
129 # CHECK ANONYMOUS PERMISSION
129 # CHECK ANONYMOUS PERMISSION
130 #======================================================================
130 #======================================================================
131 if action in ['pull', 'push']:
131 if action in ['pull', 'push']:
132 anonymous_user = self.__get_user('default')
132 anonymous_user = self.__get_user('default')
133 username = anonymous_user.username
133 username = anonymous_user.username
134 anonymous_perm = self._check_permission(action, anonymous_user,
134 anonymous_perm = self._check_permission(action, anonymous_user,
135 repo_name)
135 repo_name)
136
136
137 if anonymous_perm is not True or anonymous_user.active is False:
137 if anonymous_perm is not True or anonymous_user.active is False:
138 if anonymous_perm is not True:
138 if anonymous_perm is not True:
139 log.debug('Not enough credentials to access this '
139 log.debug('Not enough credentials to access this '
140 'repository as anonymous user')
140 'repository as anonymous user')
141 if anonymous_user.active is False:
141 if anonymous_user.active is False:
142 log.debug('Anonymous access is disabled, running '
142 log.debug('Anonymous access is disabled, running '
143 'authentication')
143 'authentication')
144 #==============================================================
144 #==============================================================
145 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
145 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
146 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
146 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
147 #==============================================================
147 #==============================================================
148
148
149 # Attempting to retrieve username from the container
149 # Attempting to retrieve username from the container
150 username = get_container_username(environ, self.config)
150 username = get_container_username(environ, self.config)
151
151
152 # If not authenticated by the container, running basic auth
152 # If not authenticated by the container, running basic auth
153 if not username:
153 if not username:
154 self.authenticate.realm = \
154 self.authenticate.realm = \
155 safe_str(self.config['rhodecode_realm'])
155 safe_str(self.config['rhodecode_realm'])
156 result = self.authenticate(environ)
156 result = self.authenticate(environ)
157 if isinstance(result, str):
157 if isinstance(result, str):
158 AUTH_TYPE.update(environ, 'basic')
158 AUTH_TYPE.update(environ, 'basic')
159 REMOTE_USER.update(environ, result)
159 REMOTE_USER.update(environ, result)
160 username = result
160 username = result
161 else:
161 else:
162 return result.wsgi_application(environ, start_response)
162 return result.wsgi_application(environ, start_response)
163
163
164 #==============================================================
164 #==============================================================
165 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
165 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
166 #==============================================================
166 #==============================================================
167 if action in ['pull', 'push']:
167 if action in ['pull', 'push']:
168 try:
168 try:
169 user = self.__get_user(username)
169 user = self.__get_user(username)
170 if user is None or not user.active:
170 if user is None or not user.active:
171 return HTTPForbidden()(environ, start_response)
171 return HTTPForbidden()(environ, start_response)
172 username = user.username
172 username = user.username
173 except:
173 except:
174 log.error(traceback.format_exc())
174 log.error(traceback.format_exc())
175 return HTTPInternalServerError()(environ,
175 return HTTPInternalServerError()(environ,
176 start_response)
176 start_response)
177
177
178 #check permissions for this repository
178 #check permissions for this repository
179 perm = self._check_permission(action, user, repo_name)
179 perm = self._check_permission(action, user, repo_name)
180 if perm is not True:
180 if perm is not True:
181 return HTTPForbidden()(environ, start_response)
181 return HTTPForbidden()(environ, start_response)
182 extras = {
182 extras = {
183 'ip': ipaddr,
183 'ip': ipaddr,
184 'username': username,
184 'username': username,
185 'action': action,
185 'action': action,
186 'repository': repo_name,
186 'repository': repo_name,
187 'scm': 'git',
187 'scm': 'git',
188 }
188 }
189
189
190 #===================================================================
190 #===================================================================
191 # GIT REQUEST HANDLING
191 # GIT REQUEST HANDLING
192 #===================================================================
192 #===================================================================
193 repo_path = os.path.join(safe_str(self.basepath), safe_str(repo_name))
193 repo_path = os.path.join(safe_str(self.basepath), safe_str(repo_name))
194 log.debug('Repository path is %s' % repo_path)
194 log.debug('Repository path is %s' % repo_path)
195
195
196 baseui = make_ui('db')
196 baseui = make_ui('db')
197 for k, v in extras.items():
197 self.__inject_extras(repo_path, baseui, extras)
198 baseui.setconfig('rhodecode_extras', k, v)
198
199
199
200 try:
200 try:
201 # invalidate cache on push
201 # invalidate cache on push
202 if action == 'push':
202 if action == 'push':
203 self._invalidate_cache(repo_name)
203 self._invalidate_cache(repo_name)
204 self._handle_githooks(action, baseui, environ)
204 self._handle_githooks(action, baseui, environ)
205
205
206 log.info('%s action on GIT repo "%s"' % (action, repo_name))
206 log.info('%s action on GIT repo "%s"' % (action, repo_name))
207 app = self.__make_app(repo_name, repo_path)
207 app = self.__make_app(repo_name, repo_path)
208 return app(environ, start_response)
208 return app(environ, start_response)
209 except Exception:
209 except Exception:
210 log.error(traceback.format_exc())
210 log.error(traceback.format_exc())
211 return HTTPInternalServerError()(environ, start_response)
211 return HTTPInternalServerError()(environ, start_response)
212
212
213 def __make_app(self, repo_name, repo_path):
213 def __make_app(self, repo_name, repo_path):
214 """
214 """
215 Make an wsgi application using dulserver
215 Make an wsgi application using dulserver
216
216
217 :param repo_name: name of the repository
217 :param repo_name: name of the repository
218 :param repo_path: full path to the repository
218 :param repo_path: full path to the repository
219 """
219 """
220 _d = {'/' + repo_name: Repo(repo_path)}
220 _d = {'/' + repo_name: Repo(repo_path)}
221 backend = dulserver.DictBackend(_d)
221 backend = dulserver.DictBackend(_d)
222 gitserve = make_wsgi_chain(backend)
222 gitserve = make_wsgi_chain(backend)
223
223
224 return gitserve
224 return gitserve
225
225
226 def __get_repository(self, environ):
226 def __get_repository(self, environ):
227 """
227 """
228 Get's repository name out of PATH_INFO header
228 Get's repository name out of PATH_INFO header
229
229
230 :param environ: environ where PATH_INFO is stored
230 :param environ: environ where PATH_INFO is stored
231 """
231 """
232 try:
232 try:
233 environ['PATH_INFO'] = self._get_by_id(environ['PATH_INFO'])
233 environ['PATH_INFO'] = self._get_by_id(environ['PATH_INFO'])
234 repo_name = GIT_PROTO_PAT.match(environ['PATH_INFO']).group(1)
234 repo_name = GIT_PROTO_PAT.match(environ['PATH_INFO']).group(1)
235 except:
235 except:
236 log.error(traceback.format_exc())
236 log.error(traceback.format_exc())
237 raise
237 raise
238
238
239 return repo_name
239 return repo_name
240
240
241 def __get_user(self, username):
241 def __get_user(self, username):
242 return User.get_by_username(username)
242 return User.get_by_username(username)
243
243
244 def __get_action(self, environ):
244 def __get_action(self, environ):
245 """
245 """
246 Maps git request commands into a pull or push command.
246 Maps git request commands into a pull or push command.
247
247
248 :param environ:
248 :param environ:
249 """
249 """
250 service = environ['QUERY_STRING'].split('=')
250 service = environ['QUERY_STRING'].split('=')
251
251
252 if len(service) > 1:
252 if len(service) > 1:
253 service_cmd = service[1]
253 service_cmd = service[1]
254 mapping = {
254 mapping = {
255 'git-receive-pack': 'push',
255 'git-receive-pack': 'push',
256 'git-upload-pack': 'pull',
256 'git-upload-pack': 'pull',
257 }
257 }
258 op = mapping[service_cmd]
258 op = mapping[service_cmd]
259 self._git_stored_op = op
259 self._git_stored_op = op
260 return op
260 return op
261 else:
261 else:
262 # try to fallback to stored variable as we don't know if the last
262 # try to fallback to stored variable as we don't know if the last
263 # operation is pull/push
263 # operation is pull/push
264 op = getattr(self, '_git_stored_op', 'pull')
264 op = getattr(self, '_git_stored_op', 'pull')
265 return op
265 return op
266
266
267 def _handle_githooks(self, action, baseui, environ):
267 def _handle_githooks(self, action, baseui, environ):
268
269 from rhodecode.lib.hooks import log_pull_action, log_push_action
268 from rhodecode.lib.hooks import log_pull_action, log_push_action
270 service = environ['QUERY_STRING'].split('=')
269 service = environ['QUERY_STRING'].split('=')
271 if len(service) < 2:
270 if len(service) < 2:
272 return
271 return
273
272
274 class cont(object):
273 from rhodecode.model.db import Repository
275 pass
274 _repo = Repository.get_by_repo_name(repo_name)
276
275 _repo = _repo.scm_instance
277 repo = cont()
276 _repo._repo.ui = baseui
278 setattr(repo, 'ui', baseui)
279
277
280 push_hook = 'pretxnchangegroup.push_logger'
278 push_hook = 'pretxnchangegroup.push_logger'
281 pull_hook = 'preoutgoing.pull_logger'
279 pull_hook = 'preoutgoing.pull_logger'
282 _hooks = dict(baseui.configitems('hooks')) or {}
280 _hooks = dict(baseui.configitems('hooks')) or {}
283 if action == 'push' and _hooks.get(push_hook):
281 if action == 'push' and _hooks.get(push_hook):
284 log_push_action(ui=baseui, repo=repo)
282 log_push_action(ui=baseui, repo=repo._repo)
285 elif action == 'pull' and _hooks.get(pull_hook):
283 elif action == 'pull' and _hooks.get(pull_hook):
286 log_pull_action(ui=baseui, repo=repo)
284 log_pull_action(ui=baseui, repo=repo._repo)
285
286 def __inject_extras(self, repo_path, baseui, extras={}):
287 """
288 Injects some extra params into baseui instance
289
290 :param baseui: baseui instance
291 :param extras: dict with extra params to put into baseui
292 """
293
294 # make our hgweb quiet so it doesn't print output
295 baseui.setconfig('ui', 'quiet', 'true')
296
297 #inject some additional parameters that will be available in ui
298 #for hooks
299 for k, v in extras.items():
300 baseui.setconfig('rhodecode_extras', k, v)
@@ -1,525 +1,545 b''
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 from dulwich.repo import Repo, NotGitRepository
16 from dulwich.repo import Repo, NotGitRepository
17 #from dulwich.config import ConfigFile
17 #from dulwich.config import ConfigFile
18 from string import Template
18 from string import Template
19 from subprocess import Popen, PIPE
19 from subprocess import Popen, PIPE
20 from rhodecode.lib.vcs.backends.base import BaseRepository
20 from rhodecode.lib.vcs.backends.base import BaseRepository
21 from rhodecode.lib.vcs.exceptions import BranchDoesNotExistError
21 from rhodecode.lib.vcs.exceptions import BranchDoesNotExistError
22 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError
22 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError
23 from rhodecode.lib.vcs.exceptions import EmptyRepositoryError
23 from rhodecode.lib.vcs.exceptions import EmptyRepositoryError
24 from rhodecode.lib.vcs.exceptions import RepositoryError
24 from rhodecode.lib.vcs.exceptions import RepositoryError
25 from rhodecode.lib.vcs.exceptions import TagAlreadyExistError
25 from rhodecode.lib.vcs.exceptions import TagAlreadyExistError
26 from rhodecode.lib.vcs.exceptions import TagDoesNotExistError
26 from rhodecode.lib.vcs.exceptions import TagDoesNotExistError
27 from rhodecode.lib.vcs.utils import safe_unicode, makedate, date_fromtimestamp
27 from rhodecode.lib.vcs.utils import safe_unicode, makedate, date_fromtimestamp
28 from rhodecode.lib.vcs.utils.lazy import LazyProperty
28 from rhodecode.lib.vcs.utils.lazy import LazyProperty
29 from rhodecode.lib.vcs.utils.ordered_dict import OrderedDict
29 from rhodecode.lib.vcs.utils.ordered_dict import OrderedDict
30 from rhodecode.lib.vcs.utils.paths import abspath
30 from rhodecode.lib.vcs.utils.paths import abspath
31 from rhodecode.lib.vcs.utils.paths import get_user_home
31 from rhodecode.lib.vcs.utils.paths import get_user_home
32 from .workdir import GitWorkdir
32 from .workdir import GitWorkdir
33 from .changeset import GitChangeset
33 from .changeset import GitChangeset
34 from .inmemory import GitInMemoryChangeset
34 from .inmemory import GitInMemoryChangeset
35 from .config import ConfigFile
35 from .config import ConfigFile
36
36
37
37
38 class GitRepository(BaseRepository):
38 class GitRepository(BaseRepository):
39 """
39 """
40 Git repository backend.
40 Git repository backend.
41 """
41 """
42 DEFAULT_BRANCH_NAME = 'master'
42 DEFAULT_BRANCH_NAME = 'master'
43 scm = 'git'
43 scm = 'git'
44
44
45 def __init__(self, repo_path, create=False, src_url=None,
45 def __init__(self, repo_path, create=False, src_url=None,
46 update_after_clone=False, bare=False):
46 update_after_clone=False, bare=False):
47
47
48 self.path = abspath(repo_path)
48 self.path = abspath(repo_path)
49 self._repo = self._get_repo(create, src_url, update_after_clone, bare)
49 self._repo = self._get_repo(create, src_url, update_after_clone, bare)
50 #temporary set that to now at later we will move it to constructor
51 baseui = None
52 if baseui is None:
53 from mercurial.ui import ui
54 baseui = ui()
55 # patch the instance of GitRepo with an "FAKE" ui object to add
56 # compatibility layer with Mercurial
57 setattr(self._repo, 'ui', baseui)
58
50 try:
59 try:
51 self.head = self._repo.head()
60 self.head = self._repo.head()
52 except KeyError:
61 except KeyError:
53 self.head = None
62 self.head = None
54
63
55 self._config_files = [
64 self._config_files = [
56 bare and abspath(self.path, 'config') or abspath(self.path, '.git',
65 bare and abspath(self.path, 'config') or abspath(self.path, '.git',
57 'config'),
66 'config'),
58 abspath(get_user_home(), '.gitconfig'),
67 abspath(get_user_home(), '.gitconfig'),
59 ]
68 ]
60
69
61 @LazyProperty
70 @LazyProperty
62 def revisions(self):
71 def revisions(self):
63 """
72 """
64 Returns list of revisions' ids, in ascending order. Being lazy
73 Returns list of revisions' ids, in ascending order. Being lazy
65 attribute allows external tools to inject shas from cache.
74 attribute allows external tools to inject shas from cache.
66 """
75 """
67 return self._get_all_revisions()
76 return self._get_all_revisions()
68
77
69 def run_git_command(self, cmd):
78 def run_git_command(self, cmd):
70 """
79 """
71 Runs given ``cmd`` as git command and returns tuple
80 Runs given ``cmd`` as git command and returns tuple
72 (returncode, stdout, stderr).
81 (returncode, stdout, stderr).
73
82
74 .. note::
83 .. note::
75 This method exists only until log/blame functionality is implemented
84 This method exists only until log/blame functionality is implemented
76 at Dulwich (see https://bugs.launchpad.net/bugs/645142). Parsing
85 at Dulwich (see https://bugs.launchpad.net/bugs/645142). Parsing
77 os command's output is road to hell...
86 os command's output is road to hell...
78
87
79 :param cmd: git command to be executed
88 :param cmd: git command to be executed
80 """
89 """
81
90
82 _copts = ['-c', 'core.quotepath=false', ]
91 _copts = ['-c', 'core.quotepath=false', ]
83 _str_cmd = False
92 _str_cmd = False
84 if isinstance(cmd, basestring):
93 if isinstance(cmd, basestring):
85 cmd = [cmd]
94 cmd = [cmd]
86 _str_cmd = True
95 _str_cmd = True
87
96
88 cmd = ['GIT_CONFIG_NOGLOBAL=1', 'git'] + _copts + cmd
97 cmd = ['GIT_CONFIG_NOGLOBAL=1', 'git'] + _copts + cmd
89 if _str_cmd:
98 if _str_cmd:
90 cmd = ' '.join(cmd)
99 cmd = ' '.join(cmd)
91 try:
100 try:
92 opts = dict(
101 opts = dict(
93 shell=isinstance(cmd, basestring),
102 shell=isinstance(cmd, basestring),
94 stdout=PIPE,
103 stdout=PIPE,
95 stderr=PIPE)
104 stderr=PIPE)
96 if os.path.isdir(self.path):
105 if os.path.isdir(self.path):
97 opts['cwd'] = self.path
106 opts['cwd'] = self.path
98 p = Popen(cmd, **opts)
107 p = Popen(cmd, **opts)
99 except OSError, err:
108 except OSError, err:
100 raise RepositoryError("Couldn't run git command (%s).\n"
109 raise RepositoryError("Couldn't run git command (%s).\n"
101 "Original error was:%s" % (cmd, err))
110 "Original error was:%s" % (cmd, err))
102 so, se = p.communicate()
111 so, se = p.communicate()
103 if not se.startswith("fatal: bad default revision 'HEAD'") and \
112 if not se.startswith("fatal: bad default revision 'HEAD'") and \
104 p.returncode != 0:
113 p.returncode != 0:
105 raise RepositoryError("Couldn't run git command (%s).\n"
114 raise RepositoryError("Couldn't run git command (%s).\n"
106 "stderr:\n%s" % (cmd, se))
115 "stderr:\n%s" % (cmd, se))
107 return so, se
116 return so, se
108
117
109 def _check_url(self, url):
118 def _check_url(self, url):
110 """
119 """
111 Functon will check given url and try to verify if it's a valid
120 Functon will check given url and try to verify if it's a valid
112 link. Sometimes it may happened that mercurial will issue basic
121 link. Sometimes it may happened that mercurial will issue basic
113 auth request that can cause whole API to hang when used from python
122 auth request that can cause whole API to hang when used from python
114 or other external calls.
123 or other external calls.
115
124
116 On failures it'll raise urllib2.HTTPError
125 On failures it'll raise urllib2.HTTPError
117 """
126 """
118
127
119 #TODO: implement this
128 #TODO: implement this
120 pass
129 pass
121
130
122 def _get_repo(self, create, src_url=None, update_after_clone=False,
131 def _get_repo(self, create, src_url=None, update_after_clone=False,
123 bare=False):
132 bare=False):
124 if create and os.path.exists(self.path):
133 if create and os.path.exists(self.path):
125 raise RepositoryError("Location already exist")
134 raise RepositoryError("Location already exist")
126 if src_url and not create:
135 if src_url and not create:
127 raise RepositoryError("Create should be set to True if src_url is "
136 raise RepositoryError("Create should be set to True if src_url is "
128 "given (clone operation creates repository)")
137 "given (clone operation creates repository)")
129 try:
138 try:
130 if create and src_url:
139 if create and src_url:
131 self._check_url(src_url)
140 self._check_url(src_url)
132 self.clone(src_url, update_after_clone, bare)
141 self.clone(src_url, update_after_clone, bare)
133 return Repo(self.path)
142 return Repo(self.path)
134 elif create:
143 elif create:
135 os.mkdir(self.path)
144 os.mkdir(self.path)
136 if bare:
145 if bare:
137 return Repo.init_bare(self.path)
146 return Repo.init_bare(self.path)
138 else:
147 else:
139 return Repo.init(self.path)
148 return Repo.init(self.path)
140 else:
149 else:
141 return Repo(self.path)
150 return Repo(self.path)
142 except (NotGitRepository, OSError), err:
151 except (NotGitRepository, OSError), err:
143 raise RepositoryError(err)
152 raise RepositoryError(err)
144
153
145 def _get_all_revisions(self):
154 def _get_all_revisions(self):
146 cmd = 'rev-list --all --date-order'
155 cmd = 'rev-list --all --date-order'
147 try:
156 try:
148 so, se = self.run_git_command(cmd)
157 so, se = self.run_git_command(cmd)
149 except RepositoryError:
158 except RepositoryError:
150 # Can be raised for empty repositories
159 # Can be raised for empty repositories
151 return []
160 return []
152 revisions = so.splitlines()
161 revisions = so.splitlines()
153 revisions.reverse()
162 revisions.reverse()
154 return revisions
163 return revisions
155
164
156 def _get_revision(self, revision):
165 def _get_revision(self, revision):
157 """
166 """
158 For git backend we always return integer here. This way we ensure
167 For git backend we always return integer here. This way we ensure
159 that changset's revision attribute would become integer.
168 that changset's revision attribute would become integer.
160 """
169 """
161 pattern = re.compile(r'^[[0-9a-fA-F]{12}|[0-9a-fA-F]{40}]$')
170 pattern = re.compile(r'^[[0-9a-fA-F]{12}|[0-9a-fA-F]{40}]$')
162 is_bstr = lambda o: isinstance(o, (str, unicode))
171 is_bstr = lambda o: isinstance(o, (str, unicode))
163 is_null = lambda o: len(o) == revision.count('0')
172 is_null = lambda o: len(o) == revision.count('0')
164
173
165 if len(self.revisions) == 0:
174 if len(self.revisions) == 0:
166 raise EmptyRepositoryError("There are no changesets yet")
175 raise EmptyRepositoryError("There are no changesets yet")
167
176
168 if revision in (None, '', 'tip', 'HEAD', 'head', -1):
177 if revision in (None, '', 'tip', 'HEAD', 'head', -1):
169 revision = self.revisions[-1]
178 revision = self.revisions[-1]
170
179
171 if ((is_bstr(revision) and revision.isdigit() and len(revision) < 12)
180 if ((is_bstr(revision) and revision.isdigit() and len(revision) < 12)
172 or isinstance(revision, int) or is_null(revision)):
181 or isinstance(revision, int) or is_null(revision)):
173 try:
182 try:
174 revision = self.revisions[int(revision)]
183 revision = self.revisions[int(revision)]
175 except:
184 except:
176 raise ChangesetDoesNotExistError("Revision %r does not exist "
185 raise ChangesetDoesNotExistError("Revision %r does not exist "
177 "for this repository %s" % (revision, self))
186 "for this repository %s" % (revision, self))
178
187
179 elif is_bstr(revision):
188 elif is_bstr(revision):
180 if not pattern.match(revision) or revision not in self.revisions:
189 if not pattern.match(revision) or revision not in self.revisions:
181 raise ChangesetDoesNotExistError("Revision %r does not exist "
190 raise ChangesetDoesNotExistError("Revision %r does not exist "
182 "for this repository %s" % (revision, self))
191 "for this repository %s" % (revision, self))
183
192
184 # Ensure we return full id
193 # Ensure we return full id
185 if not pattern.match(str(revision)):
194 if not pattern.match(str(revision)):
186 raise ChangesetDoesNotExistError("Given revision %r not recognized"
195 raise ChangesetDoesNotExistError("Given revision %r not recognized"
187 % revision)
196 % revision)
188 return revision
197 return revision
189
198
190 def _get_archives(self, archive_name='tip'):
199 def _get_archives(self, archive_name='tip'):
191
200
192 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
201 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
193 yield {"type": i[0], "extension": i[1], "node": archive_name}
202 yield {"type": i[0], "extension": i[1], "node": archive_name}
194
203
195 def _get_url(self, url):
204 def _get_url(self, url):
196 """
205 """
197 Returns normalized url. If schema is not given, would fall to
206 Returns normalized url. If schema is not given, would fall to
198 filesystem (``file:///``) schema.
207 filesystem (``file:///``) schema.
199 """
208 """
200 url = str(url)
209 url = str(url)
201 if url != 'default' and not '://' in url:
210 if url != 'default' and not '://' in url:
202 url = ':///'.join(('file', url))
211 url = ':///'.join(('file', url))
203 return url
212 return url
204
213
205 @LazyProperty
214 @LazyProperty
206 def name(self):
215 def name(self):
207 return os.path.basename(self.path)
216 return os.path.basename(self.path)
208
217
209 @LazyProperty
218 @LazyProperty
210 def last_change(self):
219 def last_change(self):
211 """
220 """
212 Returns last change made on this repository as datetime object
221 Returns last change made on this repository as datetime object
213 """
222 """
214 return date_fromtimestamp(self._get_mtime(), makedate()[1])
223 return date_fromtimestamp(self._get_mtime(), makedate()[1])
215
224
216 def _get_mtime(self):
225 def _get_mtime(self):
217 try:
226 try:
218 return time.mktime(self.get_changeset().date.timetuple())
227 return time.mktime(self.get_changeset().date.timetuple())
219 except RepositoryError:
228 except RepositoryError:
220 # fallback to filesystem
229 # fallback to filesystem
221 in_path = os.path.join(self.path, '.git', "index")
230 in_path = os.path.join(self.path, '.git', "index")
222 he_path = os.path.join(self.path, '.git', "HEAD")
231 he_path = os.path.join(self.path, '.git', "HEAD")
223 if os.path.exists(in_path):
232 if os.path.exists(in_path):
224 return os.stat(in_path).st_mtime
233 return os.stat(in_path).st_mtime
225 else:
234 else:
226 return os.stat(he_path).st_mtime
235 return os.stat(he_path).st_mtime
227
236
228 @LazyProperty
237 @LazyProperty
229 def description(self):
238 def description(self):
230 undefined_description = u'unknown'
239 undefined_description = u'unknown'
231 description_path = os.path.join(self.path, '.git', 'description')
240 description_path = os.path.join(self.path, '.git', 'description')
232 if os.path.isfile(description_path):
241 if os.path.isfile(description_path):
233 return safe_unicode(open(description_path).read())
242 return safe_unicode(open(description_path).read())
234 else:
243 else:
235 return undefined_description
244 return undefined_description
236
245
237 @LazyProperty
246 @LazyProperty
238 def contact(self):
247 def contact(self):
239 undefined_contact = u'Unknown'
248 undefined_contact = u'Unknown'
240 return undefined_contact
249 return undefined_contact
241
250
242 @property
251 @property
243 def branches(self):
252 def branches(self):
244 if not self.revisions:
253 if not self.revisions:
245 return {}
254 return {}
246 refs = self._repo.refs.as_dict()
255 refs = self._repo.refs.as_dict()
247 sortkey = lambda ctx: ctx[0]
256 sortkey = lambda ctx: ctx[0]
248 _branches = [('/'.join(ref.split('/')[2:]), head)
257 _branches = [('/'.join(ref.split('/')[2:]), head)
249 for ref, head in refs.items()
258 for ref, head in refs.items()
250 if ref.startswith('refs/heads/') and not ref.endswith('/HEAD')]
259 if ref.startswith('refs/heads/') and not ref.endswith('/HEAD')]
251 return OrderedDict(sorted(_branches, key=sortkey, reverse=False))
260 return OrderedDict(sorted(_branches, key=sortkey, reverse=False))
252
261
253 def _heads(self, reverse=False):
262 def _heads(self, reverse=False):
254 refs = self._repo.get_refs()
263 refs = self._repo.get_refs()
255 heads = {}
264 heads = {}
256
265
257 for key, val in refs.items():
266 for key, val in refs.items():
258 for ref_key in ['refs/heads/', 'refs/remotes/origin/']:
267 for ref_key in ['refs/heads/', 'refs/remotes/origin/']:
259 if key.startswith(ref_key):
268 if key.startswith(ref_key):
260 n = key[len(ref_key):]
269 n = key[len(ref_key):]
261 if n not in ['HEAD']:
270 if n not in ['HEAD']:
262 heads[n] = val
271 heads[n] = val
263
272
264 return heads if reverse else dict((y,x) for x,y in heads.iteritems())
273 return heads if reverse else dict((y,x) for x,y in heads.iteritems())
265
274
266 def _get_tags(self):
275 def _get_tags(self):
267 if not self.revisions:
276 if not self.revisions:
268 return {}
277 return {}
269 sortkey = lambda ctx: ctx[0]
278 sortkey = lambda ctx: ctx[0]
270 _tags = [('/'.join(ref.split('/')[2:]), head) for ref, head in
279 _tags = [('/'.join(ref.split('/')[2:]), head) for ref, head in
271 self._repo.get_refs().items() if ref.startswith('refs/tags/')]
280 self._repo.get_refs().items() if ref.startswith('refs/tags/')]
272 return OrderedDict(sorted(_tags, key=sortkey, reverse=True))
281 return OrderedDict(sorted(_tags, key=sortkey, reverse=True))
273
282
274 @LazyProperty
283 @LazyProperty
275 def tags(self):
284 def tags(self):
276 return self._get_tags()
285 return self._get_tags()
277
286
278 def tag(self, name, user, revision=None, message=None, date=None,
287 def tag(self, name, user, revision=None, message=None, date=None,
279 **kwargs):
288 **kwargs):
280 """
289 """
281 Creates and returns a tag for the given ``revision``.
290 Creates and returns a tag for the given ``revision``.
282
291
283 :param name: name for new tag
292 :param name: name for new tag
284 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
293 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
285 :param revision: changeset id for which new tag would be created
294 :param revision: changeset id for which new tag would be created
286 :param message: message of the tag's commit
295 :param message: message of the tag's commit
287 :param date: date of tag's commit
296 :param date: date of tag's commit
288
297
289 :raises TagAlreadyExistError: if tag with same name already exists
298 :raises TagAlreadyExistError: if tag with same name already exists
290 """
299 """
291 if name in self.tags:
300 if name in self.tags:
292 raise TagAlreadyExistError("Tag %s already exists" % name)
301 raise TagAlreadyExistError("Tag %s already exists" % name)
293 changeset = self.get_changeset(revision)
302 changeset = self.get_changeset(revision)
294 message = message or "Added tag %s for commit %s" % (name,
303 message = message or "Added tag %s for commit %s" % (name,
295 changeset.raw_id)
304 changeset.raw_id)
296 self._repo.refs["refs/tags/%s" % name] = changeset._commit.id
305 self._repo.refs["refs/tags/%s" % name] = changeset._commit.id
297
306
298 self.tags = self._get_tags()
307 self.tags = self._get_tags()
299 return changeset
308 return changeset
300
309
301 def remove_tag(self, name, user, message=None, date=None):
310 def remove_tag(self, name, user, message=None, date=None):
302 """
311 """
303 Removes tag with the given ``name``.
312 Removes tag with the given ``name``.
304
313
305 :param name: name of the tag to be removed
314 :param name: name of the tag to be removed
306 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
315 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
307 :param message: message of the tag's removal commit
316 :param message: message of the tag's removal commit
308 :param date: date of tag's removal commit
317 :param date: date of tag's removal commit
309
318
310 :raises TagDoesNotExistError: if tag with given name does not exists
319 :raises TagDoesNotExistError: if tag with given name does not exists
311 """
320 """
312 if name not in self.tags:
321 if name not in self.tags:
313 raise TagDoesNotExistError("Tag %s does not exist" % name)
322 raise TagDoesNotExistError("Tag %s does not exist" % name)
314 tagpath = posixpath.join(self._repo.refs.path, 'refs', 'tags', name)
323 tagpath = posixpath.join(self._repo.refs.path, 'refs', 'tags', name)
315 try:
324 try:
316 os.remove(tagpath)
325 os.remove(tagpath)
317 self.tags = self._get_tags()
326 self.tags = self._get_tags()
318 except OSError, e:
327 except OSError, e:
319 raise RepositoryError(e.strerror)
328 raise RepositoryError(e.strerror)
320
329
321 def get_changeset(self, revision=None):
330 def get_changeset(self, revision=None):
322 """
331 """
323 Returns ``GitChangeset`` object representing commit from git repository
332 Returns ``GitChangeset`` object representing commit from git repository
324 at the given revision or head (most recent commit) if None given.
333 at the given revision or head (most recent commit) if None given.
325 """
334 """
326 if isinstance(revision, GitChangeset):
335 if isinstance(revision, GitChangeset):
327 return revision
336 return revision
328 revision = self._get_revision(revision)
337 revision = self._get_revision(revision)
329 changeset = GitChangeset(repository=self, revision=revision)
338 changeset = GitChangeset(repository=self, revision=revision)
330 return changeset
339 return changeset
331
340
332 def get_changesets(self, start=None, end=None, start_date=None,
341 def get_changesets(self, start=None, end=None, start_date=None,
333 end_date=None, branch_name=None, reverse=False):
342 end_date=None, branch_name=None, reverse=False):
334 """
343 """
335 Returns iterator of ``GitChangeset`` objects from start to end (both
344 Returns iterator of ``GitChangeset`` objects from start to end (both
336 are inclusive), in ascending date order (unless ``reverse`` is set).
345 are inclusive), in ascending date order (unless ``reverse`` is set).
337
346
338 :param start: changeset ID, as str; first returned changeset
347 :param start: changeset ID, as str; first returned changeset
339 :param end: changeset ID, as str; last returned changeset
348 :param end: changeset ID, as str; last returned changeset
340 :param start_date: if specified, changesets with commit date less than
349 :param start_date: if specified, changesets with commit date less than
341 ``start_date`` would be filtered out from returned set
350 ``start_date`` would be filtered out from returned set
342 :param end_date: if specified, changesets with commit date greater than
351 :param end_date: if specified, changesets with commit date greater than
343 ``end_date`` would be filtered out from returned set
352 ``end_date`` would be filtered out from returned set
344 :param branch_name: if specified, changesets not reachable from given
353 :param branch_name: if specified, changesets not reachable from given
345 branch would be filtered out from returned set
354 branch would be filtered out from returned set
346 :param reverse: if ``True``, returned generator would be reversed
355 :param reverse: if ``True``, returned generator would be reversed
347 (meaning that returned changesets would have descending date order)
356 (meaning that returned changesets would have descending date order)
348
357
349 :raise BranchDoesNotExistError: If given ``branch_name`` does not
358 :raise BranchDoesNotExistError: If given ``branch_name`` does not
350 exist.
359 exist.
351 :raise ChangesetDoesNotExistError: If changeset for given ``start`` or
360 :raise ChangesetDoesNotExistError: If changeset for given ``start`` or
352 ``end`` could not be found.
361 ``end`` could not be found.
353
362
354 """
363 """
355 if branch_name and branch_name not in self.branches:
364 if branch_name and branch_name not in self.branches:
356 raise BranchDoesNotExistError("Branch '%s' not found" \
365 raise BranchDoesNotExistError("Branch '%s' not found" \
357 % branch_name)
366 % branch_name)
358 # %H at format means (full) commit hash, initial hashes are retrieved
367 # %H at format means (full) commit hash, initial hashes are retrieved
359 # in ascending date order
368 # in ascending date order
360 cmd_template = 'log --date-order --reverse --pretty=format:"%H"'
369 cmd_template = 'log --date-order --reverse --pretty=format:"%H"'
361 cmd_params = {}
370 cmd_params = {}
362 if start_date:
371 if start_date:
363 cmd_template += ' --since "$since"'
372 cmd_template += ' --since "$since"'
364 cmd_params['since'] = start_date.strftime('%m/%d/%y %H:%M:%S')
373 cmd_params['since'] = start_date.strftime('%m/%d/%y %H:%M:%S')
365 if end_date:
374 if end_date:
366 cmd_template += ' --until "$until"'
375 cmd_template += ' --until "$until"'
367 cmd_params['until'] = end_date.strftime('%m/%d/%y %H:%M:%S')
376 cmd_params['until'] = end_date.strftime('%m/%d/%y %H:%M:%S')
368 if branch_name:
377 if branch_name:
369 cmd_template += ' $branch_name'
378 cmd_template += ' $branch_name'
370 cmd_params['branch_name'] = branch_name
379 cmd_params['branch_name'] = branch_name
371 else:
380 else:
372 cmd_template += ' --all'
381 cmd_template += ' --all'
373
382
374 cmd = Template(cmd_template).safe_substitute(**cmd_params)
383 cmd = Template(cmd_template).safe_substitute(**cmd_params)
375 revs = self.run_git_command(cmd)[0].splitlines()
384 revs = self.run_git_command(cmd)[0].splitlines()
376 start_pos = 0
385 start_pos = 0
377 end_pos = len(revs)
386 end_pos = len(revs)
378 if start:
387 if start:
379 _start = self._get_revision(start)
388 _start = self._get_revision(start)
380 try:
389 try:
381 start_pos = revs.index(_start)
390 start_pos = revs.index(_start)
382 except ValueError:
391 except ValueError:
383 pass
392 pass
384
393
385 if end is not None:
394 if end is not None:
386 _end = self._get_revision(end)
395 _end = self._get_revision(end)
387 try:
396 try:
388 end_pos = revs.index(_end)
397 end_pos = revs.index(_end)
389 except ValueError:
398 except ValueError:
390 pass
399 pass
391
400
392 if None not in [start, end] and start_pos > end_pos:
401 if None not in [start, end] and start_pos > end_pos:
393 raise RepositoryError('start cannot be after end')
402 raise RepositoryError('start cannot be after end')
394
403
395 if end_pos is not None:
404 if end_pos is not None:
396 end_pos += 1
405 end_pos += 1
397
406
398 revs = revs[start_pos:end_pos]
407 revs = revs[start_pos:end_pos]
399 if reverse:
408 if reverse:
400 revs = reversed(revs)
409 revs = reversed(revs)
401 for rev in revs:
410 for rev in revs:
402 yield self.get_changeset(rev)
411 yield self.get_changeset(rev)
403
412
404 def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
413 def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
405 context=3):
414 context=3):
406 """
415 """
407 Returns (git like) *diff*, as plain text. Shows changes introduced by
416 Returns (git like) *diff*, as plain text. Shows changes introduced by
408 ``rev2`` since ``rev1``.
417 ``rev2`` since ``rev1``.
409
418
410 :param rev1: Entry point from which diff is shown. Can be
419 :param rev1: Entry point from which diff is shown. Can be
411 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
420 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
412 the changes since empty state of the repository until ``rev2``
421 the changes since empty state of the repository until ``rev2``
413 :param rev2: Until which revision changes should be shown.
422 :param rev2: Until which revision changes should be shown.
414 :param ignore_whitespace: If set to ``True``, would not show whitespace
423 :param ignore_whitespace: If set to ``True``, would not show whitespace
415 changes. Defaults to ``False``.
424 changes. Defaults to ``False``.
416 :param context: How many lines before/after changed lines should be
425 :param context: How many lines before/after changed lines should be
417 shown. Defaults to ``3``.
426 shown. Defaults to ``3``.
418 """
427 """
419 flags = ['-U%s' % context]
428 flags = ['-U%s' % context]
420 if ignore_whitespace:
429 if ignore_whitespace:
421 flags.append('-w')
430 flags.append('-w')
422
431
423 if rev1 == self.EMPTY_CHANGESET:
432 if rev1 == self.EMPTY_CHANGESET:
424 rev2 = self.get_changeset(rev2).raw_id
433 rev2 = self.get_changeset(rev2).raw_id
425 cmd = ' '.join(['show'] + flags + [rev2])
434 cmd = ' '.join(['show'] + flags + [rev2])
426 else:
435 else:
427 rev1 = self.get_changeset(rev1).raw_id
436 rev1 = self.get_changeset(rev1).raw_id
428 rev2 = self.get_changeset(rev2).raw_id
437 rev2 = self.get_changeset(rev2).raw_id
429 cmd = ' '.join(['diff'] + flags + [rev1, rev2])
438 cmd = ' '.join(['diff'] + flags + [rev1, rev2])
430
439
431 if path:
440 if path:
432 cmd += ' -- "%s"' % path
441 cmd += ' -- "%s"' % path
433 stdout, stderr = self.run_git_command(cmd)
442 stdout, stderr = self.run_git_command(cmd)
434 # If we used 'show' command, strip first few lines (until actual diff
443 # If we used 'show' command, strip first few lines (until actual diff
435 # starts)
444 # starts)
436 if rev1 == self.EMPTY_CHANGESET:
445 if rev1 == self.EMPTY_CHANGESET:
437 lines = stdout.splitlines()
446 lines = stdout.splitlines()
438 x = 0
447 x = 0
439 for line in lines:
448 for line in lines:
440 if line.startswith('diff'):
449 if line.startswith('diff'):
441 break
450 break
442 x += 1
451 x += 1
443 # Append new line just like 'diff' command do
452 # Append new line just like 'diff' command do
444 stdout = '\n'.join(lines[x:]) + '\n'
453 stdout = '\n'.join(lines[x:]) + '\n'
445 return stdout
454 return stdout
446
455
447 @LazyProperty
456 @LazyProperty
448 def in_memory_changeset(self):
457 def in_memory_changeset(self):
449 """
458 """
450 Returns ``GitInMemoryChangeset`` object for this repository.
459 Returns ``GitInMemoryChangeset`` object for this repository.
451 """
460 """
452 return GitInMemoryChangeset(self)
461 return GitInMemoryChangeset(self)
453
462
454 def clone(self, url, update_after_clone=True, bare=False):
463 def clone(self, url, update_after_clone=True, bare=False):
455 """
464 """
456 Tries to clone changes from external location.
465 Tries to clone changes from external location.
457
466
458 :param update_after_clone: If set to ``False``, git won't checkout
467 :param update_after_clone: If set to ``False``, git won't checkout
459 working directory
468 working directory
460 :param bare: If set to ``True``, repository would be cloned into
469 :param bare: If set to ``True``, repository would be cloned into
461 *bare* git repository (no working directory at all).
470 *bare* git repository (no working directory at all).
462 """
471 """
463 url = self._get_url(url)
472 url = self._get_url(url)
464 cmd = ['clone']
473 cmd = ['clone']
465 if bare:
474 if bare:
466 cmd.append('--bare')
475 cmd.append('--bare')
467 elif not update_after_clone:
476 elif not update_after_clone:
468 cmd.append('--no-checkout')
477 cmd.append('--no-checkout')
469 cmd += ['--', '"%s"' % url, '"%s"' % self.path]
478 cmd += ['--', '"%s"' % url, '"%s"' % self.path]
470 cmd = ' '.join(cmd)
479 cmd = ' '.join(cmd)
471 # If error occurs run_git_command raises RepositoryError already
480 # If error occurs run_git_command raises RepositoryError already
472 self.run_git_command(cmd)
481 self.run_git_command(cmd)
473
482
483 def pull(self, url):
484 """
485 Tries to pull changes from external location.
486 """
487 url = self._get_url(url)
488 cmd = ['pull']
489 cmd.append("--ff-only")
490 cmd = ' '.join(cmd)
491 # If error occurs run_git_command raises RepositoryError already
492 self.run_git_command(cmd)
493
474 @LazyProperty
494 @LazyProperty
475 def workdir(self):
495 def workdir(self):
476 """
496 """
477 Returns ``Workdir`` instance for this repository.
497 Returns ``Workdir`` instance for this repository.
478 """
498 """
479 return GitWorkdir(self)
499 return GitWorkdir(self)
480
500
481 def get_config_value(self, section, name, config_file=None):
501 def get_config_value(self, section, name, config_file=None):
482 """
502 """
483 Returns configuration value for a given [``section``] and ``name``.
503 Returns configuration value for a given [``section``] and ``name``.
484
504
485 :param section: Section we want to retrieve value from
505 :param section: Section we want to retrieve value from
486 :param name: Name of configuration we want to retrieve
506 :param name: Name of configuration we want to retrieve
487 :param config_file: A path to file which should be used to retrieve
507 :param config_file: A path to file which should be used to retrieve
488 configuration from (might also be a list of file paths)
508 configuration from (might also be a list of file paths)
489 """
509 """
490 if config_file is None:
510 if config_file is None:
491 config_file = []
511 config_file = []
492 elif isinstance(config_file, basestring):
512 elif isinstance(config_file, basestring):
493 config_file = [config_file]
513 config_file = [config_file]
494
514
495 def gen_configs():
515 def gen_configs():
496 for path in config_file + self._config_files:
516 for path in config_file + self._config_files:
497 try:
517 try:
498 yield ConfigFile.from_path(path)
518 yield ConfigFile.from_path(path)
499 except (IOError, OSError, ValueError):
519 except (IOError, OSError, ValueError):
500 continue
520 continue
501
521
502 for config in gen_configs():
522 for config in gen_configs():
503 try:
523 try:
504 return config.get(section, name)
524 return config.get(section, name)
505 except KeyError:
525 except KeyError:
506 continue
526 continue
507 return None
527 return None
508
528
509 def get_user_name(self, config_file=None):
529 def get_user_name(self, config_file=None):
510 """
530 """
511 Returns user's name from global configuration file.
531 Returns user's name from global configuration file.
512
532
513 :param config_file: A path to file which should be used to retrieve
533 :param config_file: A path to file which should be used to retrieve
514 configuration from (might also be a list of file paths)
534 configuration from (might also be a list of file paths)
515 """
535 """
516 return self.get_config_value('user', 'name', config_file)
536 return self.get_config_value('user', 'name', config_file)
517
537
518 def get_user_email(self, config_file=None):
538 def get_user_email(self, config_file=None):
519 """
539 """
520 Returns user's email from global configuration file.
540 Returns user's email from global configuration file.
521
541
522 :param config_file: A path to file which should be used to retrieve
542 :param config_file: A path to file which should be used to retrieve
523 configuration from (might also be a list of file paths)
543 configuration from (might also be a list of file paths)
524 """
544 """
525 return self.get_config_value('user', 'email', config_file)
545 return self.get_config_value('user', 'email', config_file)
@@ -1,466 +1,466 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.model.scm
3 rhodecode.model.scm
4 ~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~
5
5
6 Scm model for RhodeCode
6 Scm model for RhodeCode
7
7
8 :created_on: Apr 9, 2010
8 :created_on: Apr 9, 2010
9 :author: marcink
9 :author: marcink
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 :license: GPLv3, see COPYING for more details.
11 :license: GPLv3, see COPYING for more details.
12 """
12 """
13 # This program is free software: you can redistribute it and/or modify
13 # This program is free software: you can redistribute it and/or modify
14 # it under the terms of the GNU General Public License as published by
14 # it under the terms of the GNU General Public License as published by
15 # the Free Software Foundation, either version 3 of the License, or
15 # the Free Software Foundation, either version 3 of the License, or
16 # (at your option) any later version.
16 # (at your option) any later version.
17 #
17 #
18 # This program is distributed in the hope that it will be useful,
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # GNU General Public License for more details.
21 # GNU General Public License for more details.
22 #
22 #
23 # You should have received a copy of the GNU General Public License
23 # You should have received a copy of the GNU General Public License
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 import os
25 import os
26 import time
26 import time
27 import traceback
27 import traceback
28 import logging
28 import logging
29 import cStringIO
29 import cStringIO
30
30
31 from rhodecode.lib.vcs import get_backend
31 from rhodecode.lib.vcs import get_backend
32 from rhodecode.lib.vcs.exceptions import RepositoryError
32 from rhodecode.lib.vcs.exceptions import RepositoryError
33 from rhodecode.lib.vcs.utils.lazy import LazyProperty
33 from rhodecode.lib.vcs.utils.lazy import LazyProperty
34 from rhodecode.lib.vcs.nodes import FileNode
34 from rhodecode.lib.vcs.nodes import FileNode
35
35
36 from rhodecode import BACKENDS
36 from rhodecode import BACKENDS
37 from rhodecode.lib import helpers as h
37 from rhodecode.lib import helpers as h
38 from rhodecode.lib.utils2 import safe_str, safe_unicode
38 from rhodecode.lib.utils2 import safe_str, safe_unicode
39 from rhodecode.lib.auth import HasRepoPermissionAny, HasReposGroupPermissionAny
39 from rhodecode.lib.auth import HasRepoPermissionAny, HasReposGroupPermissionAny
40 from rhodecode.lib.utils import get_repos as get_filesystem_repos, make_ui, \
40 from rhodecode.lib.utils import get_repos as get_filesystem_repos, make_ui, \
41 action_logger, EmptyChangeset, REMOVED_REPO_PAT
41 action_logger, EmptyChangeset, REMOVED_REPO_PAT
42 from rhodecode.model import BaseModel
42 from rhodecode.model import BaseModel
43 from rhodecode.model.db import Repository, RhodeCodeUi, CacheInvalidation, \
43 from rhodecode.model.db import Repository, RhodeCodeUi, CacheInvalidation, \
44 UserFollowing, UserLog, User, RepoGroup
44 UserFollowing, UserLog, User, RepoGroup
45
45
46 log = logging.getLogger(__name__)
46 log = logging.getLogger(__name__)
47
47
48
48
49 class UserTemp(object):
49 class UserTemp(object):
50 def __init__(self, user_id):
50 def __init__(self, user_id):
51 self.user_id = user_id
51 self.user_id = user_id
52
52
53 def __repr__(self):
53 def __repr__(self):
54 return "<%s('id:%s')>" % (self.__class__.__name__, self.user_id)
54 return "<%s('id:%s')>" % (self.__class__.__name__, self.user_id)
55
55
56
56
57 class RepoTemp(object):
57 class RepoTemp(object):
58 def __init__(self, repo_id):
58 def __init__(self, repo_id):
59 self.repo_id = repo_id
59 self.repo_id = repo_id
60
60
61 def __repr__(self):
61 def __repr__(self):
62 return "<%s('id:%s')>" % (self.__class__.__name__, self.repo_id)
62 return "<%s('id:%s')>" % (self.__class__.__name__, self.repo_id)
63
63
64
64
65 class CachedRepoList(object):
65 class CachedRepoList(object):
66
66
67 def __init__(self, db_repo_list, repos_path, order_by=None):
67 def __init__(self, db_repo_list, repos_path, order_by=None):
68 self.db_repo_list = db_repo_list
68 self.db_repo_list = db_repo_list
69 self.repos_path = repos_path
69 self.repos_path = repos_path
70 self.order_by = order_by
70 self.order_by = order_by
71 self.reversed = (order_by or '').startswith('-')
71 self.reversed = (order_by or '').startswith('-')
72
72
73 def __len__(self):
73 def __len__(self):
74 return len(self.db_repo_list)
74 return len(self.db_repo_list)
75
75
76 def __repr__(self):
76 def __repr__(self):
77 return '<%s (%s)>' % (self.__class__.__name__, self.__len__())
77 return '<%s (%s)>' % (self.__class__.__name__, self.__len__())
78
78
79 def __iter__(self):
79 def __iter__(self):
80 for dbr in self.db_repo_list:
80 for dbr in self.db_repo_list:
81 scmr = dbr.scm_instance_cached
81 scmr = dbr.scm_instance_cached
82 # check permission at this level
82 # check permission at this level
83 if not HasRepoPermissionAny(
83 if not HasRepoPermissionAny(
84 'repository.read', 'repository.write', 'repository.admin'
84 'repository.read', 'repository.write', 'repository.admin'
85 )(dbr.repo_name, 'get repo check'):
85 )(dbr.repo_name, 'get repo check'):
86 continue
86 continue
87
87
88 if scmr is None:
88 if scmr is None:
89 log.error(
89 log.error(
90 '%s this repository is present in database but it '
90 '%s this repository is present in database but it '
91 'cannot be created as an scm instance' % dbr.repo_name
91 'cannot be created as an scm instance' % dbr.repo_name
92 )
92 )
93 continue
93 continue
94
94
95 last_change = scmr.last_change
95 last_change = scmr.last_change
96 tip = h.get_changeset_safe(scmr, 'tip')
96 tip = h.get_changeset_safe(scmr, 'tip')
97
97
98 tmp_d = {}
98 tmp_d = {}
99 tmp_d['name'] = dbr.repo_name
99 tmp_d['name'] = dbr.repo_name
100 tmp_d['name_sort'] = tmp_d['name'].lower()
100 tmp_d['name_sort'] = tmp_d['name'].lower()
101 tmp_d['description'] = dbr.description
101 tmp_d['description'] = dbr.description
102 tmp_d['description_sort'] = tmp_d['description']
102 tmp_d['description_sort'] = tmp_d['description']
103 tmp_d['last_change'] = last_change
103 tmp_d['last_change'] = last_change
104 tmp_d['last_change_sort'] = time.mktime(last_change.timetuple())
104 tmp_d['last_change_sort'] = time.mktime(last_change.timetuple())
105 tmp_d['tip'] = tip.raw_id
105 tmp_d['tip'] = tip.raw_id
106 tmp_d['tip_sort'] = tip.revision
106 tmp_d['tip_sort'] = tip.revision
107 tmp_d['rev'] = tip.revision
107 tmp_d['rev'] = tip.revision
108 tmp_d['contact'] = dbr.user.full_contact
108 tmp_d['contact'] = dbr.user.full_contact
109 tmp_d['contact_sort'] = tmp_d['contact']
109 tmp_d['contact_sort'] = tmp_d['contact']
110 tmp_d['owner_sort'] = tmp_d['contact']
110 tmp_d['owner_sort'] = tmp_d['contact']
111 tmp_d['repo_archives'] = list(scmr._get_archives())
111 tmp_d['repo_archives'] = list(scmr._get_archives())
112 tmp_d['last_msg'] = tip.message
112 tmp_d['last_msg'] = tip.message
113 tmp_d['author'] = tip.author
113 tmp_d['author'] = tip.author
114 tmp_d['dbrepo'] = dbr.get_dict()
114 tmp_d['dbrepo'] = dbr.get_dict()
115 tmp_d['dbrepo_fork'] = dbr.fork.get_dict() if dbr.fork else {}
115 tmp_d['dbrepo_fork'] = dbr.fork.get_dict() if dbr.fork else {}
116 yield tmp_d
116 yield tmp_d
117
117
118
118
119 class GroupList(object):
119 class GroupList(object):
120
120
121 def __init__(self, db_repo_group_list):
121 def __init__(self, db_repo_group_list):
122 self.db_repo_group_list = db_repo_group_list
122 self.db_repo_group_list = db_repo_group_list
123
123
124 def __len__(self):
124 def __len__(self):
125 return len(self.db_repo_group_list)
125 return len(self.db_repo_group_list)
126
126
127 def __repr__(self):
127 def __repr__(self):
128 return '<%s (%s)>' % (self.__class__.__name__, self.__len__())
128 return '<%s (%s)>' % (self.__class__.__name__, self.__len__())
129
129
130 def __iter__(self):
130 def __iter__(self):
131 for dbgr in self.db_repo_group_list:
131 for dbgr in self.db_repo_group_list:
132 # check permission at this level
132 # check permission at this level
133 if not HasReposGroupPermissionAny(
133 if not HasReposGroupPermissionAny(
134 'group.read', 'group.write', 'group.admin'
134 'group.read', 'group.write', 'group.admin'
135 )(dbgr.group_name, 'get group repo check'):
135 )(dbgr.group_name, 'get group repo check'):
136 continue
136 continue
137
137
138 yield dbgr
138 yield dbgr
139
139
140
140
141 class ScmModel(BaseModel):
141 class ScmModel(BaseModel):
142 """
142 """
143 Generic Scm Model
143 Generic Scm Model
144 """
144 """
145
145
146 def __get_repo(self, instance):
146 def __get_repo(self, instance):
147 cls = Repository
147 cls = Repository
148 if isinstance(instance, cls):
148 if isinstance(instance, cls):
149 return instance
149 return instance
150 elif isinstance(instance, int) or str(instance).isdigit():
150 elif isinstance(instance, int) or str(instance).isdigit():
151 return cls.get(instance)
151 return cls.get(instance)
152 elif isinstance(instance, basestring):
152 elif isinstance(instance, basestring):
153 return cls.get_by_repo_name(instance)
153 return cls.get_by_repo_name(instance)
154 elif instance:
154 elif instance:
155 raise Exception('given object must be int, basestr or Instance'
155 raise Exception('given object must be int, basestr or Instance'
156 ' of %s got %s' % (type(cls), type(instance)))
156 ' of %s got %s' % (type(cls), type(instance)))
157
157
158 @LazyProperty
158 @LazyProperty
159 def repos_path(self):
159 def repos_path(self):
160 """
160 """
161 Get's the repositories root path from database
161 Get's the repositories root path from database
162 """
162 """
163
163
164 q = self.sa.query(RhodeCodeUi).filter(RhodeCodeUi.ui_key == '/').one()
164 q = self.sa.query(RhodeCodeUi).filter(RhodeCodeUi.ui_key == '/').one()
165
165
166 return q.ui_value
166 return q.ui_value
167
167
168 def repo_scan(self, repos_path=None):
168 def repo_scan(self, repos_path=None):
169 """
169 """
170 Listing of repositories in given path. This path should not be a
170 Listing of repositories in given path. This path should not be a
171 repository itself. Return a dictionary of repository objects
171 repository itself. Return a dictionary of repository objects
172
172
173 :param repos_path: path to directory containing repositories
173 :param repos_path: path to directory containing repositories
174 """
174 """
175
175
176 if repos_path is None:
176 if repos_path is None:
177 repos_path = self.repos_path
177 repos_path = self.repos_path
178
178
179 log.info('scanning for repositories in %s' % repos_path)
179 log.info('scanning for repositories in %s' % repos_path)
180
180
181 baseui = make_ui('db')
181 baseui = make_ui('db')
182 repos = {}
182 repos = {}
183
183
184 for name, path in get_filesystem_repos(repos_path, recursive=True):
184 for name, path in get_filesystem_repos(repos_path, recursive=True):
185 # skip removed repos
185 # skip removed repos
186 if REMOVED_REPO_PAT.match(name):
186 if REMOVED_REPO_PAT.match(name):
187 continue
187 continue
188
188
189 # name need to be decomposed and put back together using the /
189 # name need to be decomposed and put back together using the /
190 # since this is internal storage separator for rhodecode
190 # since this is internal storage separator for rhodecode
191 name = Repository.url_sep().join(name.split(os.sep))
191 name = Repository.url_sep().join(name.split(os.sep))
192
192
193 try:
193 try:
194 if name in repos:
194 if name in repos:
195 raise RepositoryError('Duplicate repository name %s '
195 raise RepositoryError('Duplicate repository name %s '
196 'found in %s' % (name, path))
196 'found in %s' % (name, path))
197 else:
197 else:
198
198
199 klass = get_backend(path[0])
199 klass = get_backend(path[0])
200
200
201 if path[0] == 'hg' and path[0] in BACKENDS.keys():
201 if path[0] == 'hg' and path[0] in BACKENDS.keys():
202 repos[name] = klass(safe_str(path[1]), baseui=baseui)
202 repos[name] = klass(safe_str(path[1]), baseui=baseui)
203
203
204 if path[0] == 'git' and path[0] in BACKENDS.keys():
204 if path[0] == 'git' and path[0] in BACKENDS.keys():
205 repos[name] = klass(path[1])
205 repos[name] = klass(path[1])
206 except OSError:
206 except OSError:
207 continue
207 continue
208
208
209 return repos
209 return repos
210
210
211 def get_repos(self, all_repos=None, sort_key=None):
211 def get_repos(self, all_repos=None, sort_key=None):
212 """
212 """
213 Get all repos from db and for each repo create it's
213 Get all repos from db and for each repo create it's
214 backend instance and fill that backed with information from database
214 backend instance and fill that backed with information from database
215
215
216 :param all_repos: list of repository names as strings
216 :param all_repos: list of repository names as strings
217 give specific repositories list, good for filtering
217 give specific repositories list, good for filtering
218 """
218 """
219 if all_repos is None:
219 if all_repos is None:
220 all_repos = self.sa.query(Repository)\
220 all_repos = self.sa.query(Repository)\
221 .filter(Repository.group_id == None)\
221 .filter(Repository.group_id == None)\
222 .order_by(Repository.repo_name).all()
222 .order_by(Repository.repo_name).all()
223
223
224 repo_iter = CachedRepoList(all_repos, repos_path=self.repos_path,
224 repo_iter = CachedRepoList(all_repos, repos_path=self.repos_path,
225 order_by=sort_key)
225 order_by=sort_key)
226
226
227 return repo_iter
227 return repo_iter
228
228
229 def get_repos_groups(self, all_groups=None):
229 def get_repos_groups(self, all_groups=None):
230 if all_groups is None:
230 if all_groups is None:
231 all_groups = RepoGroup.query()\
231 all_groups = RepoGroup.query()\
232 .filter(RepoGroup.group_parent_id == None).all()
232 .filter(RepoGroup.group_parent_id == None).all()
233 group_iter = GroupList(all_groups)
233 group_iter = GroupList(all_groups)
234
234
235 return group_iter
235 return group_iter
236
236
237 def mark_for_invalidation(self, repo_name):
237 def mark_for_invalidation(self, repo_name):
238 """
238 """
239 Puts cache invalidation task into db for
239 Puts cache invalidation task into db for
240 further global cache invalidation
240 further global cache invalidation
241
241
242 :param repo_name: this repo that should invalidation take place
242 :param repo_name: this repo that should invalidation take place
243 """
243 """
244 CacheInvalidation.set_invalidate(repo_name)
244 CacheInvalidation.set_invalidate(repo_name)
245
245
246 def toggle_following_repo(self, follow_repo_id, user_id):
246 def toggle_following_repo(self, follow_repo_id, user_id):
247
247
248 f = self.sa.query(UserFollowing)\
248 f = self.sa.query(UserFollowing)\
249 .filter(UserFollowing.follows_repo_id == follow_repo_id)\
249 .filter(UserFollowing.follows_repo_id == follow_repo_id)\
250 .filter(UserFollowing.user_id == user_id).scalar()
250 .filter(UserFollowing.user_id == user_id).scalar()
251
251
252 if f is not None:
252 if f is not None:
253 try:
253 try:
254 self.sa.delete(f)
254 self.sa.delete(f)
255 action_logger(UserTemp(user_id),
255 action_logger(UserTemp(user_id),
256 'stopped_following_repo',
256 'stopped_following_repo',
257 RepoTemp(follow_repo_id))
257 RepoTemp(follow_repo_id))
258 return
258 return
259 except:
259 except:
260 log.error(traceback.format_exc())
260 log.error(traceback.format_exc())
261 raise
261 raise
262
262
263 try:
263 try:
264 f = UserFollowing()
264 f = UserFollowing()
265 f.user_id = user_id
265 f.user_id = user_id
266 f.follows_repo_id = follow_repo_id
266 f.follows_repo_id = follow_repo_id
267 self.sa.add(f)
267 self.sa.add(f)
268
268
269 action_logger(UserTemp(user_id),
269 action_logger(UserTemp(user_id),
270 'started_following_repo',
270 'started_following_repo',
271 RepoTemp(follow_repo_id))
271 RepoTemp(follow_repo_id))
272 except:
272 except:
273 log.error(traceback.format_exc())
273 log.error(traceback.format_exc())
274 raise
274 raise
275
275
276 def toggle_following_user(self, follow_user_id, user_id):
276 def toggle_following_user(self, follow_user_id, user_id):
277 f = self.sa.query(UserFollowing)\
277 f = self.sa.query(UserFollowing)\
278 .filter(UserFollowing.follows_user_id == follow_user_id)\
278 .filter(UserFollowing.follows_user_id == follow_user_id)\
279 .filter(UserFollowing.user_id == user_id).scalar()
279 .filter(UserFollowing.user_id == user_id).scalar()
280
280
281 if f is not None:
281 if f is not None:
282 try:
282 try:
283 self.sa.delete(f)
283 self.sa.delete(f)
284 return
284 return
285 except:
285 except:
286 log.error(traceback.format_exc())
286 log.error(traceback.format_exc())
287 raise
287 raise
288
288
289 try:
289 try:
290 f = UserFollowing()
290 f = UserFollowing()
291 f.user_id = user_id
291 f.user_id = user_id
292 f.follows_user_id = follow_user_id
292 f.follows_user_id = follow_user_id
293 self.sa.add(f)
293 self.sa.add(f)
294 except:
294 except:
295 log.error(traceback.format_exc())
295 log.error(traceback.format_exc())
296 raise
296 raise
297
297
298 def is_following_repo(self, repo_name, user_id, cache=False):
298 def is_following_repo(self, repo_name, user_id, cache=False):
299 r = self.sa.query(Repository)\
299 r = self.sa.query(Repository)\
300 .filter(Repository.repo_name == repo_name).scalar()
300 .filter(Repository.repo_name == repo_name).scalar()
301
301
302 f = self.sa.query(UserFollowing)\
302 f = self.sa.query(UserFollowing)\
303 .filter(UserFollowing.follows_repository == r)\
303 .filter(UserFollowing.follows_repository == r)\
304 .filter(UserFollowing.user_id == user_id).scalar()
304 .filter(UserFollowing.user_id == user_id).scalar()
305
305
306 return f is not None
306 return f is not None
307
307
308 def is_following_user(self, username, user_id, cache=False):
308 def is_following_user(self, username, user_id, cache=False):
309 u = User.get_by_username(username)
309 u = User.get_by_username(username)
310
310
311 f = self.sa.query(UserFollowing)\
311 f = self.sa.query(UserFollowing)\
312 .filter(UserFollowing.follows_user == u)\
312 .filter(UserFollowing.follows_user == u)\
313 .filter(UserFollowing.user_id == user_id).scalar()
313 .filter(UserFollowing.user_id == user_id).scalar()
314
314
315 return f is not None
315 return f is not None
316
316
317 def get_followers(self, repo_id):
317 def get_followers(self, repo_id):
318 if not isinstance(repo_id, int):
318 if not isinstance(repo_id, int):
319 repo_id = getattr(Repository.get_by_repo_name(repo_id), 'repo_id')
319 repo_id = getattr(Repository.get_by_repo_name(repo_id), 'repo_id')
320
320
321 return self.sa.query(UserFollowing)\
321 return self.sa.query(UserFollowing)\
322 .filter(UserFollowing.follows_repo_id == repo_id).count()
322 .filter(UserFollowing.follows_repo_id == repo_id).count()
323
323
324 def get_forks(self, repo_id):
324 def get_forks(self, repo_id):
325 if not isinstance(repo_id, int):
325 if not isinstance(repo_id, int):
326 repo_id = getattr(Repository.get_by_repo_name(repo_id), 'repo_id')
326 repo_id = getattr(Repository.get_by_repo_name(repo_id), 'repo_id')
327
327
328 return self.sa.query(Repository)\
328 return self.sa.query(Repository)\
329 .filter(Repository.fork_id == repo_id).count()
329 .filter(Repository.fork_id == repo_id).count()
330
330
331 def mark_as_fork(self, repo, fork, user):
331 def mark_as_fork(self, repo, fork, user):
332 repo = self.__get_repo(repo)
332 repo = self.__get_repo(repo)
333 fork = self.__get_repo(fork)
333 fork = self.__get_repo(fork)
334 repo.fork = fork
334 repo.fork = fork
335 self.sa.add(repo)
335 self.sa.add(repo)
336 return repo
336 return repo
337
337
338 def pull_changes(self, repo_name, username):
338 def pull_changes(self, repo_name, username):
339 dbrepo = Repository.get_by_repo_name(repo_name)
339 dbrepo = Repository.get_by_repo_name(repo_name)
340 clone_uri = dbrepo.clone_uri
340 clone_uri = dbrepo.clone_uri
341 if not clone_uri:
341 if not clone_uri:
342 raise Exception("This repository doesn't have a clone uri")
342 raise Exception("This repository doesn't have a clone uri")
343
343
344 repo = dbrepo.scm_instance
344 repo = dbrepo.scm_instance
345 try:
345 try:
346 extras = {
346 extras = {
347 'ip': '',
347 'ip': '',
348 'username': username,
348 'username': username,
349 'action': 'push_remote',
349 'action': 'push_remote',
350 'repository': repo_name,
350 'repository': repo_name,
351 'scm': repo.alias,
351 'scm': repo.alias,
352 }
352 }
353
353
354 #inject ui extra param to log this action via push logger
354 # inject ui extra param to log this action via push logger
355 for k, v in extras.items():
355 for k, v in extras.items():
356 repo._repo.ui.setconfig('rhodecode_extras', k, v)
356 repo._repo.ui.setconfig('rhodecode_extras', k, v)
357
357
358 repo.pull(clone_uri)
358 repo.pull(clone_uri)
359 self.mark_for_invalidation(repo_name)
359 self.mark_for_invalidation(repo_name)
360 except:
360 except:
361 log.error(traceback.format_exc())
361 log.error(traceback.format_exc())
362 raise
362 raise
363
363
364 def commit_change(self, repo, repo_name, cs, user, author, message,
364 def commit_change(self, repo, repo_name, cs, user, author, message,
365 content, f_path):
365 content, f_path):
366
366
367 if repo.alias == 'hg':
367 if repo.alias == 'hg':
368 from rhodecode.lib.vcs.backends.hg import \
368 from rhodecode.lib.vcs.backends.hg import \
369 MercurialInMemoryChangeset as IMC
369 MercurialInMemoryChangeset as IMC
370 elif repo.alias == 'git':
370 elif repo.alias == 'git':
371 from rhodecode.lib.vcs.backends.git import \
371 from rhodecode.lib.vcs.backends.git import \
372 GitInMemoryChangeset as IMC
372 GitInMemoryChangeset as IMC
373
373
374 # decoding here will force that we have proper encoded values
374 # decoding here will force that we have proper encoded values
375 # in any other case this will throw exceptions and deny commit
375 # in any other case this will throw exceptions and deny commit
376 content = safe_str(content)
376 content = safe_str(content)
377 path = safe_str(f_path)
377 path = safe_str(f_path)
378 # message and author needs to be unicode
378 # message and author needs to be unicode
379 # proper backend should then translate that into required type
379 # proper backend should then translate that into required type
380 message = safe_unicode(message)
380 message = safe_unicode(message)
381 author = safe_unicode(author)
381 author = safe_unicode(author)
382 m = IMC(repo)
382 m = IMC(repo)
383 m.change(FileNode(path, content))
383 m.change(FileNode(path, content))
384 tip = m.commit(message=message,
384 tip = m.commit(message=message,
385 author=author,
385 author=author,
386 parents=[cs], branch=cs.branch)
386 parents=[cs], branch=cs.branch)
387
387
388 new_cs = tip.short_id
388 new_cs = tip.short_id
389 action = 'push_local:%s' % new_cs
389 action = 'push_local:%s' % new_cs
390
390
391 action_logger(user, action, repo_name)
391 action_logger(user, action, repo_name)
392
392
393 self.mark_for_invalidation(repo_name)
393 self.mark_for_invalidation(repo_name)
394
394
395 def create_node(self, repo, repo_name, cs, user, author, message, content,
395 def create_node(self, repo, repo_name, cs, user, author, message, content,
396 f_path):
396 f_path):
397 if repo.alias == 'hg':
397 if repo.alias == 'hg':
398 from rhodecode.lib.vcs.backends.hg import MercurialInMemoryChangeset as IMC
398 from rhodecode.lib.vcs.backends.hg import MercurialInMemoryChangeset as IMC
399 elif repo.alias == 'git':
399 elif repo.alias == 'git':
400 from rhodecode.lib.vcs.backends.git import GitInMemoryChangeset as IMC
400 from rhodecode.lib.vcs.backends.git import GitInMemoryChangeset as IMC
401 # decoding here will force that we have proper encoded values
401 # decoding here will force that we have proper encoded values
402 # in any other case this will throw exceptions and deny commit
402 # in any other case this will throw exceptions and deny commit
403
403
404 if isinstance(content, (basestring,)):
404 if isinstance(content, (basestring,)):
405 content = safe_str(content)
405 content = safe_str(content)
406 elif isinstance(content, (file, cStringIO.OutputType,)):
406 elif isinstance(content, (file, cStringIO.OutputType,)):
407 content = content.read()
407 content = content.read()
408 else:
408 else:
409 raise Exception('Content is of unrecognized type %s' % (
409 raise Exception('Content is of unrecognized type %s' % (
410 type(content)
410 type(content)
411 ))
411 ))
412
412
413 message = safe_unicode(message)
413 message = safe_unicode(message)
414 author = safe_unicode(author)
414 author = safe_unicode(author)
415 path = safe_str(f_path)
415 path = safe_str(f_path)
416 m = IMC(repo)
416 m = IMC(repo)
417
417
418 if isinstance(cs, EmptyChangeset):
418 if isinstance(cs, EmptyChangeset):
419 # EmptyChangeset means we we're editing empty repository
419 # EmptyChangeset means we we're editing empty repository
420 parents = None
420 parents = None
421 else:
421 else:
422 parents = [cs]
422 parents = [cs]
423
423
424 m.add(FileNode(path, content=content))
424 m.add(FileNode(path, content=content))
425 tip = m.commit(message=message,
425 tip = m.commit(message=message,
426 author=author,
426 author=author,
427 parents=parents, branch=cs.branch)
427 parents=parents, branch=cs.branch)
428 new_cs = tip.short_id
428 new_cs = tip.short_id
429 action = 'push_local:%s' % new_cs
429 action = 'push_local:%s' % new_cs
430
430
431 action_logger(user, action, repo_name)
431 action_logger(user, action, repo_name)
432
432
433 self.mark_for_invalidation(repo_name)
433 self.mark_for_invalidation(repo_name)
434
434
435 def get_nodes(self, repo_name, revision, root_path='/', flat=True):
435 def get_nodes(self, repo_name, revision, root_path='/', flat=True):
436 """
436 """
437 recursive walk in root dir and return a set of all path in that dir
437 recursive walk in root dir and return a set of all path in that dir
438 based on repository walk function
438 based on repository walk function
439
439
440 :param repo_name: name of repository
440 :param repo_name: name of repository
441 :param revision: revision for which to list nodes
441 :param revision: revision for which to list nodes
442 :param root_path: root path to list
442 :param root_path: root path to list
443 :param flat: return as a list, if False returns a dict with decription
443 :param flat: return as a list, if False returns a dict with decription
444
444
445 """
445 """
446 _files = list()
446 _files = list()
447 _dirs = list()
447 _dirs = list()
448 try:
448 try:
449 _repo = self.__get_repo(repo_name)
449 _repo = self.__get_repo(repo_name)
450 changeset = _repo.scm_instance.get_changeset(revision)
450 changeset = _repo.scm_instance.get_changeset(revision)
451 root_path = root_path.lstrip('/')
451 root_path = root_path.lstrip('/')
452 for topnode, dirs, files in changeset.walk(root_path):
452 for topnode, dirs, files in changeset.walk(root_path):
453 for f in files:
453 for f in files:
454 _files.append(f.path if flat else {"name": f.path,
454 _files.append(f.path if flat else {"name": f.path,
455 "type": "file"})
455 "type": "file"})
456 for d in dirs:
456 for d in dirs:
457 _dirs.append(d.path if flat else {"name": d.path,
457 _dirs.append(d.path if flat else {"name": d.path,
458 "type": "dir"})
458 "type": "dir"})
459 except RepositoryError:
459 except RepositoryError:
460 log.debug(traceback.format_exc())
460 log.debug(traceback.format_exc())
461 raise
461 raise
462
462
463 return _dirs, _files
463 return _dirs, _files
464
464
465 def get_unread_journal(self):
465 def get_unread_journal(self):
466 return self.sa.query(UserLog).count()
466 return self.sa.query(UserLog).count()
General Comments 0
You need to be logged in to leave comments. Login now