##// END OF EJS Templates
replace equality comparision to None
marcink -
r3889:b84c83b6 beta
parent child Browse files
Show More
@@ -1,244 +1,244 b''
1 1 # This program is free software: you can redistribute it and/or modify
2 2 # it under the terms of the GNU General Public License as published by
3 3 # the Free Software Foundation, either version 3 of the License, or
4 4 # (at your option) any later version.
5 5 #
6 6 # This program is distributed in the hope that it will be useful,
7 7 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 8 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 9 # GNU General Public License for more details.
10 10 #
11 11 # You should have received a copy of the GNU General Public License
12 12 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 13
14 14 import ldap
15 15 import urllib2
16 16 import uuid
17 17
18 18 try:
19 19 from rhodecode.lib.compat import json
20 20 except ImportError:
21 21 try:
22 22 import simplejson as json
23 23 except ImportError:
24 24 import json
25 25
26 26 from ConfigParser import ConfigParser
27 27
28 28 config = ConfigParser()
29 29 config.read('ldap_sync.conf')
30 30
31 31
32 32 class InvalidResponseIDError(Exception):
33 33 """ Request and response don't have the same UUID. """
34 34
35 35
36 36 class RhodecodeResponseError(Exception):
37 37 """ Response has an error, something went wrong with request execution. """
38 38
39 39
40 40 class UserAlreadyInGroupError(Exception):
41 41 """ User is already a member of the target group. """
42 42
43 43
44 44 class UserNotInGroupError(Exception):
45 45 """ User is not a member of the target group. """
46 46
47 47
48 48 class RhodecodeAPI():
49 49
50 50 def __init__(self, url, key):
51 51 self.url = url
52 52 self.key = key
53 53
54 54 def get_api_data(self, uid, method, args):
55 55 """Prepare dict for API post."""
56 56 return {
57 57 "id": uid,
58 58 "api_key": self.key,
59 59 "method": method,
60 60 "args": args
61 61 }
62 62
63 63 def rhodecode_api_post(self, method, args):
64 64 """Send a generic API post to Rhodecode.
65 65
66 66 This will generate the UUID for validation check after the
67 67 response is returned. Handle errors and get the result back.
68 68 """
69 69 uid = str(uuid.uuid1())
70 70 data = self.get_api_data(uid, method, args)
71 71
72 72 data = json.dumps(data)
73 73 headers = {'content-type': 'text/plain'}
74 74 req = urllib2.Request(self.url, data, headers)
75 75
76 76 response = urllib2.urlopen(req)
77 77 response = json.load(response)
78 78
79 79 if uid != response["id"]:
80 80 raise InvalidResponseIDError("UUID does not match.")
81 81
82 if response["error"] != None:
82 if response["error"] is not None:
83 83 raise RhodecodeResponseError(response["error"])
84 84
85 85 return response["result"]
86 86
87 87 def create_group(self, name, active=True):
88 88 """Create the Rhodecode user group."""
89 89 args = {
90 90 "group_name": name,
91 91 "active": str(active)
92 92 }
93 93 self.rhodecode_api_post("create_users_group", args)
94 94
95 95 def add_membership(self, group, username):
96 96 """Add specific user to a group."""
97 97 args = {
98 98 "usersgroupid": group,
99 99 "userid": username
100 100 }
101 101 result = self.rhodecode_api_post("add_user_to_users_group", args)
102 102 if not result["success"]:
103 103 raise UserAlreadyInGroupError("User %s already in group %s." %
104 104 (username, group))
105 105
106 106 def remove_membership(self, group, username):
107 107 """Remove specific user from a group."""
108 108 args = {
109 109 "usersgroupid": group,
110 110 "userid": username
111 111 }
112 112 result = self.rhodecode_api_post("remove_user_from_users_group", args)
113 113 if not result["success"]:
114 114 raise UserNotInGroupError("User %s not in group %s." %
115 115 (username, group))
116 116
117 117 def get_group_members(self, name):
118 118 """Get the list of member usernames from a user group."""
119 119 args = {"usersgroupid": name}
120 120 members = self.rhodecode_api_post("get_users_group", args)['members']
121 121 member_list = []
122 122 for member in members:
123 123 member_list.append(member["username"])
124 124 return member_list
125 125
126 126 def get_group(self, name):
127 127 """Return group info."""
128 128 args = {"usersgroupid": name}
129 129 return self.rhodecode_api_post("get_users_group", args)
130 130
131 131 def get_user(self, username):
132 132 """Return user info."""
133 133 args = {"userid": username}
134 134 return self.rhodecode_api_post("get_user", args)
135 135
136 136
137 137 class LdapClient():
138 138
139 139 def __init__(self, uri, user, key, base_dn):
140 140 self.client = ldap.initialize(uri, trace_level=0)
141 141 self.client.set_option(ldap.OPT_REFERRALS, 0)
142 142 self.client.simple_bind(user, key)
143 143 self.base_dn = base_dn
144 144
145 145 def __del__(self):
146 146 self.client.unbind()
147 147
148 148 def get_groups(self):
149 149 """Get all the groups in form of dict {group_name: group_info,...}."""
150 150 searchFilter = "objectClass=groupOfUniqueNames"
151 151 result = self.client.search_s(self.base_dn, ldap.SCOPE_SUBTREE,
152 152 searchFilter)
153 153
154 154 groups = {}
155 155 for group in result:
156 156 groups[group[1]['cn'][0]] = group[1]
157 157
158 158 return groups
159 159
160 160 def get_group_users(self, groups, group):
161 161 """Returns all the users belonging to a single group.
162 162
163 163 Based on the list of groups and memberships, returns all the
164 164 users belonging to a single group, searching recursively.
165 165 """
166 166 users = []
167 167 for member in groups[group]["uniqueMember"]:
168 168 member = self.parse_member_string(member)
169 169 if member[0] == "uid":
170 170 users.append(member[1])
171 171 elif member[0] == "cn":
172 172 users += self.get_group_users(groups, member[1])
173 173
174 174 return users
175 175
176 176 def parse_member_string(self, member):
177 177 """Parses the member string and returns a touple of type and name.
178 178
179 179 Unique member can be either user or group. Users will have 'uid' as
180 180 prefix while groups will have 'cn'.
181 181 """
182 182 member = member.split(",")[0]
183 183 return member.split('=')
184 184
185 185
186 186 class LdapSync(object):
187 187
188 188 def __init__(self):
189 189 self.ldap_client = LdapClient(config.get("default", "ldap_uri"),
190 190 config.get("default", "ldap_user"),
191 191 config.get("default", "ldap_key"),
192 192 config.get("default", "base_dn"))
193 193 self.rhodocode_api = RhodecodeAPI(config.get("default", "api_url"),
194 194 config.get("default", "api_key"))
195 195
196 196 def update_groups_from_ldap(self):
197 197 """Add all the groups from LDAP to Rhodecode."""
198 198 added = existing = 0
199 199 groups = self.ldap_client.get_groups()
200 200 for group in groups:
201 201 try:
202 202 self.rhodecode_api.create_group(group)
203 203 added += 1
204 204 except Exception:
205 205 existing += 1
206 206
207 207 return added, existing
208 208
209 209 def update_memberships_from_ldap(self, group):
210 210 """Update memberships in rhodecode based on the LDAP groups."""
211 211 groups = self.ldap_client.get_groups()
212 212 group_users = self.ldap_client.get_group_users(groups, group)
213 213
214 214 # Delete memberships first from each group which are not part
215 215 # of the group any more.
216 216 rhodecode_members = self.rhodecode_api.get_group_members(group)
217 217 for rhodecode_member in rhodecode_members:
218 218 if rhodecode_member not in group_users:
219 219 try:
220 220 self.rhodocode_api.remove_membership(group,
221 221 rhodecode_member)
222 222 except UserNotInGroupError:
223 223 pass
224 224
225 225 # Add memberships.
226 226 for member in group_users:
227 227 try:
228 228 self.rhodecode_api.add_membership(group, member)
229 229 except UserAlreadyInGroupError:
230 230 # TODO: handle somehow maybe..
231 231 pass
232 232
233 233
234 234 if __name__ == '__main__':
235 235 sync = LdapSync()
236 236 print sync.update_groups_from_ldap()
237 237
238 238 for gr in sync.ldap_client.get_groups():
239 239 # TODO: exception when user does not exist during add membership...
240 240 # How should we handle this.. Either sync users as well at this step,
241 241 # or just ignore those who don't exist. If we want the second case,
242 242 # we need to find a way to recognize the right exception (we always get
243 243 # RhodecodeResponseError with no error code so maybe by return msg (?)
244 244 sync.update_memberships_from_ldap(gr)
@@ -1,439 +1,439 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.changeset
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 changeset controller for pylons showoing changes beetween
7 7 revisions
8 8
9 9 :created_on: Apr 25, 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 import logging
27 27 import traceback
28 28 from collections import defaultdict
29 29 from webob.exc import HTTPForbidden, HTTPBadRequest, HTTPNotFound
30 30
31 31 from pylons import tmpl_context as c, url, request, response
32 32 from pylons.i18n.translation import _
33 33 from pylons.controllers.util import redirect
34 34 from rhodecode.lib.utils import jsonify
35 35
36 36 from rhodecode.lib.vcs.exceptions import RepositoryError, \
37 37 ChangesetDoesNotExistError
38 38
39 39 import rhodecode.lib.helpers as h
40 40 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator,\
41 41 NotAnonymous
42 42 from rhodecode.lib.base import BaseRepoController, render
43 43 from rhodecode.lib.utils import action_logger
44 44 from rhodecode.lib.compat import OrderedDict
45 45 from rhodecode.lib import diffs
46 46 from rhodecode.model.db import ChangesetComment, ChangesetStatus
47 47 from rhodecode.model.comment import ChangesetCommentsModel
48 48 from rhodecode.model.changeset_status import ChangesetStatusModel
49 49 from rhodecode.model.meta import Session
50 50 from rhodecode.model.repo import RepoModel
51 51 from rhodecode.lib.diffs import LimitedDiffContainer
52 52 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
53 53 from rhodecode.lib.vcs.backends.base import EmptyChangeset
54 54 from rhodecode.lib.utils2 import safe_unicode
55 55
56 56 log = logging.getLogger(__name__)
57 57
58 58
59 59 def _update_with_GET(params, GET):
60 60 for k in ['diff1', 'diff2', 'diff']:
61 61 params[k] += GET.getall(k)
62 62
63 63
64 64 def anchor_url(revision, path, GET):
65 65 fid = h.FID(revision, path)
66 66 return h.url.current(anchor=fid, **dict(GET))
67 67
68 68
69 69 def get_ignore_ws(fid, GET):
70 70 ig_ws_global = GET.get('ignorews')
71 71 ig_ws = filter(lambda k: k.startswith('WS'), GET.getall(fid))
72 72 if ig_ws:
73 73 try:
74 74 return int(ig_ws[0].split(':')[-1])
75 75 except Exception:
76 76 pass
77 77 return ig_ws_global
78 78
79 79
80 80 def _ignorews_url(GET, fileid=None):
81 81 fileid = str(fileid) if fileid else None
82 82 params = defaultdict(list)
83 83 _update_with_GET(params, GET)
84 84 lbl = _('Show white space')
85 85 ig_ws = get_ignore_ws(fileid, GET)
86 86 ln_ctx = get_line_ctx(fileid, GET)
87 87 # global option
88 88 if fileid is None:
89 89 if ig_ws is None:
90 90 params['ignorews'] += [1]
91 91 lbl = _('Ignore white space')
92 92 ctx_key = 'context'
93 93 ctx_val = ln_ctx
94 94 # per file options
95 95 else:
96 96 if ig_ws is None:
97 97 params[fileid] += ['WS:1']
98 98 lbl = _('Ignore white space')
99 99
100 100 ctx_key = fileid
101 101 ctx_val = 'C:%s' % ln_ctx
102 102 # if we have passed in ln_ctx pass it along to our params
103 103 if ln_ctx:
104 104 params[ctx_key] += [ctx_val]
105 105
106 106 params['anchor'] = fileid
107 107 img = h.image(h.url('/images/icons/text_strikethrough.png'), lbl, class_='icon')
108 108 return h.link_to(img, h.url.current(**params), title=lbl, class_='tooltip')
109 109
110 110
111 111 def get_line_ctx(fid, GET):
112 112 ln_ctx_global = GET.get('context')
113 113 if fid:
114 114 ln_ctx = filter(lambda k: k.startswith('C'), GET.getall(fid))
115 115 else:
116 116 _ln_ctx = filter(lambda k: k.startswith('C'), GET)
117 117 ln_ctx = GET.get(_ln_ctx[0]) if _ln_ctx else ln_ctx_global
118 118 if ln_ctx:
119 119 ln_ctx = [ln_ctx]
120 120
121 121 if ln_ctx:
122 122 retval = ln_ctx[0].split(':')[-1]
123 123 else:
124 124 retval = ln_ctx_global
125 125
126 126 try:
127 127 return int(retval)
128 128 except Exception:
129 129 return 3
130 130
131 131
132 132 def _context_url(GET, fileid=None):
133 133 """
134 134 Generates url for context lines
135 135
136 136 :param fileid:
137 137 """
138 138
139 139 fileid = str(fileid) if fileid else None
140 140 ig_ws = get_ignore_ws(fileid, GET)
141 141 ln_ctx = (get_line_ctx(fileid, GET) or 3) * 2
142 142
143 143 params = defaultdict(list)
144 144 _update_with_GET(params, GET)
145 145
146 146 # global option
147 147 if fileid is None:
148 148 if ln_ctx > 0:
149 149 params['context'] += [ln_ctx]
150 150
151 151 if ig_ws:
152 152 ig_ws_key = 'ignorews'
153 153 ig_ws_val = 1
154 154
155 155 # per file option
156 156 else:
157 157 params[fileid] += ['C:%s' % ln_ctx]
158 158 ig_ws_key = fileid
159 159 ig_ws_val = 'WS:%s' % 1
160 160
161 161 if ig_ws:
162 162 params[ig_ws_key] += [ig_ws_val]
163 163
164 164 lbl = _('%s line context') % ln_ctx
165 165
166 166 params['anchor'] = fileid
167 167 img = h.image(h.url('/images/icons/table_add.png'), lbl, class_='icon')
168 168 return h.link_to(img, h.url.current(**params), title=lbl, class_='tooltip')
169 169
170 170
171 171 class ChangesetController(BaseRepoController):
172 172
173 173 def __before__(self):
174 174 super(ChangesetController, self).__before__()
175 175 c.affected_files_cut_off = 60
176 176 repo_model = RepoModel()
177 177 c.users_array = repo_model.get_users_js()
178 178 c.users_groups_array = repo_model.get_users_groups_js()
179 179
180 180 def _index(self, revision, method):
181 181 c.anchor_url = anchor_url
182 182 c.ignorews_url = _ignorews_url
183 183 c.context_url = _context_url
184 184 c.fulldiff = fulldiff = request.GET.get('fulldiff')
185 185 #get ranges of revisions if preset
186 186 rev_range = revision.split('...')[:2]
187 187 enable_comments = True
188 188 try:
189 189 if len(rev_range) == 2:
190 190 enable_comments = False
191 191 rev_start = rev_range[0]
192 192 rev_end = rev_range[1]
193 193 rev_ranges = c.rhodecode_repo.get_changesets(start=rev_start,
194 194 end=rev_end)
195 195 else:
196 196 rev_ranges = [c.rhodecode_repo.get_changeset(revision)]
197 197
198 198 c.cs_ranges = list(rev_ranges)
199 199 if not c.cs_ranges:
200 200 raise RepositoryError('Changeset range returned empty result')
201 201
202 202 except (RepositoryError, ChangesetDoesNotExistError, Exception), e:
203 203 log.error(traceback.format_exc())
204 204 h.flash(str(e), category='error')
205 205 raise HTTPNotFound()
206 206
207 207 c.changes = OrderedDict()
208 208
209 209 c.lines_added = 0 # count of lines added
210 210 c.lines_deleted = 0 # count of lines removes
211 211
212 212 c.changeset_statuses = ChangesetStatus.STATUSES
213 213 c.comments = []
214 214 c.statuses = []
215 215 c.inline_comments = []
216 216 c.inline_cnt = 0
217 217
218 218 # Iterate over ranges (default changeset view is always one changeset)
219 219 for changeset in c.cs_ranges:
220 220 inlines = []
221 221 if method == 'show':
222 222 c.statuses.extend([ChangesetStatusModel().get_status(
223 223 c.rhodecode_db_repo.repo_id, changeset.raw_id)])
224 224
225 225 c.comments.extend(ChangesetCommentsModel()\
226 226 .get_comments(c.rhodecode_db_repo.repo_id,
227 227 revision=changeset.raw_id))
228 228
229 229 #comments from PR
230 230 st = ChangesetStatusModel().get_statuses(
231 231 c.rhodecode_db_repo.repo_id, changeset.raw_id,
232 232 with_revisions=True)
233 233 # from associated statuses, check the pull requests, and
234 234 # show comments from them
235 235
236 236 prs = set([x.pull_request for x in
237 filter(lambda x: x.pull_request != None, st)])
237 filter(lambda x: x.pull_request is not None, st)])
238 238
239 239 for pr in prs:
240 240 c.comments.extend(pr.comments)
241 241 inlines = ChangesetCommentsModel()\
242 242 .get_inline_comments(c.rhodecode_db_repo.repo_id,
243 243 revision=changeset.raw_id)
244 244 c.inline_comments.extend(inlines)
245 245
246 246 c.changes[changeset.raw_id] = []
247 247
248 248 cs2 = changeset.raw_id
249 249 cs1 = changeset.parents[0].raw_id if changeset.parents else EmptyChangeset()
250 250 context_lcl = get_line_ctx('', request.GET)
251 251 ign_whitespace_lcl = ign_whitespace_lcl = get_ignore_ws('', request.GET)
252 252
253 253 _diff = c.rhodecode_repo.get_diff(cs1, cs2,
254 254 ignore_whitespace=ign_whitespace_lcl, context=context_lcl)
255 255 diff_limit = self.cut_off_limit if not fulldiff else None
256 256 diff_processor = diffs.DiffProcessor(_diff,
257 257 vcs=c.rhodecode_repo.alias,
258 258 format='gitdiff',
259 259 diff_limit=diff_limit)
260 260 cs_changes = OrderedDict()
261 261 if method == 'show':
262 262 _parsed = diff_processor.prepare()
263 263 c.limited_diff = False
264 264 if isinstance(_parsed, LimitedDiffContainer):
265 265 c.limited_diff = True
266 266 for f in _parsed:
267 267 st = f['stats']
268 268 c.lines_added += st['added']
269 269 c.lines_deleted += st['deleted']
270 270 fid = h.FID(changeset.raw_id, f['filename'])
271 271 diff = diff_processor.as_html(enable_comments=enable_comments,
272 272 parsed_lines=[f])
273 273 cs_changes[fid] = [cs1, cs2, f['operation'], f['filename'],
274 274 diff, st]
275 275 else:
276 276 # downloads/raw we only need RAW diff nothing else
277 277 diff = diff_processor.as_raw()
278 278 cs_changes[''] = [None, None, None, None, diff, None]
279 279 c.changes[changeset.raw_id] = cs_changes
280 280
281 281 #sort comments by how they were generated
282 282 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
283 283
284 284 # count inline comments
285 285 for __, lines in c.inline_comments:
286 286 for comments in lines.values():
287 287 c.inline_cnt += len(comments)
288 288
289 289 if len(c.cs_ranges) == 1:
290 290 c.changeset = c.cs_ranges[0]
291 291 c.parent_tmpl = ''.join(['# Parent %s\n' % x.raw_id
292 292 for x in c.changeset.parents])
293 293 if method == 'download':
294 294 response.content_type = 'text/plain'
295 295 response.content_disposition = 'attachment; filename=%s.diff' \
296 296 % revision[:12]
297 297 return diff
298 298 elif method == 'patch':
299 299 response.content_type = 'text/plain'
300 300 c.diff = safe_unicode(diff)
301 301 return render('changeset/patch_changeset.html')
302 302 elif method == 'raw':
303 303 response.content_type = 'text/plain'
304 304 return diff
305 305 elif method == 'show':
306 306 if len(c.cs_ranges) == 1:
307 307 return render('changeset/changeset.html')
308 308 else:
309 309 return render('changeset/changeset_range.html')
310 310
311 311 @LoginRequired()
312 312 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
313 313 'repository.admin')
314 314 def index(self, revision, method='show'):
315 315 return self._index(revision, method=method)
316 316
317 317 @LoginRequired()
318 318 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
319 319 'repository.admin')
320 320 def changeset_raw(self, revision):
321 321 return self._index(revision, method='raw')
322 322
323 323 @LoginRequired()
324 324 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
325 325 'repository.admin')
326 326 def changeset_patch(self, revision):
327 327 return self._index(revision, method='patch')
328 328
329 329 @LoginRequired()
330 330 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
331 331 'repository.admin')
332 332 def changeset_download(self, revision):
333 333 return self._index(revision, method='download')
334 334
335 335 @LoginRequired()
336 336 @NotAnonymous()
337 337 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
338 338 'repository.admin')
339 339 @jsonify
340 340 def comment(self, repo_name, revision):
341 341 status = request.POST.get('changeset_status')
342 342 change_status = request.POST.get('change_changeset_status')
343 343 text = request.POST.get('text')
344 344 if status and change_status:
345 345 text = text or (_('Status change -> %s')
346 346 % ChangesetStatus.get_status_lbl(status))
347 347
348 348 c.co = comm = ChangesetCommentsModel().create(
349 349 text=text,
350 350 repo=c.rhodecode_db_repo.repo_id,
351 351 user=c.rhodecode_user.user_id,
352 352 revision=revision,
353 353 f_path=request.POST.get('f_path'),
354 354 line_no=request.POST.get('line'),
355 355 status_change=(ChangesetStatus.get_status_lbl(status)
356 356 if status and change_status else None)
357 357 )
358 358
359 359 # get status if set !
360 360 if status and change_status:
361 361 # if latest status was from pull request and it's closed
362 362 # disallow changing status !
363 363 # dont_allow_on_closed_pull_request = True !
364 364
365 365 try:
366 366 ChangesetStatusModel().set_status(
367 367 c.rhodecode_db_repo.repo_id,
368 368 status,
369 369 c.rhodecode_user.user_id,
370 370 comm,
371 371 revision=revision,
372 372 dont_allow_on_closed_pull_request=True
373 373 )
374 374 except StatusChangeOnClosedPullRequestError:
375 375 log.error(traceback.format_exc())
376 376 msg = _('Changing status on a changeset associated with '
377 377 'a closed pull request is not allowed')
378 378 h.flash(msg, category='warning')
379 379 return redirect(h.url('changeset_home', repo_name=repo_name,
380 380 revision=revision))
381 381 action_logger(self.rhodecode_user,
382 382 'user_commented_revision:%s' % revision,
383 383 c.rhodecode_db_repo, self.ip_addr, self.sa)
384 384
385 385 Session().commit()
386 386
387 387 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
388 388 return redirect(h.url('changeset_home', repo_name=repo_name,
389 389 revision=revision))
390 390 #only ajax below
391 391 data = {
392 392 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
393 393 }
394 394 if comm:
395 395 data.update(comm.get_dict())
396 396 data.update({'rendered_text':
397 397 render('changeset/changeset_comment_block.html')})
398 398
399 399 return data
400 400
401 401 @LoginRequired()
402 402 @NotAnonymous()
403 403 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
404 404 'repository.admin')
405 405 def preview_comment(self):
406 406 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
407 407 raise HTTPBadRequest()
408 408 text = request.POST.get('text')
409 409 if text:
410 410 return h.rst_w_mentions(text)
411 411 return ''
412 412
413 413 @LoginRequired()
414 414 @NotAnonymous()
415 415 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
416 416 'repository.admin')
417 417 @jsonify
418 418 def delete_comment(self, repo_name, comment_id):
419 419 co = ChangesetComment.get(comment_id)
420 420 owner = co.author.user_id == c.rhodecode_user.user_id
421 421 if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
422 422 ChangesetCommentsModel().delete(comment=co)
423 423 Session().commit()
424 424 return True
425 425 else:
426 426 raise HTTPForbidden()
427 427
428 428 @LoginRequired()
429 429 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
430 430 'repository.admin')
431 431 @jsonify
432 432 def changeset_info(self, repo_name, revision):
433 433 if request.is_xhr:
434 434 try:
435 435 return c.rhodecode_repo.get_changeset(revision)
436 436 except ChangesetDoesNotExistError, e:
437 437 return EmptyChangeset(message=str(e))
438 438 else:
439 439 raise HTTPBadRequest()
@@ -1,725 +1,725 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.db_manage
4 4 ~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Database creation, and setup module for RhodeCode. Used for creation
7 7 of database as well as for migration operations
8 8
9 9 :created_on: Apr 10, 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 sys
29 29 import uuid
30 30 import logging
31 31 from os.path import dirname as dn, join as jn
32 32
33 33 from rhodecode import __dbversion__, __py_version__
34 34
35 35 from rhodecode.model.user import UserModel
36 36 from rhodecode.lib.utils import ask_ok
37 37 from rhodecode.model import init_model
38 38 from rhodecode.model.db import User, Permission, RhodeCodeUi, \
39 39 RhodeCodeSetting, UserToPerm, DbMigrateVersion, RepoGroup, \
40 40 UserRepoGroupToPerm, CacheInvalidation, UserGroup
41 41
42 42 from sqlalchemy.engine import create_engine
43 43 from rhodecode.model.repos_group import ReposGroupModel
44 44 #from rhodecode.model import meta
45 45 from rhodecode.model.meta import Session, Base
46 46 from rhodecode.model.repo import RepoModel
47 47 from rhodecode.model.permission import PermissionModel
48 48 from rhodecode.model.users_group import UserGroupModel
49 49
50 50
51 51 log = logging.getLogger(__name__)
52 52
53 53
54 54 def notify(msg):
55 55 """
56 56 Notification for migrations messages
57 57 """
58 58 ml = len(msg) + (4 * 2)
59 59 print >> sys.stdout, ('*** %s ***\n%s' % (msg, '*' * ml)).upper()
60 60
61 61
62 62 class DbManage(object):
63 63 def __init__(self, log_sql, dbconf, root, tests=False, cli_args={}):
64 64 self.dbname = dbconf.split('/')[-1]
65 65 self.tests = tests
66 66 self.root = root
67 67 self.dburi = dbconf
68 68 self.log_sql = log_sql
69 69 self.db_exists = False
70 70 self.cli_args = cli_args
71 71 self.init_db()
72 72
73 73 force_ask = self.cli_args.get('force_ask')
74 74 if force_ask is not None:
75 75 global ask_ok
76 76 ask_ok = lambda *args, **kwargs: force_ask
77 77
78 78 def init_db(self):
79 79 engine = create_engine(self.dburi, echo=self.log_sql)
80 80 init_model(engine)
81 81 self.sa = Session()
82 82
83 83 def create_tables(self, override=False):
84 84 """
85 85 Create a auth database
86 86 """
87 87
88 88 log.info("Any existing database is going to be destroyed")
89 89 if self.tests:
90 90 destroy = True
91 91 else:
92 92 destroy = ask_ok('Are you sure to destroy old database ? [y/n]')
93 93 if not destroy:
94 94 sys.exit('Nothing tables created')
95 95 if destroy:
96 96 Base.metadata.drop_all()
97 97
98 98 checkfirst = not override
99 99 Base.metadata.create_all(checkfirst=checkfirst)
100 100 log.info('Created tables for %s' % self.dbname)
101 101
102 102 def set_db_version(self):
103 103 ver = DbMigrateVersion()
104 104 ver.version = __dbversion__
105 105 ver.repository_id = 'rhodecode_db_migrations'
106 106 ver.repository_path = 'versions'
107 107 self.sa.add(ver)
108 108 log.info('db version set to: %s' % __dbversion__)
109 109
110 110 def upgrade(self):
111 111 """
112 112 Upgrades given database schema to given revision following
113 113 all needed steps, to perform the upgrade
114 114
115 115 """
116 116
117 117 from rhodecode.lib.dbmigrate.migrate.versioning import api
118 118 from rhodecode.lib.dbmigrate.migrate.exceptions import \
119 119 DatabaseNotControlledError
120 120
121 121 if 'sqlite' in self.dburi:
122 122 print (
123 123 '********************** WARNING **********************\n'
124 124 'Make sure your version of sqlite is at least 3.7.X. \n'
125 125 'Earlier versions are known to fail on some migrations\n'
126 126 '*****************************************************\n'
127 127 )
128 128 upgrade = ask_ok('You are about to perform database upgrade, make '
129 129 'sure You backed up your database before. '
130 130 'Continue ? [y/n]')
131 131 if not upgrade:
132 132 sys.exit('No upgrade performed')
133 133
134 134 repository_path = jn(dn(dn(dn(os.path.realpath(__file__)))),
135 135 'rhodecode/lib/dbmigrate')
136 136 db_uri = self.dburi
137 137
138 138 try:
139 139 curr_version = api.db_version(db_uri, repository_path)
140 140 msg = ('Found current database under version'
141 141 ' control with version %s' % curr_version)
142 142
143 143 except (RuntimeError, DatabaseNotControlledError):
144 144 curr_version = 1
145 145 msg = ('Current database is not under version control. Setting'
146 146 ' as version %s' % curr_version)
147 147 api.version_control(db_uri, repository_path, curr_version)
148 148
149 149 notify(msg)
150 150
151 151 if curr_version == __dbversion__:
152 152 sys.exit('This database is already at the newest version')
153 153
154 154 # clear cache keys
155 155 log.info("Clearing cache keys now...")
156 156 CacheInvalidation.clear_cache()
157 157
158 158 #======================================================================
159 159 # UPGRADE STEPS
160 160 #======================================================================
161 161
162 162 class UpgradeSteps(object):
163 163 """
164 164 Those steps follow schema versions so for example schema
165 165 for example schema with seq 002 == step_2 and so on.
166 166 """
167 167
168 168 def __init__(self, klass):
169 169 self.klass = klass
170 170
171 171 def step_0(self):
172 172 # step 0 is the schema upgrade, and than follow proper upgrades
173 173 notify('attempting to do database upgrade from '
174 174 'version %s to version %s' %(curr_version, __dbversion__))
175 175 api.upgrade(db_uri, repository_path, __dbversion__)
176 176 notify('Schema upgrade completed')
177 177
178 178 def step_1(self):
179 179 pass
180 180
181 181 def step_2(self):
182 182 notify('Patching repo paths for newer version of RhodeCode')
183 183 self.klass.fix_repo_paths()
184 184
185 185 notify('Patching default user of RhodeCode')
186 186 self.klass.fix_default_user()
187 187
188 188 log.info('Changing ui settings')
189 189 self.klass.create_ui_settings()
190 190
191 191 def step_3(self):
192 192 notify('Adding additional settings into RhodeCode db')
193 193 self.klass.fix_settings()
194 194 notify('Adding ldap defaults')
195 195 self.klass.create_ldap_options(skip_existing=True)
196 196
197 197 def step_4(self):
198 198 notify('create permissions and fix groups')
199 199 self.klass.create_permissions()
200 200 self.klass.fixup_groups()
201 201
202 202 def step_5(self):
203 203 pass
204 204
205 205 def step_6(self):
206 206
207 207 notify('re-checking permissions')
208 208 self.klass.create_permissions()
209 209
210 210 notify('installing new UI options')
211 211 sett4 = RhodeCodeSetting('show_public_icon', True)
212 212 Session().add(sett4)
213 213 sett5 = RhodeCodeSetting('show_private_icon', True)
214 214 Session().add(sett5)
215 215 sett6 = RhodeCodeSetting('stylify_metatags', False)
216 216 Session().add(sett6)
217 217
218 218 notify('fixing old PULL hook')
219 219 _pull = RhodeCodeUi.get_by_key('preoutgoing.pull_logger')
220 220 if _pull:
221 221 _pull.ui_key = RhodeCodeUi.HOOK_PULL
222 222 Session().add(_pull)
223 223
224 224 notify('fixing old PUSH hook')
225 225 _push = RhodeCodeUi.get_by_key('pretxnchangegroup.push_logger')
226 226 if _push:
227 227 _push.ui_key = RhodeCodeUi.HOOK_PUSH
228 228 Session().add(_push)
229 229
230 230 notify('installing new pre-push hook')
231 231 hooks4 = RhodeCodeUi()
232 232 hooks4.ui_section = 'hooks'
233 233 hooks4.ui_key = RhodeCodeUi.HOOK_PRE_PUSH
234 234 hooks4.ui_value = 'python:rhodecode.lib.hooks.pre_push'
235 235 Session().add(hooks4)
236 236
237 237 notify('installing new pre-pull hook')
238 238 hooks6 = RhodeCodeUi()
239 239 hooks6.ui_section = 'hooks'
240 240 hooks6.ui_key = RhodeCodeUi.HOOK_PRE_PULL
241 241 hooks6.ui_value = 'python:rhodecode.lib.hooks.pre_pull'
242 242 Session().add(hooks6)
243 243
244 244 notify('installing hgsubversion option')
245 245 # enable hgsubversion disabled by default
246 246 hgsubversion = RhodeCodeUi()
247 247 hgsubversion.ui_section = 'extensions'
248 248 hgsubversion.ui_key = 'hgsubversion'
249 249 hgsubversion.ui_value = ''
250 250 hgsubversion.ui_active = False
251 251 Session().add(hgsubversion)
252 252
253 253 notify('installing hg git option')
254 254 # enable hggit disabled by default
255 255 hggit = RhodeCodeUi()
256 256 hggit.ui_section = 'extensions'
257 257 hggit.ui_key = 'hggit'
258 258 hggit.ui_value = ''
259 259 hggit.ui_active = False
260 260 Session().add(hggit)
261 261
262 262 notify('re-check default permissions')
263 263 default_user = User.get_by_username(User.DEFAULT_USER)
264 264 perm = Permission.get_by_key('hg.fork.repository')
265 265 reg_perm = UserToPerm()
266 266 reg_perm.user = default_user
267 267 reg_perm.permission = perm
268 268 Session().add(reg_perm)
269 269
270 270 def step_7(self):
271 271 perm_fixes = self.klass.reset_permissions(User.DEFAULT_USER)
272 272 Session().commit()
273 273 if perm_fixes:
274 274 notify('There was an inconsistent state of permissions '
275 275 'detected for default user. Permissions are now '
276 276 'reset to the default value for default user. '
277 277 'Please validate and check default permissions '
278 278 'in admin panel')
279 279
280 280 def step_8(self):
281 281 self.klass.create_permissions()
282 282 self.klass.populate_default_permissions()
283 283 self.klass.create_default_options(skip_existing=True)
284 284 Session().commit()
285 285
286 286 def step_9(self):
287 287 perm_fixes = self.klass.reset_permissions(User.DEFAULT_USER)
288 288 Session().commit()
289 289 if perm_fixes:
290 290 notify('There was an inconsistent state of permissions '
291 291 'detected for default user. Permissions are now '
292 292 'reset to the default value for default user. '
293 293 'Please validate and check default permissions '
294 294 'in admin panel')
295 295
296 296 def step_10(self):
297 297 pass
298 298
299 299 def step_11(self):
300 300 self.klass.update_repo_info()
301 301
302 302 def step_12(self):
303 303 self.klass.create_permissions()
304 304 Session().commit()
305 305
306 306 self.klass.populate_default_permissions()
307 307 Session().commit()
308 308
309 309 #fix all usergroups
310 310 ug_model = UserGroupModel()
311 311 for ug in UserGroup.get_all():
312 312 perm_obj = ug_model._create_default_perms(ug)
313 313 Session().add(perm_obj)
314 314 Session().commit()
315 315
316 316 upgrade_steps = [0] + range(curr_version + 1, __dbversion__ + 1)
317 317
318 318 # CALL THE PROPER ORDER OF STEPS TO PERFORM FULL UPGRADE
319 319 _step = None
320 320 for step in upgrade_steps:
321 321 notify('performing upgrade step %s' % step)
322 322 getattr(UpgradeSteps(self), 'step_%s' % step)()
323 323 self.sa.commit()
324 324 _step = step
325 325
326 326 notify('upgrade to version %s successful' % _step)
327 327
328 328 def fix_repo_paths(self):
329 329 """
330 330 Fixes a old rhodecode version path into new one without a '*'
331 331 """
332 332
333 333 paths = self.sa.query(RhodeCodeUi)\
334 334 .filter(RhodeCodeUi.ui_key == '/')\
335 335 .scalar()
336 336
337 337 paths.ui_value = paths.ui_value.replace('*', '')
338 338
339 339 try:
340 340 self.sa.add(paths)
341 341 self.sa.commit()
342 342 except Exception:
343 343 self.sa.rollback()
344 344 raise
345 345
346 346 def fix_default_user(self):
347 347 """
348 348 Fixes a old default user with some 'nicer' default values,
349 349 used mostly for anonymous access
350 350 """
351 351 def_user = self.sa.query(User)\
352 352 .filter(User.username == 'default')\
353 353 .one()
354 354
355 355 def_user.name = 'Anonymous'
356 356 def_user.lastname = 'User'
357 357 def_user.email = 'anonymous@rhodecode.org'
358 358
359 359 try:
360 360 self.sa.add(def_user)
361 361 self.sa.commit()
362 362 except Exception:
363 363 self.sa.rollback()
364 364 raise
365 365
366 366 def fix_settings(self):
367 367 """
368 368 Fixes rhodecode settings adds ga_code key for google analytics
369 369 """
370 370
371 371 hgsettings3 = RhodeCodeSetting('ga_code', '')
372 372
373 373 try:
374 374 self.sa.add(hgsettings3)
375 375 self.sa.commit()
376 376 except Exception:
377 377 self.sa.rollback()
378 378 raise
379 379
380 380 def admin_prompt(self, second=False):
381 381 if not self.tests:
382 382 import getpass
383 383
384 384 # defaults
385 385 defaults = self.cli_args
386 386 username = defaults.get('username')
387 387 password = defaults.get('password')
388 388 email = defaults.get('email')
389 389
390 390 def get_password():
391 391 password = getpass.getpass('Specify admin password '
392 392 '(min 6 chars):')
393 393 confirm = getpass.getpass('Confirm password:')
394 394
395 395 if password != confirm:
396 396 log.error('passwords mismatch')
397 397 return False
398 398 if len(password) < 6:
399 399 log.error('password is to short use at least 6 characters')
400 400 return False
401 401
402 402 return password
403 403 if username is None:
404 404 username = raw_input('Specify admin username:')
405 405 if password is None:
406 406 password = get_password()
407 407 if not password:
408 408 #second try
409 409 password = get_password()
410 410 if not password:
411 411 sys.exit()
412 412 if email is None:
413 413 email = raw_input('Specify admin email:')
414 414 self.create_user(username, password, email, True)
415 415 else:
416 416 log.info('creating admin and regular test users')
417 417 from rhodecode.tests import TEST_USER_ADMIN_LOGIN, \
418 418 TEST_USER_ADMIN_PASS, TEST_USER_ADMIN_EMAIL, \
419 419 TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS, \
420 420 TEST_USER_REGULAR_EMAIL, TEST_USER_REGULAR2_LOGIN, \
421 421 TEST_USER_REGULAR2_PASS, TEST_USER_REGULAR2_EMAIL
422 422
423 423 self.create_user(TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS,
424 424 TEST_USER_ADMIN_EMAIL, True)
425 425
426 426 self.create_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS,
427 427 TEST_USER_REGULAR_EMAIL, False)
428 428
429 429 self.create_user(TEST_USER_REGULAR2_LOGIN, TEST_USER_REGULAR2_PASS,
430 430 TEST_USER_REGULAR2_EMAIL, False)
431 431
432 432 def create_ui_settings(self):
433 433 """
434 434 Creates ui settings, fills out hooks
435 435 and disables dotencode
436 436 """
437 437
438 438 #HOOKS
439 439 hooks1_key = RhodeCodeUi.HOOK_UPDATE
440 440 hooks1_ = self.sa.query(RhodeCodeUi)\
441 441 .filter(RhodeCodeUi.ui_key == hooks1_key).scalar()
442 442
443 443 hooks1 = RhodeCodeUi() if hooks1_ is None else hooks1_
444 444 hooks1.ui_section = 'hooks'
445 445 hooks1.ui_key = hooks1_key
446 446 hooks1.ui_value = 'hg update >&2'
447 447 hooks1.ui_active = False
448 448 self.sa.add(hooks1)
449 449
450 450 hooks2_key = RhodeCodeUi.HOOK_REPO_SIZE
451 451 hooks2_ = self.sa.query(RhodeCodeUi)\
452 452 .filter(RhodeCodeUi.ui_key == hooks2_key).scalar()
453 453 hooks2 = RhodeCodeUi() if hooks2_ is None else hooks2_
454 454 hooks2.ui_section = 'hooks'
455 455 hooks2.ui_key = hooks2_key
456 456 hooks2.ui_value = 'python:rhodecode.lib.hooks.repo_size'
457 457 self.sa.add(hooks2)
458 458
459 459 hooks3 = RhodeCodeUi()
460 460 hooks3.ui_section = 'hooks'
461 461 hooks3.ui_key = RhodeCodeUi.HOOK_PUSH
462 462 hooks3.ui_value = 'python:rhodecode.lib.hooks.log_push_action'
463 463 self.sa.add(hooks3)
464 464
465 465 hooks4 = RhodeCodeUi()
466 466 hooks4.ui_section = 'hooks'
467 467 hooks4.ui_key = RhodeCodeUi.HOOK_PRE_PUSH
468 468 hooks4.ui_value = 'python:rhodecode.lib.hooks.pre_push'
469 469 self.sa.add(hooks4)
470 470
471 471 hooks5 = RhodeCodeUi()
472 472 hooks5.ui_section = 'hooks'
473 473 hooks5.ui_key = RhodeCodeUi.HOOK_PULL
474 474 hooks5.ui_value = 'python:rhodecode.lib.hooks.log_pull_action'
475 475 self.sa.add(hooks5)
476 476
477 477 hooks6 = RhodeCodeUi()
478 478 hooks6.ui_section = 'hooks'
479 479 hooks6.ui_key = RhodeCodeUi.HOOK_PRE_PULL
480 480 hooks6.ui_value = 'python:rhodecode.lib.hooks.pre_pull'
481 481 self.sa.add(hooks6)
482 482
483 483 # enable largefiles
484 484 largefiles = RhodeCodeUi()
485 485 largefiles.ui_section = 'extensions'
486 486 largefiles.ui_key = 'largefiles'
487 487 largefiles.ui_value = ''
488 488 self.sa.add(largefiles)
489 489
490 490 # enable hgsubversion disabled by default
491 491 hgsubversion = RhodeCodeUi()
492 492 hgsubversion.ui_section = 'extensions'
493 493 hgsubversion.ui_key = 'hgsubversion'
494 494 hgsubversion.ui_value = ''
495 495 hgsubversion.ui_active = False
496 496 self.sa.add(hgsubversion)
497 497
498 498 # enable hggit disabled by default
499 499 hggit = RhodeCodeUi()
500 500 hggit.ui_section = 'extensions'
501 501 hggit.ui_key = 'hggit'
502 502 hggit.ui_value = ''
503 503 hggit.ui_active = False
504 504 self.sa.add(hggit)
505 505
506 506 def create_ldap_options(self, skip_existing=False):
507 507 """Creates ldap settings"""
508 508
509 509 for k, v in [('ldap_active', 'false'), ('ldap_host', ''),
510 510 ('ldap_port', '389'), ('ldap_tls_kind', 'PLAIN'),
511 511 ('ldap_tls_reqcert', ''), ('ldap_dn_user', ''),
512 512 ('ldap_dn_pass', ''), ('ldap_base_dn', ''),
513 513 ('ldap_filter', ''), ('ldap_search_scope', ''),
514 514 ('ldap_attr_login', ''), ('ldap_attr_firstname', ''),
515 515 ('ldap_attr_lastname', ''), ('ldap_attr_email', '')]:
516 516
517 if skip_existing and RhodeCodeSetting.get_by_name(k) != None:
517 if skip_existing and RhodeCodeSetting.get_by_name(k) is not None:
518 518 log.debug('Skipping option %s' % k)
519 519 continue
520 520 setting = RhodeCodeSetting(k, v)
521 521 self.sa.add(setting)
522 522
523 523 def create_default_options(self, skip_existing=False):
524 524 """Creates default settings"""
525 525
526 526 for k, v in [
527 527 ('default_repo_enable_locking', False),
528 528 ('default_repo_enable_downloads', False),
529 529 ('default_repo_enable_statistics', False),
530 530 ('default_repo_private', False),
531 531 ('default_repo_type', 'hg')]:
532 532
533 if skip_existing and RhodeCodeSetting.get_by_name(k) != None:
533 if skip_existing and RhodeCodeSetting.get_by_name(k) is not None:
534 534 log.debug('Skipping option %s' % k)
535 535 continue
536 536 setting = RhodeCodeSetting(k, v)
537 537 self.sa.add(setting)
538 538
539 539 def fixup_groups(self):
540 540 def_usr = User.get_default_user()
541 541 for g in RepoGroup.query().all():
542 542 g.group_name = g.get_new_name(g.name)
543 543 self.sa.add(g)
544 544 # get default perm
545 545 default = UserRepoGroupToPerm.query()\
546 546 .filter(UserRepoGroupToPerm.group == g)\
547 547 .filter(UserRepoGroupToPerm.user == def_usr)\
548 548 .scalar()
549 549
550 550 if default is None:
551 551 log.debug('missing default permission for group %s adding' % g)
552 552 perm_obj = ReposGroupModel()._create_default_perms(g)
553 553 self.sa.add(perm_obj)
554 554
555 555 def reset_permissions(self, username):
556 556 """
557 557 Resets permissions to default state, usefull when old systems had
558 558 bad permissions, we must clean them up
559 559
560 560 :param username:
561 561 """
562 562 default_user = User.get_by_username(username)
563 563 if not default_user:
564 564 return
565 565
566 566 u2p = UserToPerm.query()\
567 567 .filter(UserToPerm.user == default_user).all()
568 568 fixed = False
569 569 if len(u2p) != len(Permission.DEFAULT_USER_PERMISSIONS):
570 570 for p in u2p:
571 571 Session().delete(p)
572 572 fixed = True
573 573 self.populate_default_permissions()
574 574 return fixed
575 575
576 576 def update_repo_info(self):
577 577 RepoModel.update_repoinfo()
578 578
579 579 def config_prompt(self, test_repo_path='', retries=3):
580 580 defaults = self.cli_args
581 581 _path = defaults.get('repos_location')
582 582 if retries == 3:
583 583 log.info('Setting up repositories config')
584 584
585 585 if _path is not None:
586 586 path = _path
587 587 elif not self.tests and not test_repo_path:
588 588 path = raw_input(
589 589 'Enter a valid absolute path to store repositories. '
590 590 'All repositories in that path will be added automatically:'
591 591 )
592 592 else:
593 593 path = test_repo_path
594 594 path_ok = True
595 595
596 596 # check proper dir
597 597 if not os.path.isdir(path):
598 598 path_ok = False
599 599 log.error('Given path %s is not a valid directory' % path)
600 600
601 601 elif not os.path.isabs(path):
602 602 path_ok = False
603 603 log.error('Given path %s is not an absolute path' % path)
604 604
605 605 # check write access
606 606 elif not os.access(path, os.W_OK) and path_ok:
607 607 path_ok = False
608 608 log.error('No write permission to given path %s' % path)
609 609
610 610 if retries == 0:
611 611 sys.exit('max retries reached')
612 612 if not path_ok:
613 613 retries -= 1
614 614 return self.config_prompt(test_repo_path, retries)
615 615
616 616 real_path = os.path.normpath(os.path.realpath(path))
617 617
618 618 if real_path != os.path.normpath(path):
619 619 if not ask_ok(('Path looks like a symlink, Rhodecode will store '
620 620 'given path as %s ? [y/n]') % (real_path)):
621 621 log.error('Canceled by user')
622 622 sys.exit(-1)
623 623
624 624 return real_path
625 625
626 626 def create_settings(self, path):
627 627
628 628 self.create_ui_settings()
629 629
630 630 #HG UI OPTIONS
631 631 web1 = RhodeCodeUi()
632 632 web1.ui_section = 'web'
633 633 web1.ui_key = 'push_ssl'
634 634 web1.ui_value = 'false'
635 635
636 636 web2 = RhodeCodeUi()
637 637 web2.ui_section = 'web'
638 638 web2.ui_key = 'allow_archive'
639 639 web2.ui_value = 'gz zip bz2'
640 640
641 641 web3 = RhodeCodeUi()
642 642 web3.ui_section = 'web'
643 643 web3.ui_key = 'allow_push'
644 644 web3.ui_value = '*'
645 645
646 646 web4 = RhodeCodeUi()
647 647 web4.ui_section = 'web'
648 648 web4.ui_key = 'baseurl'
649 649 web4.ui_value = '/'
650 650
651 651 paths = RhodeCodeUi()
652 652 paths.ui_section = 'paths'
653 653 paths.ui_key = '/'
654 654 paths.ui_value = path
655 655
656 656 phases = RhodeCodeUi()
657 657 phases.ui_section = 'phases'
658 658 phases.ui_key = 'publish'
659 659 phases.ui_value = False
660 660
661 661 sett1 = RhodeCodeSetting('realm', 'RhodeCode authentication')
662 662 sett2 = RhodeCodeSetting('title', 'RhodeCode')
663 663 sett3 = RhodeCodeSetting('ga_code', '')
664 664
665 665 sett4 = RhodeCodeSetting('show_public_icon', True)
666 666 sett5 = RhodeCodeSetting('show_private_icon', True)
667 667 sett6 = RhodeCodeSetting('stylify_metatags', False)
668 668
669 669 self.sa.add(web1)
670 670 self.sa.add(web2)
671 671 self.sa.add(web3)
672 672 self.sa.add(web4)
673 673 self.sa.add(paths)
674 674 self.sa.add(sett1)
675 675 self.sa.add(sett2)
676 676 self.sa.add(sett3)
677 677 self.sa.add(sett4)
678 678 self.sa.add(sett5)
679 679 self.sa.add(sett6)
680 680
681 681 self.create_ldap_options()
682 682 self.create_default_options()
683 683
684 684 log.info('created ui config')
685 685
686 686 def create_user(self, username, password, email='', admin=False):
687 687 log.info('creating user %s' % username)
688 688 UserModel().create_or_update(username, password, email,
689 689 firstname='RhodeCode', lastname='Admin',
690 690 active=True, admin=admin)
691 691
692 692 def create_default_user(self):
693 693 log.info('creating default user')
694 694 # create default user for handling default permissions.
695 695 UserModel().create_or_update(username='default',
696 696 password=str(uuid.uuid1())[:8],
697 697 email='anonymous@rhodecode.org',
698 698 firstname='Anonymous', lastname='User')
699 699
700 700 def create_permissions(self):
701 701 """
702 702 Creates all permissions defined in the system
703 703 """
704 704 # module.(access|create|change|delete)_[name]
705 705 # module.(none|read|write|admin)
706 706 log.info('creating permissions')
707 707 PermissionModel(self.sa).create_permissions()
708 708
709 709 def populate_default_permissions(self):
710 710 """
711 711 Populate default permissions. It will create only the default
712 712 permissions that are missing, and not alter already defined ones
713 713 """
714 714 log.info('creating default user permissions')
715 715 PermissionModel(self.sa).create_default_permissions(user=User.DEFAULT_USER)
716 716
717 717 @staticmethod
718 718 def check_waitress():
719 719 """
720 720 Function executed at the end of setup
721 721 """
722 722 if not __py_version__ >= (2, 6):
723 723 notify('Python2.5 detected, please switch '
724 724 'egg:waitress#main -> egg:Paste#http '
725 725 'in your .ini file')
@@ -1,336 +1,336 b''
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 path_info, isgit_path != None)
100 path_info, isgit_path is not 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 Exception:
126 126 return HTTPInternalServerError()(environ, start_response)
127 127
128 128 # quick check if that dir exists...
129 129 if not is_valid_repo(repo_name, self.basepath, 'git'):
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 not anonymous_perm or not anonymous_user.active:
147 147 if not anonymous_perm:
148 148 log.debug('Not enough credentials to access this '
149 149 'repository as anonymous user')
150 150 if not anonymous_user.active:
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 Exception:
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 not perm:
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 str_repo_name = safe_str(repo_name)
210 210 repo_path = os.path.join(safe_str(self.basepath),str_repo_name)
211 211 log.debug('Repository path is %s' % repo_path)
212 212
213 213 # CHECK LOCKING only if it's not ANONYMOUS USER
214 214 if username != User.DEFAULT_USER:
215 215 log.debug('Checking locking on repository')
216 216 (make_lock,
217 217 locked,
218 218 locked_by) = self._check_locking_state(
219 219 environ=environ, action=action,
220 220 repo=repo_name, user_id=user.user_id
221 221 )
222 222 # store the make_lock for later evaluation in hooks
223 223 extras.update({'make_lock': make_lock,
224 224 'locked_by': locked_by})
225 225
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, str_repo_name, safe_str(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 Exception:
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 320
321 321 _hooks = dict(baseui.configitems('hooks')) or {}
322 322 if action == 'pull':
323 323 # stupid git, emulate pre-pull hook !
324 324 pre_pull(ui=baseui, repo=_repo._repo)
325 325 if action == 'pull' and _hooks.get(RhodeCodeUi.HOOK_PULL):
326 326 log_pull_action(ui=baseui, repo=_repo._repo)
327 327
328 328 def __inject_extras(self, repo_path, baseui, extras={}):
329 329 """
330 330 Injects some extra params into baseui instance
331 331
332 332 :param baseui: baseui instance
333 333 :param extras: dict with extra params to put into baseui
334 334 """
335 335
336 336 _set_extras(extras)
@@ -1,453 +1,453 b''
1 1 # The code in this module is entirely lifted from the Lamson project
2 2 # (http://lamsonproject.org/). Its copyright is:
3 3
4 4 # Copyright (c) 2008, Zed A. Shaw
5 5 # All rights reserved.
6 6
7 7 # It is provided under this license:
8 8
9 9 # Redistribution and use in source and binary forms, with or without
10 10 # modification, are permitted provided that the following conditions are met:
11 11
12 12 # * Redistributions of source code must retain the above copyright notice, this
13 13 # list of conditions and the following disclaimer.
14 14
15 15 # * Redistributions in binary form must reproduce the above copyright notice,
16 16 # this list of conditions and the following disclaimer in the documentation
17 17 # and/or other materials provided with the distribution.
18 18
19 19 # * Neither the name of the Zed A. Shaw nor the names of its contributors may
20 20 # be used to endorse or promote products derived from this software without
21 21 # specific prior written permission.
22 22
23 23 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
24 24 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
25 25 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
26 26 # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
27 27 # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
28 28 # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
29 29 # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
30 30 # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
31 31 # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
32 32 # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
33 33 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
34 34 # POSSIBILITY OF SUCH DAMAGE.
35 35
36 36 import os
37 37 import mimetypes
38 38 import string
39 39 from email import encoders
40 40 from email.charset import Charset
41 41 from email.utils import parseaddr
42 42 from email.mime.base import MIMEBase
43 43
44 44 ADDRESS_HEADERS_WHITELIST = ['From', 'To', 'Delivered-To', 'Cc']
45 45 DEFAULT_ENCODING = "utf-8"
46 46 VALUE_IS_EMAIL_ADDRESS = lambda v: '@' in v
47 47
48 48
49 49 def normalize_header(header):
50 50 return string.capwords(header.lower(), '-')
51 51
52 52
53 53 class EncodingError(Exception):
54 54 """Thrown when there is an encoding error."""
55 55 pass
56 56
57 57
58 58 class MailBase(object):
59 59 """MailBase is used as the basis of lamson.mail and contains the basics of
60 60 encoding an email. You actually can do all your email processing with this
61 61 class, but it's more raw.
62 62 """
63 63 def __init__(self, items=()):
64 64 self.headers = dict(items)
65 65 self.parts = []
66 66 self.body = None
67 67 self.content_encoding = {'Content-Type': (None, {}),
68 68 'Content-Disposition': (None, {}),
69 69 'Content-Transfer-Encoding': (None, {})}
70 70
71 71 def __getitem__(self, key):
72 72 return self.headers.get(normalize_header(key), None)
73 73
74 74 def __len__(self):
75 75 return len(self.headers)
76 76
77 77 def __iter__(self):
78 78 return iter(self.headers)
79 79
80 80 def __contains__(self, key):
81 81 return normalize_header(key) in self.headers
82 82
83 83 def __setitem__(self, key, value):
84 84 self.headers[normalize_header(key)] = value
85 85
86 86 def __delitem__(self, key):
87 87 del self.headers[normalize_header(key)]
88 88
89 89 def __nonzero__(self):
90 return self.body != None or len(self.headers) > 0 or len(self.parts) > 0
90 return self.body is not None or len(self.headers) > 0 or len(self.parts) > 0
91 91
92 92 def keys(self):
93 93 """Returns the sorted keys."""
94 94 return sorted(self.headers.keys())
95 95
96 96 def attach_file(self, filename, data, ctype, disposition):
97 97 """
98 98 A file attachment is a raw attachment with a disposition that
99 99 indicates the file name.
100 100 """
101 101 assert filename, "You can't attach a file without a filename."
102 102 ctype = ctype.lower()
103 103
104 104 part = MailBase()
105 105 part.body = data
106 106 part.content_encoding['Content-Type'] = (ctype, {'name': filename})
107 107 part.content_encoding['Content-Disposition'] = (disposition,
108 108 {'filename': filename})
109 109 self.parts.append(part)
110 110
111 111 def attach_text(self, data, ctype):
112 112 """
113 113 This attaches a simpler text encoded part, which doesn't have a
114 114 filename.
115 115 """
116 116 ctype = ctype.lower()
117 117
118 118 part = MailBase()
119 119 part.body = data
120 120 part.content_encoding['Content-Type'] = (ctype, {})
121 121 self.parts.append(part)
122 122
123 123 def walk(self):
124 124 for p in self.parts:
125 125 yield p
126 126 for x in p.walk():
127 127 yield x
128 128
129 129
130 130 class MailResponse(object):
131 131 """
132 132 You are given MailResponse objects from the lamson.view methods, and
133 133 whenever you want to generate an email to send to someone. It has the
134 134 same basic functionality as MailRequest, but it is designed to be written
135 135 to, rather than read from (although you can do both).
136 136
137 137 You can easily set a Body or Html during creation or after by passing it
138 138 as __init__ parameters, or by setting those attributes.
139 139
140 140 You can initially set the From, To, and Subject, but they are headers so
141 141 use the dict notation to change them: msg['From'] = 'joe@test.com'.
142 142
143 143 The message is not fully crafted until right when you convert it with
144 144 MailResponse.to_message. This lets you change it and work with it, then
145 145 send it out when it's ready.
146 146 """
147 147 def __init__(self, To=None, From=None, Subject=None, Body=None, Html=None,
148 148 separator="; "):
149 149 self.Body = Body
150 150 self.Html = Html
151 151 self.base = MailBase([('To', To), ('From', From), ('Subject', Subject)])
152 152 self.multipart = self.Body and self.Html
153 153 self.attachments = []
154 154 self.separator = separator
155 155
156 156 def __contains__(self, key):
157 157 return self.base.__contains__(key)
158 158
159 159 def __getitem__(self, key):
160 160 return self.base.__getitem__(key)
161 161
162 162 def __setitem__(self, key, val):
163 163 return self.base.__setitem__(key, val)
164 164
165 165 def __delitem__(self, name):
166 166 del self.base[name]
167 167
168 168 def attach(self, filename=None, content_type=None, data=None,
169 169 disposition=None):
170 170 """
171 171
172 172 Simplifies attaching files from disk or data as files. To attach
173 173 simple text simple give data and a content_type. To attach a file,
174 174 give the data/content_type/filename/disposition combination.
175 175
176 176 For convenience, if you don't give data and only a filename, then it
177 177 will read that file's contents when you call to_message() later. If
178 178 you give data and filename then it will assume you've filled data
179 179 with what the file's contents are and filename is just the name to
180 180 use.
181 181 """
182 182
183 183 assert filename or data, ("You must give a filename or some data to "
184 184 "attach.")
185 185 assert data or os.path.exists(filename), ("File doesn't exist, and no "
186 186 "data given.")
187 187
188 188 self.multipart = True
189 189
190 190 if filename and not content_type:
191 191 content_type, encoding = mimetypes.guess_type(filename)
192 192
193 193 assert content_type, ("No content type given, and couldn't guess "
194 194 "from the filename: %r" % filename)
195 195
196 196 self.attachments.append({'filename': filename,
197 197 'content_type': content_type,
198 198 'data': data,
199 199 'disposition': disposition,})
200 200
201 201 def attach_part(self, part):
202 202 """
203 203 Attaches a raw MailBase part from a MailRequest (or anywhere)
204 204 so that you can copy it over.
205 205 """
206 206 self.multipart = True
207 207
208 208 self.attachments.append({'filename': None,
209 209 'content_type': None,
210 210 'data': None,
211 211 'disposition': None,
212 212 'part': part,
213 213 })
214 214
215 215 def attach_all_parts(self, mail_request):
216 216 """
217 217 Used for copying the attachment parts of a mail.MailRequest
218 218 object for mailing lists that need to maintain attachments.
219 219 """
220 220 for part in mail_request.all_parts():
221 221 self.attach_part(part)
222 222
223 223 self.base.content_encoding = mail_request.base.content_encoding.copy()
224 224
225 225 def clear(self):
226 226 """
227 227 Clears out the attachments so you can redo them. Use this to keep the
228 228 headers for a series of different messages with different attachments.
229 229 """
230 230 del self.attachments[:]
231 231 del self.base.parts[:]
232 232 self.multipart = False
233 233
234 234 def update(self, message):
235 235 """
236 236 Used to easily set a bunch of heading from another dict
237 237 like object.
238 238 """
239 239 for k in message.keys():
240 240 self.base[k] = message[k]
241 241
242 242 def __str__(self):
243 243 """
244 244 Converts to a string.
245 245 """
246 246 return self.to_message().as_string()
247 247
248 248 def _encode_attachment(self, filename=None, content_type=None, data=None,
249 249 disposition=None, part=None):
250 250 """
251 251 Used internally to take the attachments mentioned in self.attachments
252 252 and do the actual encoding in a lazy way when you call to_message.
253 253 """
254 254 if part:
255 255 self.base.parts.append(part)
256 256 elif filename:
257 257 if not data:
258 258 data = open(filename).read()
259 259
260 260 self.base.attach_file(filename, data, content_type,
261 261 disposition or 'attachment')
262 262 else:
263 263 self.base.attach_text(data, content_type)
264 264
265 265 ctype = self.base.content_encoding['Content-Type'][0]
266 266
267 267 if ctype and not ctype.startswith('multipart'):
268 268 self.base.content_encoding['Content-Type'] = ('multipart/mixed', {})
269 269
270 270 def to_message(self):
271 271 """
272 272 Figures out all the required steps to finally craft the
273 273 message you need and return it. The resulting message
274 274 is also available as a self.base attribute.
275 275
276 276 What is returned is a Python email API message you can
277 277 use with those APIs. The self.base attribute is the raw
278 278 lamson.encoding.MailBase.
279 279 """
280 280 del self.base.parts[:]
281 281
282 282 if self.Body and self.Html:
283 283 self.multipart = True
284 284 self.base.content_encoding['Content-Type'] = (
285 285 'multipart/alternative', {})
286 286
287 287 if self.multipart:
288 288 self.base.body = None
289 289 if self.Body:
290 290 self.base.attach_text(self.Body, 'text/plain')
291 291
292 292 if self.Html:
293 293 self.base.attach_text(self.Html, 'text/html')
294 294
295 295 for args in self.attachments:
296 296 self._encode_attachment(**args)
297 297
298 298 elif self.Body:
299 299 self.base.body = self.Body
300 300 self.base.content_encoding['Content-Type'] = ('text/plain', {})
301 301
302 302 elif self.Html:
303 303 self.base.body = self.Html
304 304 self.base.content_encoding['Content-Type'] = ('text/html', {})
305 305
306 306 return to_message(self.base, separator=self.separator)
307 307
308 308 def all_parts(self):
309 309 """
310 310 Returns all the encoded parts. Only useful for debugging
311 311 or inspecting after calling to_message().
312 312 """
313 313 return self.base.parts
314 314
315 315 def keys(self):
316 316 return self.base.keys()
317 317
318 318
319 319 def to_message(mail, separator="; "):
320 320 """
321 321 Given a MailBase message, this will construct a MIMEPart
322 322 that is canonicalized for use with the Python email API.
323 323 """
324 324 ctype, params = mail.content_encoding['Content-Type']
325 325
326 326 if not ctype:
327 327 if mail.parts:
328 328 ctype = 'multipart/mixed'
329 329 else:
330 330 ctype = 'text/plain'
331 331 else:
332 332 if mail.parts:
333 333 assert ctype.startswith(("multipart", "message")), \
334 334 "Content type should be multipart or message, not %r" % ctype
335 335
336 336 # adjust the content type according to what it should be now
337 337 mail.content_encoding['Content-Type'] = (ctype, params)
338 338
339 339 try:
340 340 out = MIMEPart(ctype, **params)
341 341 except TypeError, exc: # pragma: no cover
342 342 raise EncodingError("Content-Type malformed, not allowed: %r; "
343 343 "%r (Python ERROR: %s" %
344 344 (ctype, params, exc.message))
345 345
346 346 for k in mail.keys():
347 347 if k in ADDRESS_HEADERS_WHITELIST:
348 348 out[k.encode('ascii')] = header_to_mime_encoding(
349 349 mail[k],
350 350 not_email=False,
351 351 separator=separator
352 352 )
353 353 else:
354 354 out[k.encode('ascii')] = header_to_mime_encoding(
355 355 mail[k],
356 356 not_email=True
357 357 )
358 358
359 359 out.extract_payload(mail)
360 360
361 361 # go through the children
362 362 for part in mail.parts:
363 363 out.attach(to_message(part))
364 364
365 365 return out
366 366
367 367
368 368 class MIMEPart(MIMEBase):
369 369 """
370 370 A reimplementation of nearly everything in email.mime to be more useful
371 371 for actually attaching things. Rather than one class for every type of
372 372 thing you'd encode, there's just this one, and it figures out how to
373 373 encode what you ask it.
374 374 """
375 375 def __init__(self, type, **params):
376 376 self.maintype, self.subtype = type.split('/')
377 377 MIMEBase.__init__(self, self.maintype, self.subtype, **params)
378 378
379 379 def add_text(self, content):
380 380 # this is text, so encode it in canonical form
381 381 try:
382 382 encoded = content.encode('ascii')
383 383 charset = 'ascii'
384 384 except UnicodeError:
385 385 encoded = content.encode('utf-8')
386 386 charset = 'utf-8'
387 387
388 388 self.set_payload(encoded, charset=charset)
389 389
390 390 def extract_payload(self, mail):
391 if mail.body == None:
391 if mail.body is None:
392 392 return # only None, '' is still ok
393 393
394 394 ctype, ctype_params = mail.content_encoding['Content-Type']
395 395 cdisp, cdisp_params = mail.content_encoding['Content-Disposition']
396 396
397 397 assert ctype, ("Extract payload requires that mail.content_encoding "
398 398 "have a valid Content-Type.")
399 399
400 400 if ctype.startswith("text/"):
401 401 self.add_text(mail.body)
402 402 else:
403 403 if cdisp:
404 404 # replicate the content-disposition settings
405 405 self.add_header('Content-Disposition', cdisp, **cdisp_params)
406 406
407 407 self.set_payload(mail.body)
408 408 encoders.encode_base64(self)
409 409
410 410 def __repr__(self):
411 411 return "<MIMEPart '%s/%s': %r, %r, multipart=%r>" % (
412 412 self.subtype,
413 413 self.maintype,
414 414 self['Content-Type'],
415 415 self['Content-Disposition'],
416 416 self.is_multipart())
417 417
418 418
419 419 def header_to_mime_encoding(value, not_email=False, separator=", "):
420 420 if not value:
421 421 return ""
422 422
423 423 encoder = Charset(DEFAULT_ENCODING)
424 424 if type(value) == list:
425 425 return separator.join(properly_encode_header(
426 426 v, encoder, not_email) for v in value)
427 427 else:
428 428 return properly_encode_header(value, encoder, not_email)
429 429
430 430
431 431 def properly_encode_header(value, encoder, not_email):
432 432 """
433 433 The only thing special (weird) about this function is that it tries
434 434 to do a fast check to see if the header value has an email address in
435 435 it. Since random headers could have an email address, and email addresses
436 436 have weird special formatting rules, we have to check for it.
437 437
438 438 Normally this works fine, but in Librelist, we need to "obfuscate" email
439 439 addresses by changing the '@' to '-AT-'. This is where
440 440 VALUE_IS_EMAIL_ADDRESS exists. It's a simple lambda returning True/False
441 441 to check if a header value has an email address. If you need to make this
442 442 check different, then change this.
443 443 """
444 444 try:
445 445 return value.encode("ascii")
446 446 except UnicodeEncodeError:
447 447 if not not_email and VALUE_IS_EMAIL_ADDRESS(value):
448 448 # this could have an email address, make sure we don't screw it up
449 449 name, address = parseaddr(value)
450 450 return '"%s" <%s>' % (
451 451 encoder.header_encode(name.encode("utf-8")), address)
452 452
453 453 return encoder.header_encode(value.encode("utf-8"))
@@ -1,415 +1,415 b''
1 1 """
2 2 Module provides a class allowing to wrap communication over subprocess.Popen
3 3 input, output, error streams into a meaningfull, non-blocking, concurrent
4 4 stream processor exposing the output data as an iterator fitting to be a
5 5 return value passed by a WSGI applicaiton to a WSGI server per PEP 3333.
6 6
7 7 Copyright (c) 2011 Daniel Dotsenko <dotsa@hotmail.com>
8 8
9 9 This file is part of git_http_backend.py Project.
10 10
11 11 git_http_backend.py Project is free software: you can redistribute it and/or
12 12 modify it under the terms of the GNU Lesser General Public License as
13 13 published by the Free Software Foundation, either version 2.1 of the License,
14 14 or (at your option) any later version.
15 15
16 16 git_http_backend.py Project is distributed in the hope that it will be useful,
17 17 but WITHOUT ANY WARRANTY; without even the implied warranty of
18 18 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 19 GNU Lesser General Public License for more details.
20 20
21 21 You should have received a copy of the GNU Lesser General Public License
22 22 along with git_http_backend.py Project.
23 23 If not, see <http://www.gnu.org/licenses/>.
24 24 """
25 25 import os
26 26 import subprocess
27 27 from rhodecode.lib.vcs.utils.compat import deque, Event, Thread, _bytes, _bytearray
28 28
29 29
30 30 class StreamFeeder(Thread):
31 31 """
32 32 Normal writing into pipe-like is blocking once the buffer is filled.
33 33 This thread allows a thread to seep data from a file-like into a pipe
34 34 without blocking the main thread.
35 35 We close inpipe once the end of the source stream is reached.
36 36 """
37 37 def __init__(self, source):
38 38 super(StreamFeeder, self).__init__()
39 39 self.daemon = True
40 40 filelike = False
41 41 self.bytes = _bytes()
42 42 if type(source) in (type(''), _bytes, _bytearray): # string-like
43 43 self.bytes = _bytes(source)
44 44 else: # can be either file pointer or file-like
45 45 if type(source) in (int, long): # file pointer it is
46 46 ## converting file descriptor (int) stdin into file-like
47 47 try:
48 48 source = os.fdopen(source, 'rb', 16384)
49 49 except Exception:
50 50 pass
51 51 # let's see if source is file-like by now
52 52 try:
53 53 filelike = source.read
54 54 except Exception:
55 55 pass
56 56 if not filelike and not self.bytes:
57 57 raise TypeError("StreamFeeder's source object must be a readable "
58 58 "file-like, a file descriptor, or a string-like.")
59 59 self.source = source
60 60 self.readiface, self.writeiface = os.pipe()
61 61
62 62 def run(self):
63 63 t = self.writeiface
64 64 if self.bytes:
65 65 os.write(t, self.bytes)
66 66 else:
67 67 s = self.source
68 68 b = s.read(4096)
69 69 while b:
70 70 os.write(t, b)
71 71 b = s.read(4096)
72 72 os.close(t)
73 73
74 74 @property
75 75 def output(self):
76 76 return self.readiface
77 77
78 78
79 79 class InputStreamChunker(Thread):
80 80 def __init__(self, source, target, buffer_size, chunk_size):
81 81
82 82 super(InputStreamChunker, self).__init__()
83 83
84 84 self.daemon = True # die die die.
85 85
86 86 self.source = source
87 87 self.target = target
88 88 self.chunk_count_max = int(buffer_size / chunk_size) + 1
89 89 self.chunk_size = chunk_size
90 90
91 91 self.data_added = Event()
92 92 self.data_added.clear()
93 93
94 94 self.keep_reading = Event()
95 95 self.keep_reading.set()
96 96
97 97 self.EOF = Event()
98 98 self.EOF.clear()
99 99
100 100 self.go = Event()
101 101 self.go.set()
102 102
103 103 def stop(self):
104 104 self.go.clear()
105 105 self.EOF.set()
106 106 try:
107 107 # this is not proper, but is done to force the reader thread let
108 108 # go of the input because, if successful, .close() will send EOF
109 109 # down the pipe.
110 110 self.source.close()
111 111 except:
112 112 pass
113 113
114 114 def run(self):
115 115 s = self.source
116 116 t = self.target
117 117 cs = self.chunk_size
118 118 ccm = self.chunk_count_max
119 119 kr = self.keep_reading
120 120 da = self.data_added
121 121 go = self.go
122 122
123 123 try:
124 124 b = s.read(cs)
125 125 except ValueError:
126 126 b = ''
127 127
128 128 while b and go.is_set():
129 129 if len(t) > ccm:
130 130 kr.clear()
131 131 kr.wait(2)
132 132 # # this only works on 2.7.x and up
133 133 # if not kr.wait(10):
134 134 # raise Exception("Timed out while waiting for input to be read.")
135 135 # instead we'll use this
136 136 if len(t) > ccm + 3:
137 137 raise IOError("Timed out while waiting for input from subprocess.")
138 138 t.append(b)
139 139 da.set()
140 140 b = s.read(cs)
141 141 self.EOF.set()
142 142 da.set() # for cases when done but there was no input.
143 143
144 144
145 145 class BufferedGenerator():
146 146 """
147 147 Class behaves as a non-blocking, buffered pipe reader.
148 148 Reads chunks of data (through a thread)
149 149 from a blocking pipe, and attaches these to an array (Deque) of chunks.
150 150 Reading is halted in the thread when max chunks is internally buffered.
151 151 The .next() may operate in blocking or non-blocking fashion by yielding
152 152 '' if no data is ready
153 153 to be sent or by not returning until there is some data to send
154 154 When we get EOF from underlying source pipe we raise the marker to raise
155 155 StopIteration after the last chunk of data is yielded.
156 156 """
157 157
158 158 def __init__(self, source, buffer_size=65536, chunk_size=4096,
159 159 starting_values=[], bottomless=False):
160 160
161 161 if bottomless:
162 162 maxlen = int(buffer_size / chunk_size)
163 163 else:
164 164 maxlen = None
165 165
166 166 self.data = deque(starting_values, maxlen)
167 167
168 168 self.worker = InputStreamChunker(source, self.data, buffer_size,
169 169 chunk_size)
170 170 if starting_values:
171 171 self.worker.data_added.set()
172 172 self.worker.start()
173 173
174 174 ####################
175 175 # Generator's methods
176 176 ####################
177 177
178 178 def __iter__(self):
179 179 return self
180 180
181 181 def next(self):
182 182 while not len(self.data) and not self.worker.EOF.is_set():
183 183 self.worker.data_added.clear()
184 184 self.worker.data_added.wait(0.2)
185 185 if len(self.data):
186 186 self.worker.keep_reading.set()
187 187 return _bytes(self.data.popleft())
188 188 elif self.worker.EOF.is_set():
189 189 raise StopIteration
190 190
191 191 def throw(self, type, value=None, traceback=None):
192 192 if not self.worker.EOF.is_set():
193 193 raise type(value)
194 194
195 195 def start(self):
196 196 self.worker.start()
197 197
198 198 def stop(self):
199 199 self.worker.stop()
200 200
201 201 def close(self):
202 202 try:
203 203 self.worker.stop()
204 204 self.throw(GeneratorExit)
205 205 except (GeneratorExit, StopIteration):
206 206 pass
207 207
208 208 def __del__(self):
209 209 self.close()
210 210
211 211 ####################
212 212 # Threaded reader's infrastructure.
213 213 ####################
214 214 @property
215 215 def input(self):
216 216 return self.worker.w
217 217
218 218 @property
219 219 def data_added_event(self):
220 220 return self.worker.data_added
221 221
222 222 @property
223 223 def data_added(self):
224 224 return self.worker.data_added.is_set()
225 225
226 226 @property
227 227 def reading_paused(self):
228 228 return not self.worker.keep_reading.is_set()
229 229
230 230 @property
231 231 def done_reading_event(self):
232 232 """
233 233 Done_reding does not mean that the iterator's buffer is empty.
234 234 Iterator might have done reading from underlying source, but the read
235 235 chunks might still be available for serving through .next() method.
236 236
237 237 @return An Event class instance.
238 238 """
239 239 return self.worker.EOF
240 240
241 241 @property
242 242 def done_reading(self):
243 243 """
244 244 Done_reding does not mean that the iterator's buffer is empty.
245 245 Iterator might have done reading from underlying source, but the read
246 246 chunks might still be available for serving through .next() method.
247 247
248 248 @return An Bool value.
249 249 """
250 250 return self.worker.EOF.is_set()
251 251
252 252 @property
253 253 def length(self):
254 254 """
255 255 returns int.
256 256
257 257 This is the lenght of the que of chunks, not the length of
258 258 the combined contents in those chunks.
259 259
260 260 __len__() cannot be meaningfully implemented because this
261 261 reader is just flying throuh a bottomless pit content and
262 262 can only know the lenght of what it already saw.
263 263
264 264 If __len__() on WSGI server per PEP 3333 returns a value,
265 265 the responce's length will be set to that. In order not to
266 266 confuse WSGI PEP3333 servers, we will not implement __len__
267 267 at all.
268 268 """
269 269 return len(self.data)
270 270
271 271 def prepend(self, x):
272 272 self.data.appendleft(x)
273 273
274 274 def append(self, x):
275 275 self.data.append(x)
276 276
277 277 def extend(self, o):
278 278 self.data.extend(o)
279 279
280 280 def __getitem__(self, i):
281 281 return self.data[i]
282 282
283 283
284 284 class SubprocessIOChunker(object):
285 285 """
286 286 Processor class wrapping handling of subprocess IO.
287 287
288 288 In a way, this is a "communicate()" replacement with a twist.
289 289
290 290 - We are multithreaded. Writing in and reading out, err are all sep threads.
291 291 - We support concurrent (in and out) stream processing.
292 292 - The output is not a stream. It's a queue of read string (bytes, not unicode)
293 293 chunks. The object behaves as an iterable. You can "for chunk in obj:" us.
294 294 - We are non-blocking in more respects than communicate()
295 295 (reading from subprocess out pauses when internal buffer is full, but
296 296 does not block the parent calling code. On the flip side, reading from
297 297 slow-yielding subprocess may block the iteration until data shows up. This
298 298 does not block the parallel inpipe reading occurring parallel thread.)
299 299
300 300 The purpose of the object is to allow us to wrap subprocess interactions into
301 301 and interable that can be passed to a WSGI server as the application's return
302 302 value. Because of stream-processing-ability, WSGI does not have to read ALL
303 303 of the subprocess's output and buffer it, before handing it to WSGI server for
304 304 HTTP response. Instead, the class initializer reads just a bit of the stream
305 305 to figure out if error ocurred or likely to occur and if not, just hands the
306 306 further iteration over subprocess output to the server for completion of HTTP
307 307 response.
308 308
309 309 The real or perceived subprocess error is trapped and raised as one of
310 310 EnvironmentError family of exceptions
311 311
312 312 Example usage:
313 313 # try:
314 314 # answer = SubprocessIOChunker(
315 315 # cmd,
316 316 # input,
317 317 # buffer_size = 65536,
318 318 # chunk_size = 4096
319 319 # )
320 320 # except (EnvironmentError) as e:
321 321 # print str(e)
322 322 # raise e
323 323 #
324 324 # return answer
325 325
326 326
327 327 """
328 328 def __init__(self, cmd, inputstream=None, buffer_size=65536,
329 329 chunk_size=4096, starting_values=[], **kwargs):
330 330 """
331 331 Initializes SubprocessIOChunker
332 332
333 333 :param cmd: A Subprocess.Popen style "cmd". Can be string or array of strings
334 334 :param inputstream: (Default: None) A file-like, string, or file pointer.
335 335 :param buffer_size: (Default: 65536) A size of total buffer per stream in bytes.
336 336 :param chunk_size: (Default: 4096) A max size of a chunk. Actual chunk may be smaller.
337 337 :param starting_values: (Default: []) An array of strings to put in front of output que.
338 338 """
339 339
340 340 if inputstream:
341 341 input_streamer = StreamFeeder(inputstream)
342 342 input_streamer.start()
343 343 inputstream = input_streamer.output
344 344
345 345 _shell = kwargs.get('shell', True)
346 346 if isinstance(cmd, (list, tuple)):
347 347 cmd = ' '.join(cmd)
348 348
349 349 kwargs['shell'] = _shell
350 350 _p = subprocess.Popen(cmd,
351 351 bufsize=-1,
352 352 stdin=inputstream,
353 353 stdout=subprocess.PIPE,
354 354 stderr=subprocess.PIPE,
355 355 **kwargs
356 356 )
357 357
358 358 bg_out = BufferedGenerator(_p.stdout, buffer_size, chunk_size, starting_values)
359 359 bg_err = BufferedGenerator(_p.stderr, 16000, 1, bottomless=True)
360 360
361 361 while not bg_out.done_reading and not bg_out.reading_paused and not bg_err.length:
362 362 # doing this until we reach either end of file, or end of buffer.
363 363 bg_out.data_added_event.wait(1)
364 364 bg_out.data_added_event.clear()
365 365
366 366 # at this point it's still ambiguous if we are done reading or just full buffer.
367 367 # Either way, if error (returned by ended process, or implied based on
368 368 # presence of stuff in stderr output) we error out.
369 369 # Else, we are happy.
370 370 _returncode = _p.poll()
371 if _returncode or (_returncode == None and bg_err.length):
371 if _returncode or (_returncode is None and bg_err.length):
372 372 try:
373 373 _p.terminate()
374 374 except:
375 375 pass
376 376 bg_out.stop()
377 377 bg_err.stop()
378 378 err = '%s' % ''.join(bg_err)
379 379 if err:
380 380 raise EnvironmentError("Subprocess exited due to an error:\n" + err)
381 381 raise EnvironmentError("Subprocess exited with non 0 ret code:%s" % _returncode)
382 382
383 383 self.process = _p
384 384 self.output = bg_out
385 385 self.error = bg_err
386 386
387 387 def __iter__(self):
388 388 return self
389 389
390 390 def next(self):
391 391 if self.process.poll():
392 392 err = '%s' % ''.join(self.error)
393 393 raise EnvironmentError("Subprocess exited due to an error:\n" + err)
394 394 return self.output.next()
395 395
396 396 def throw(self, type, value=None, traceback=None):
397 397 if self.output.length or not self.output.done_reading:
398 398 raise type(value)
399 399
400 400 def close(self):
401 401 try:
402 402 self.process.terminate()
403 403 except:
404 404 pass
405 405 try:
406 406 self.output.close()
407 407 except:
408 408 pass
409 409 try:
410 410 self.error.close()
411 411 except:
412 412 pass
413 413
414 414 def __del__(self):
415 415 self.close()
@@ -1,265 +1,265 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 from rhodecode.lib.auth import get_crypt_password, check_password
4 4 from rhodecode.model.db import User, RhodeCodeSetting, Repository
5 5 from rhodecode.tests import *
6 6 from rhodecode.lib import helpers as h
7 7 from rhodecode.model.user import UserModel
8 8 from rhodecode.model.scm import ScmModel
9 9 from rhodecode.model.meta import Session
10 10
11 11
12 12 class TestAdminSettingsController(TestController):
13 13
14 14 def test_index(self):
15 15 response = self.app.get(url('admin_settings'))
16 16 # Test response...
17 17
18 18 def test_index_as_xml(self):
19 19 response = self.app.get(url('formatted_admin_settings', format='xml'))
20 20
21 21 def test_create(self):
22 22 response = self.app.post(url('admin_settings'))
23 23
24 24 def test_new(self):
25 25 response = self.app.get(url('admin_new_setting'))
26 26
27 27 def test_new_as_xml(self):
28 28 response = self.app.get(url('formatted_admin_new_setting', format='xml'))
29 29
30 30 def test_update(self):
31 31 response = self.app.put(url('admin_setting', setting_id=1))
32 32
33 33 def test_update_browser_fakeout(self):
34 34 response = self.app.post(url('admin_setting', setting_id=1), params=dict(_method='put'))
35 35
36 36 def test_delete(self):
37 37 response = self.app.delete(url('admin_setting', setting_id=1))
38 38
39 39 def test_delete_browser_fakeout(self):
40 40 response = self.app.post(url('admin_setting', setting_id=1), params=dict(_method='delete'))
41 41
42 42 def test_show(self):
43 43 response = self.app.get(url('admin_setting', setting_id=1))
44 44
45 45 def test_show_as_xml(self):
46 46 response = self.app.get(url('formatted_admin_setting', setting_id=1, format='xml'))
47 47
48 48 def test_edit(self):
49 49 response = self.app.get(url('admin_edit_setting', setting_id=1))
50 50
51 51 def test_edit_as_xml(self):
52 52 response = self.app.get(url('formatted_admin_edit_setting',
53 53 setting_id=1, format='xml'))
54 54
55 55 def test_ga_code_active(self):
56 56 self.log_user()
57 57 old_title = 'RhodeCode'
58 58 old_realm = 'RhodeCode authentication'
59 59 new_ga_code = 'ga-test-123456789'
60 60 response = self.app.post(url('admin_setting', setting_id='global'),
61 61 params=dict(
62 62 _method='put',
63 63 rhodecode_title=old_title,
64 64 rhodecode_realm=old_realm,
65 65 rhodecode_ga_code=new_ga_code
66 66 ))
67 67
68 68 self.checkSessionFlash(response, 'Updated application settings')
69 69
70 70 self.assertEqual(RhodeCodeSetting
71 71 .get_app_settings()['rhodecode_ga_code'], new_ga_code)
72 72
73 73 response = response.follow()
74 74 response.mustcontain("""_gaq.push(['_setAccount', '%s']);""" % new_ga_code)
75 75
76 76 def test_ga_code_inactive(self):
77 77 self.log_user()
78 78 old_title = 'RhodeCode'
79 79 old_realm = 'RhodeCode authentication'
80 80 new_ga_code = ''
81 81 response = self.app.post(url('admin_setting', setting_id='global'),
82 82 params=dict(
83 83 _method='put',
84 84 rhodecode_title=old_title,
85 85 rhodecode_realm=old_realm,
86 86 rhodecode_ga_code=new_ga_code
87 87 ))
88 88
89 89 self.checkSessionFlash(response, 'Updated application settings')
90 90 self.assertEqual(RhodeCodeSetting
91 91 .get_app_settings()['rhodecode_ga_code'], new_ga_code)
92 92
93 93 response = response.follow()
94 94 response.mustcontain(no=["_gaq.push(['_setAccount', '%s']);" % new_ga_code])
95 95
96 96 def test_title_change(self):
97 97 self.log_user()
98 98 old_title = 'RhodeCode'
99 99 new_title = old_title + '_changed'
100 100 old_realm = 'RhodeCode authentication'
101 101
102 102 for new_title in ['Changed', 'Ε»Γ³Ε‚wik', old_title]:
103 103 response = self.app.post(url('admin_setting', setting_id='global'),
104 104 params=dict(
105 105 _method='put',
106 106 rhodecode_title=new_title,
107 107 rhodecode_realm=old_realm,
108 108 rhodecode_ga_code=''
109 109 ))
110 110
111 111 self.checkSessionFlash(response, 'Updated application settings')
112 112 self.assertEqual(RhodeCodeSetting
113 113 .get_app_settings()['rhodecode_title'],
114 114 new_title.decode('utf-8'))
115 115
116 116 response = response.follow()
117 117 response.mustcontain("""<h1><a href="/">%s</a></h1>""" % new_title)
118 118
119 119 def test_my_account(self):
120 120 self.log_user()
121 121 response = self.app.get(url('admin_settings_my_account'))
122 122
123 123 response.mustcontain('value="test_admin')
124 124
125 125 @parameterized.expand([('firstname', 'new_username'),
126 126 ('lastname', 'new_username'),
127 127 ('admin', True),
128 128 ('admin', False),
129 129 ('ldap_dn', 'test'),
130 130 ('ldap_dn', None),
131 131 ('active', False),
132 132 ('active', True),
133 133 ('email', 'some@email.com'),
134 134 ])
135 135 def test_my_account_update(self, name, expected):
136 136 uname = 'testme'
137 137 usr = UserModel().create_or_update(username=uname, password='qweqwe',
138 138 email='testme@rhodecod.org')
139 139 Session().commit()
140 140 params = usr.get_api_data()
141 141 user_id = usr.user_id
142 142 self.log_user(username=uname, password='qweqwe')
143 143 params.update({name: expected})
144 144 params.update({'password_confirmation': ''})
145 145 params.update({'new_password': ''})
146 146
147 147 try:
148 148 response = self.app.put(url('admin_settings_my_account_update',
149 149 id=user_id), params)
150 150
151 151 self.checkSessionFlash(response,
152 152 'Your account was updated successfully')
153 153
154 154 updated_user = User.get_by_username(uname)
155 155 updated_params = updated_user.get_api_data()
156 156 updated_params.update({'password_confirmation': ''})
157 157 updated_params.update({'new_password': ''})
158 158
159 159 params['last_login'] = updated_params['last_login']
160 160 if name == 'email':
161 161 params['emails'] = [expected]
162 162 if name == 'ldap_dn':
163 163 #cannot update this via form
164 164 params['ldap_dn'] = None
165 165 if name == 'active':
166 166 #my account cannot deactivate account
167 167 params['active'] = True
168 168 if name == 'admin':
169 169 #my account cannot make you an admin !
170 170 params['admin'] = False
171 171
172 172 self.assertEqual(params, updated_params)
173 173
174 174 finally:
175 175 UserModel().delete('testme')
176 176
177 177 def test_my_account_update_err_email_exists(self):
178 178 self.log_user()
179 179
180 180 new_email = 'test_regular@mail.com' # already exisitn email
181 181 response = self.app.put(url('admin_settings_my_account_update'),
182 182 params=dict(
183 183 username='test_admin',
184 184 new_password='test12',
185 185 password_confirmation='test122',
186 186 firstname='NewName',
187 187 lastname='NewLastname',
188 188 email=new_email,)
189 189 )
190 190
191 191 response.mustcontain('This e-mail address is already taken')
192 192
193 193 def test_my_account_update_err(self):
194 194 self.log_user('test_regular2', 'test12')
195 195
196 196 new_email = 'newmail.pl'
197 197 response = self.app.post(url('admin_settings_my_account_update'),
198 198 params=dict(
199 199 _method='put',
200 200 username='test_admin',
201 201 new_password='test12',
202 202 password_confirmation='test122',
203 203 firstname='NewName',
204 204 lastname='NewLastname',
205 205 email=new_email,)
206 206 )
207 207
208 208 response.mustcontain('An email address must contain a single @')
209 209 from rhodecode.model import validators
210 210 msg = validators.ValidUsername(edit=False,
211 211 old_data={})._messages['username_exists']
212 212 msg = h.html_escape(msg % {'username': 'test_admin'})
213 213 response.mustcontain(u"%s" % msg)
214 214
215 215 def test_set_repo_fork_has_no_self_id(self):
216 216 self.log_user()
217 217 repo = Repository.get_by_repo_name(HG_REPO)
218 218 response = self.app.get(url('edit_repo', repo_name=HG_REPO))
219 219 opt = """<option value="%s">vcs_test_git</option>""" % repo.repo_id
220 220 response.mustcontain(no=[opt])
221 221
222 222 def test_set_fork_of_repo(self):
223 223 self.log_user()
224 224 repo = Repository.get_by_repo_name(HG_REPO)
225 225 repo2 = Repository.get_by_repo_name(GIT_REPO)
226 226 response = self.app.put(url('repo_as_fork', repo_name=HG_REPO),
227 227 params=dict(
228 228 id_fork_of=repo2.repo_id
229 229 ))
230 230 repo = Repository.get_by_repo_name(HG_REPO)
231 231 repo2 = Repository.get_by_repo_name(GIT_REPO)
232 232 self.checkSessionFlash(response,
233 233 'Marked repo %s as fork of %s' % (repo.repo_name, repo2.repo_name))
234 234
235 235 assert repo.fork == repo2
236 236 response = response.follow()
237 237 # check if given repo is selected
238 238
239 239 opt = """<option value="%s" selected="selected">%s</option>""" % (
240 240 repo2.repo_id, repo2.repo_name)
241 241 response.mustcontain(opt)
242 242
243 243 # clean session flash
244 244 #response = self.app.get(url('edit_repo', repo_name=HG_REPO))
245 245
246 246 ## mark it as None
247 247 response = self.app.put(url('repo_as_fork', repo_name=HG_REPO),
248 248 params=dict(
249 249 id_fork_of=None
250 250 ))
251 251 repo = Repository.get_by_repo_name(HG_REPO)
252 252 repo2 = Repository.get_by_repo_name(GIT_REPO)
253 253 self.checkSessionFlash(response,
254 254 'Marked repo %s as fork of %s' % (repo.repo_name, "Nothing"))
255 assert repo.fork == None
255 assert repo.fork is None
256 256
257 257 def test_set_fork_of_same_repo(self):
258 258 self.log_user()
259 259 repo = Repository.get_by_repo_name(HG_REPO)
260 260 response = self.app.put(url('repo_as_fork', repo_name=HG_REPO),
261 261 params=dict(
262 262 id_fork_of=repo.repo_id
263 263 ))
264 264 self.checkSessionFlash(response,
265 265 'An error occurred during this operation')
@@ -1,253 +1,253 b''
1 1 # -*- coding: utf-8 -*-
2 2 import formencode
3 3
4 4 from rhodecode.tests import *
5 5
6 6 from rhodecode.model import validators as v
7 7 from rhodecode.model.users_group import UserGroupModel
8 8
9 9 from rhodecode.model.meta import Session
10 10 from rhodecode.model.repos_group import ReposGroupModel
11 11 from rhodecode.model.db import ChangesetStatus, Repository
12 12 from rhodecode.model.changeset_status import ChangesetStatusModel
13 13 from rhodecode.tests.fixture import Fixture
14 14
15 15 fixture = Fixture()
16 16
17 17
18 18 class TestReposGroups(BaseTestCase):
19 19
20 20 def setUp(self):
21 21 pass
22 22
23 23 def tearDown(self):
24 24 Session.remove()
25 25
26 26 def test_Message_extractor(self):
27 27 validator = v.ValidUsername()
28 28 self.assertRaises(formencode.Invalid, validator.to_python, 'default')
29 29
30 30 class StateObj(object):
31 31 pass
32 32
33 33 self.assertRaises(formencode.Invalid,
34 34 validator.to_python, 'default', StateObj)
35 35
36 36 def test_ValidUsername(self):
37 37 validator = v.ValidUsername()
38 38
39 39 self.assertRaises(formencode.Invalid, validator.to_python, 'default')
40 40 self.assertRaises(formencode.Invalid, validator.to_python, 'new_user')
41 41 self.assertRaises(formencode.Invalid, validator.to_python, '.,')
42 42 self.assertRaises(formencode.Invalid, validator.to_python,
43 43 TEST_USER_ADMIN_LOGIN)
44 44 self.assertEqual('test', validator.to_python('test'))
45 45
46 46 validator = v.ValidUsername(edit=True, old_data={'user_id': 1})
47 47
48 48 def test_ValidRepoUser(self):
49 49 validator = v.ValidRepoUser()
50 50 self.assertRaises(formencode.Invalid, validator.to_python, 'nouser')
51 51 self.assertEqual(TEST_USER_ADMIN_LOGIN,
52 52 validator.to_python(TEST_USER_ADMIN_LOGIN))
53 53
54 54 def test_ValidUserGroup(self):
55 55 validator = v.ValidUserGroup()
56 56 self.assertRaises(formencode.Invalid, validator.to_python, 'default')
57 57 self.assertRaises(formencode.Invalid, validator.to_python, '.,')
58 58
59 59 gr = fixture.create_user_group('test')
60 60 gr2 = fixture.create_user_group('tes2')
61 61 Session().commit()
62 62 self.assertRaises(formencode.Invalid, validator.to_python, 'test')
63 assert gr.users_group_id != None
63 assert gr.users_group_id is not None
64 64 validator = v.ValidUserGroup(edit=True,
65 65 old_data={'users_group_id':
66 66 gr2.users_group_id})
67 67
68 68 self.assertRaises(formencode.Invalid, validator.to_python, 'test')
69 69 self.assertRaises(formencode.Invalid, validator.to_python, 'TesT')
70 70 self.assertRaises(formencode.Invalid, validator.to_python, 'TEST')
71 71 UserGroupModel().delete(gr)
72 72 UserGroupModel().delete(gr2)
73 73 Session().commit()
74 74
75 75 def test_ValidReposGroup(self):
76 76 validator = v.ValidReposGroup()
77 77 model = ReposGroupModel()
78 78 self.assertRaises(formencode.Invalid, validator.to_python,
79 79 {'group_name': HG_REPO, })
80 80 gr = model.create(group_name='test_gr', group_description='desc',
81 81 parent=None,
82 82 just_db=True,
83 83 owner=TEST_USER_ADMIN_LOGIN)
84 84 self.assertRaises(formencode.Invalid,
85 85 validator.to_python, {'group_name': gr.group_name, })
86 86
87 87 validator = v.ValidReposGroup(edit=True,
88 88 old_data={'group_id': gr.group_id})
89 89 self.assertRaises(formencode.Invalid,
90 90 validator.to_python, {
91 91 'group_name': gr.group_name + 'n',
92 92 'group_parent_id': gr.group_id
93 93 })
94 94 model.delete(gr)
95 95
96 96 def test_ValidPassword(self):
97 97 validator = v.ValidPassword()
98 98 self.assertEqual('lol', validator.to_python('lol'))
99 99 self.assertEqual(None, validator.to_python(None))
100 100 self.assertRaises(formencode.Invalid, validator.to_python, 'Δ…Δ‡ΕΌΕΊ')
101 101
102 102 def test_ValidPasswordsMatch(self):
103 103 validator = v.ValidPasswordsMatch()
104 104 self.assertRaises(formencode.Invalid,
105 105 validator.to_python, {'password': 'pass',
106 106 'password_confirmation': 'pass2'})
107 107
108 108 self.assertRaises(formencode.Invalid,
109 109 validator.to_python, {'new_password': 'pass',
110 110 'password_confirmation': 'pass2'})
111 111
112 112 self.assertEqual({'new_password': 'pass',
113 113 'password_confirmation': 'pass'},
114 114 validator.to_python({'new_password': 'pass',
115 115 'password_confirmation': 'pass'}))
116 116
117 117 self.assertEqual({'password': 'pass',
118 118 'password_confirmation': 'pass'},
119 119 validator.to_python({'password': 'pass',
120 120 'password_confirmation': 'pass'}))
121 121
122 122 def test_ValidAuth(self):
123 123 validator = v.ValidAuth()
124 124 valid_creds = {
125 125 'username': TEST_USER_REGULAR2_LOGIN,
126 126 'password': TEST_USER_REGULAR2_PASS,
127 127 }
128 128 invalid_creds = {
129 129 'username': 'err',
130 130 'password': 'err',
131 131 }
132 132 self.assertEqual(valid_creds, validator.to_python(valid_creds))
133 133 self.assertRaises(formencode.Invalid,
134 134 validator.to_python, invalid_creds)
135 135
136 136 def test_ValidAuthToken(self):
137 137 validator = v.ValidAuthToken()
138 138 # this is untestable without a threadlocal
139 139 # self.assertRaises(formencode.Invalid,
140 140 # validator.to_python, 'BadToken')
141 141 validator
142 142
143 143 def test_ValidRepoName(self):
144 144 validator = v.ValidRepoName()
145 145
146 146 self.assertRaises(formencode.Invalid,
147 147 validator.to_python, {'repo_name': ''})
148 148
149 149 self.assertRaises(formencode.Invalid,
150 150 validator.to_python, {'repo_name': HG_REPO})
151 151
152 152 gr = ReposGroupModel().create(group_name='group_test',
153 153 group_description='desc',
154 154 parent=None,
155 155 owner=TEST_USER_ADMIN_LOGIN)
156 156 self.assertRaises(formencode.Invalid,
157 157 validator.to_python, {'repo_name': gr.group_name})
158 158
159 159 #TODO: write an error case for that ie. create a repo withinh a group
160 160 # self.assertRaises(formencode.Invalid,
161 161 # validator.to_python, {'repo_name': 'some',
162 162 # 'repo_group': gr.group_id})
163 163
164 164 def test_ValidForkName(self):
165 165 # this uses ValidRepoName validator
166 166 assert True
167 167
168 168 @parameterized.expand([
169 169 ('test', 'test'), ('lolz!', 'lolz'), (' aavv', 'aavv'),
170 170 ('ala ma kota', 'ala-ma-kota'), ('@nooo', 'nooo'),
171 171 ('$!haha lolz !', 'haha-lolz'), ('$$$$$', ''), ('{}OK!', 'OK'),
172 172 ('/]re po', 're-po')])
173 173 def test_SlugifyName(self, name, expected):
174 174 validator = v.SlugifyName()
175 175 self.assertEqual(expected, validator.to_python(name))
176 176
177 177 def test_ValidCloneUri(self):
178 178 #TODO: write this one
179 179 pass
180 180
181 181 def test_ValidForkType(self):
182 182 validator = v.ValidForkType(old_data={'repo_type': 'hg'})
183 183 self.assertEqual('hg', validator.to_python('hg'))
184 184 self.assertRaises(formencode.Invalid, validator.to_python, 'git')
185 185
186 186 def test_ValidPerms(self):
187 187 #TODO: write this one
188 188 pass
189 189
190 190 def test_ValidSettings(self):
191 191 validator = v.ValidSettings()
192 192 self.assertEqual({'pass': 'pass'},
193 193 validator.to_python(value={'user': 'test',
194 194 'pass': 'pass'}))
195 195
196 196 self.assertEqual({'user2': 'test', 'pass': 'pass'},
197 197 validator.to_python(value={'user2': 'test',
198 198 'pass': 'pass'}))
199 199
200 200 def test_ValidPath(self):
201 201 validator = v.ValidPath()
202 202 self.assertEqual(TESTS_TMP_PATH,
203 203 validator.to_python(TESTS_TMP_PATH))
204 204 self.assertRaises(formencode.Invalid, validator.to_python,
205 205 '/no_such_dir')
206 206
207 207 def test_UniqSystemEmail(self):
208 208 validator = v.UniqSystemEmail(old_data={})
209 209
210 210 self.assertEqual('mail@python.org',
211 211 validator.to_python('MaiL@Python.org'))
212 212
213 213 email = TEST_USER_REGULAR2_EMAIL
214 214 self.assertRaises(formencode.Invalid, validator.to_python, email)
215 215
216 216 def test_ValidSystemEmail(self):
217 217 validator = v.ValidSystemEmail()
218 218 email = TEST_USER_REGULAR2_EMAIL
219 219
220 220 self.assertEqual(email, validator.to_python(email))
221 221 self.assertRaises(formencode.Invalid, validator.to_python, 'err')
222 222
223 223 def test_LdapLibValidator(self):
224 224 if ldap_lib_installed:
225 225 validator = v.LdapLibValidator()
226 226 self.assertEqual("DN", validator.to_python('DN'))
227 227 else:
228 228 validator = v.LdapLibValidator()
229 229 self.assertRaises(v.LdapImportError, validator.to_python, 'err')
230 230
231 231 def test_AttrLoginValidator(self):
232 232 validator = v.AttrLoginValidator()
233 233 self.assertEqual('DN_attr', validator.to_python('DN_attr'))
234 234
235 235 def test_NotReviewedRevisions(self):
236 236 repo_id = Repository.get_by_repo_name(HG_REPO).repo_id
237 237 validator = v.NotReviewedRevisions(repo_id)
238 238 rev = '0' * 40
239 239 # add status for a rev, that should throw an error because it is already
240 240 # reviewed
241 241 new_status = ChangesetStatus()
242 242 new_status.author = ChangesetStatusModel()._get_user(TEST_USER_ADMIN_LOGIN)
243 243 new_status.repo = ChangesetStatusModel()._get_repo(HG_REPO)
244 244 new_status.status = ChangesetStatus.STATUS_APPROVED
245 245 new_status.comment = None
246 246 new_status.revision = rev
247 247 Session().add(new_status)
248 248 Session().commit()
249 249 try:
250 250 self.assertRaises(formencode.Invalid, validator.to_python, [rev])
251 251 finally:
252 252 Session().delete(new_status)
253 253 Session().commit()
@@ -1,220 +1,220 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.tests.test_hg_operations
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Test suite for making push/pull operations
7 7
8 8 :created_on: Dec 30, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import os
27 27 import sys
28 28 import shutil
29 29 import logging
30 30 from os.path import join as jn
31 31 from os.path import dirname as dn
32 32
33 33 from tempfile import _RandomNameSequence
34 34 from subprocess import Popen, PIPE
35 35
36 36 from paste.deploy import appconfig
37 37 from pylons import config
38 38 from sqlalchemy import engine_from_config
39 39
40 40 from rhodecode.lib.utils import add_cache
41 41 from rhodecode.model import init_model
42 42 from rhodecode.model import meta
43 43 from rhodecode.model.db import User, Repository
44 44 from rhodecode.lib.auth import get_crypt_password
45 45
46 46 from rhodecode.tests import TESTS_TMP_PATH, NEW_HG_REPO, HG_REPO
47 47 from rhodecode.config.environment import load_environment
48 48
49 49 rel_path = dn(dn(dn(dn(os.path.abspath(__file__)))))
50 50 conf = appconfig('config:rc.ini', relative_to=rel_path)
51 51 load_environment(conf.global_conf, conf.local_conf)
52 52
53 53 add_cache(conf)
54 54
55 55 USER = 'test_admin'
56 56 PASS = 'test12'
57 57 HOST = 'rc.local'
58 58 METHOD = 'pull'
59 59 DEBUG = True
60 60 log = logging.getLogger(__name__)
61 61
62 62
63 63 class Command(object):
64 64
65 65 def __init__(self, cwd):
66 66 self.cwd = cwd
67 67
68 68 def execute(self, cmd, *args):
69 69 """Runs command on the system with given ``args``.
70 70 """
71 71
72 72 command = cmd + ' ' + ' '.join(args)
73 73 log.debug('Executing %s' % command)
74 74 if DEBUG:
75 75 print command
76 76 p = Popen(command, shell=True, stdout=PIPE, stderr=PIPE, cwd=self.cwd)
77 77 stdout, stderr = p.communicate()
78 78 if DEBUG:
79 79 print stdout, stderr
80 80 return stdout, stderr
81 81
82 82
83 83 def get_session():
84 84 engine = engine_from_config(conf, 'sqlalchemy.db1.')
85 85 init_model(engine)
86 86 sa = meta.Session
87 87 return sa
88 88
89 89
90 90 def create_test_user(force=True):
91 91 print 'creating test user'
92 92 sa = get_session()
93 93
94 94 user = sa.query(User).filter(User.username == USER).scalar()
95 95
96 96 if force and user is not None:
97 97 print 'removing current user'
98 98 for repo in sa.query(Repository).filter(Repository.user == user).all():
99 99 sa.delete(repo)
100 100 sa.delete(user)
101 101 sa.commit()
102 102
103 103 if user is None or force:
104 104 print 'creating new one'
105 105 new_usr = User()
106 106 new_usr.username = USER
107 107 new_usr.password = get_crypt_password(PASS)
108 108 new_usr.email = 'mail@mail.com'
109 109 new_usr.name = 'test'
110 110 new_usr.lastname = 'lasttestname'
111 111 new_usr.active = True
112 112 new_usr.admin = True
113 113 sa.add(new_usr)
114 114 sa.commit()
115 115
116 116 print 'done'
117 117
118 118
119 119 def create_test_repo(force=True):
120 120 print 'creating test repo'
121 121 from rhodecode.model.repo import RepoModel
122 122 sa = get_session()
123 123
124 124 user = sa.query(User).filter(User.username == USER).scalar()
125 125 if user is None:
126 126 raise Exception('user not found')
127 127
128 128 repo = sa.query(Repository).filter(Repository.repo_name == HG_REPO).scalar()
129 129
130 130 if repo is None:
131 131 print 'repo not found creating'
132 132
133 133 form_data = {'repo_name': HG_REPO,
134 134 'repo_type': 'hg',
135 135 'private':False,
136 136 'clone_uri': '' }
137 137 rm = RepoModel(sa)
138 138 rm.base_path = '/home/hg'
139 139 rm.create(form_data, user)
140 140
141 141 print 'done'
142 142
143 143
144 144 def set_anonymous_access(enable=True):
145 145 sa = get_session()
146 146 user = sa.query(User).filter(User.username == 'default').one()
147 147 user.active = enable
148 148 sa.add(user)
149 149 sa.commit()
150 150
151 151
152 152 def get_anonymous_access():
153 153 sa = get_session()
154 154 return sa.query(User).filter(User.username == 'default').one().active
155 155
156 156
157 157 #==============================================================================
158 158 # TESTS
159 159 #==============================================================================
160 160 def test_clone_with_credentials(no_errors=False, repo=HG_REPO, method=METHOD,
161 161 seq=None, backend='hg'):
162 162 cwd = path = jn(TESTS_TMP_PATH, repo)
163 163
164 if seq == None:
164 if seq is None:
165 165 seq = _RandomNameSequence().next()
166 166
167 167 try:
168 168 shutil.rmtree(path, ignore_errors=True)
169 169 os.makedirs(path)
170 170 #print 'made dirs %s' % jn(path)
171 171 except OSError:
172 172 raise
173 173
174 174 clone_url = 'http://%(user)s:%(pass)s@%(host)s/%(cloned_repo)s' % \
175 175 {'user': USER,
176 176 'pass': PASS,
177 177 'host': HOST,
178 178 'cloned_repo': repo, }
179 179
180 180 dest = path + seq
181 181 if method == 'pull':
182 182 stdout, stderr = Command(cwd).execute(backend, method, '--cwd', dest, clone_url)
183 183 else:
184 184 stdout, stderr = Command(cwd).execute(backend, method, clone_url, dest)
185 185 print stdout,'sdasdsadsa'
186 186 if not no_errors:
187 187 if backend == 'hg':
188 188 assert """adding file changes""" in stdout, 'no messages about cloning'
189 189 assert """abort""" not in stderr , 'got error from clone'
190 190 elif backend == 'git':
191 191 assert """Cloning into""" in stdout, 'no messages about cloning'
192 192
193 193 if __name__ == '__main__':
194 194 try:
195 195 create_test_user(force=False)
196 196 seq = None
197 197 import time
198 198
199 199 try:
200 200 METHOD = sys.argv[3]
201 201 except Exception:
202 202 pass
203 203
204 204 try:
205 205 backend = sys.argv[4]
206 206 except Exception:
207 207 backend = 'hg'
208 208
209 209 if METHOD == 'pull':
210 210 seq = _RandomNameSequence().next()
211 211 test_clone_with_credentials(repo=sys.argv[1], method='clone',
212 212 seq=seq, backend=backend)
213 213 s = time.time()
214 214 for i in range(1, int(sys.argv[2]) + 1):
215 215 print 'take', i
216 216 test_clone_with_credentials(repo=sys.argv[1], method=METHOD,
217 217 seq=seq, backend=backend)
218 218 print 'time taken %.3f' % (time.time() - s)
219 219 except Exception, e:
220 220 sys.exit('stop on %s' % e)
General Comments 0
You need to be logged in to leave comments. Login now