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