##// END OF EJS Templates
fixed issue #671 commenting on pull requests sometimes used old JSON encoder and broke. This changeset replaces it's with RhodeCode json encoder to ensure all data is properly serializable
marcink -
r3061:7727faad beta
parent child Browse files
Show More
@@ -1,391 +1,390
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.changeset
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 changeset controller for pylons showoing changes beetween
7 7 revisions
8 8
9 9 :created_on: Apr 25, 2010
10 10 :author: marcink
11 11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 12 :license: GPLv3, see COPYING for more details.
13 13 """
14 14 # This program is free software: you can redistribute it and/or modify
15 15 # it under the terms of the GNU General Public License as published by
16 16 # the Free Software Foundation, either version 3 of the License, or
17 17 # (at your option) any later version.
18 18 #
19 19 # This program is distributed in the hope that it will be useful,
20 20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 22 # GNU General Public License for more details.
23 23 #
24 24 # You should have received a copy of the GNU General Public License
25 25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 26 import logging
27 27 import traceback
28 28 from collections import defaultdict
29 29 from webob.exc import HTTPForbidden, HTTPBadRequest
30 30
31 31 from pylons import tmpl_context as c, url, request, response
32 32 from pylons.i18n.translation import _
33 33 from pylons.controllers.util import redirect
34 from pylons.decorators import jsonify
34 from rhodecode.lib.utils import jsonify
35 35
36 from rhodecode.lib.vcs.exceptions import RepositoryError, ChangesetError, \
36 from rhodecode.lib.vcs.exceptions import RepositoryError, \
37 37 ChangesetDoesNotExistError
38 from rhodecode.lib.vcs.nodes import FileNode
39 38
40 39 import rhodecode.lib.helpers as h
41 40 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
42 41 from rhodecode.lib.base import BaseRepoController, render
43 42 from rhodecode.lib.utils import action_logger
44 43 from rhodecode.lib.compat import OrderedDict
45 44 from rhodecode.lib import diffs
46 45 from rhodecode.model.db import ChangesetComment, ChangesetStatus
47 46 from rhodecode.model.comment import ChangesetCommentsModel
48 47 from rhodecode.model.changeset_status import ChangesetStatusModel
49 48 from rhodecode.model.meta import Session
49 from rhodecode.model.repo import RepoModel
50 50 from rhodecode.lib.diffs import LimitedDiffContainer
51 from rhodecode.model.repo import RepoModel
52 51 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
53 52 from rhodecode.lib.vcs.backends.base import EmptyChangeset
54 53 from rhodecode.lib.utils2 import safe_unicode
55 54
56 55 log = logging.getLogger(__name__)
57 56
58 57
59 58 def _update_with_GET(params, GET):
60 59 for k in ['diff1', 'diff2', 'diff']:
61 60 params[k] += GET.getall(k)
62 61
63 62
64 63 def anchor_url(revision, path, GET):
65 64 fid = h.FID(revision, path)
66 65 return h.url.current(anchor=fid, **dict(GET))
67 66
68 67
69 68 def get_ignore_ws(fid, GET):
70 69 ig_ws_global = GET.get('ignorews')
71 70 ig_ws = filter(lambda k: k.startswith('WS'), GET.getall(fid))
72 71 if ig_ws:
73 72 try:
74 73 return int(ig_ws[0].split(':')[-1])
75 74 except:
76 75 pass
77 76 return ig_ws_global
78 77
79 78
80 79 def _ignorews_url(GET, fileid=None):
81 80 fileid = str(fileid) if fileid else None
82 81 params = defaultdict(list)
83 82 _update_with_GET(params, GET)
84 83 lbl = _('show white space')
85 84 ig_ws = get_ignore_ws(fileid, GET)
86 85 ln_ctx = get_line_ctx(fileid, GET)
87 86 # global option
88 87 if fileid is None:
89 88 if ig_ws is None:
90 89 params['ignorews'] += [1]
91 90 lbl = _('ignore white space')
92 91 ctx_key = 'context'
93 92 ctx_val = ln_ctx
94 93 # per file options
95 94 else:
96 95 if ig_ws is None:
97 96 params[fileid] += ['WS:1']
98 97 lbl = _('ignore white space')
99 98
100 99 ctx_key = fileid
101 100 ctx_val = 'C:%s' % ln_ctx
102 101 # if we have passed in ln_ctx pass it along to our params
103 102 if ln_ctx:
104 103 params[ctx_key] += [ctx_val]
105 104
106 105 params['anchor'] = fileid
107 106 img = h.image(h.url('/images/icons/text_strikethrough.png'), lbl, class_='icon')
108 107 return h.link_to(img, h.url.current(**params), title=lbl, class_='tooltip')
109 108
110 109
111 110 def get_line_ctx(fid, GET):
112 111 ln_ctx_global = GET.get('context')
113 112 if fid:
114 113 ln_ctx = filter(lambda k: k.startswith('C'), GET.getall(fid))
115 114 else:
116 115 _ln_ctx = filter(lambda k: k.startswith('C'), GET)
117 116 ln_ctx = GET.get(_ln_ctx[0]) if _ln_ctx else ln_ctx_global
118 117 if ln_ctx:
119 118 ln_ctx = [ln_ctx]
120 119
121 120 if ln_ctx:
122 121 retval = ln_ctx[0].split(':')[-1]
123 122 else:
124 123 retval = ln_ctx_global
125 124
126 125 try:
127 126 return int(retval)
128 127 except:
129 128 return 3
130 129
131 130
132 131 def _context_url(GET, fileid=None):
133 132 """
134 133 Generates url for context lines
135 134
136 135 :param fileid:
137 136 """
138 137
139 138 fileid = str(fileid) if fileid else None
140 139 ig_ws = get_ignore_ws(fileid, GET)
141 140 ln_ctx = (get_line_ctx(fileid, GET) or 3) * 2
142 141
143 142 params = defaultdict(list)
144 143 _update_with_GET(params, GET)
145 144
146 145 # global option
147 146 if fileid is None:
148 147 if ln_ctx > 0:
149 148 params['context'] += [ln_ctx]
150 149
151 150 if ig_ws:
152 151 ig_ws_key = 'ignorews'
153 152 ig_ws_val = 1
154 153
155 154 # per file option
156 155 else:
157 156 params[fileid] += ['C:%s' % ln_ctx]
158 157 ig_ws_key = fileid
159 158 ig_ws_val = 'WS:%s' % 1
160 159
161 160 if ig_ws:
162 161 params[ig_ws_key] += [ig_ws_val]
163 162
164 163 lbl = _('%s line context') % ln_ctx
165 164
166 165 params['anchor'] = fileid
167 166 img = h.image(h.url('/images/icons/table_add.png'), lbl, class_='icon')
168 167 return h.link_to(img, h.url.current(**params), title=lbl, class_='tooltip')
169 168
170 169
171 170 class ChangesetController(BaseRepoController):
172 171
173 172 @LoginRequired()
174 173 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
175 174 'repository.admin')
176 175 def __before__(self):
177 176 super(ChangesetController, self).__before__()
178 177 c.affected_files_cut_off = 60
179 178 repo_model = RepoModel()
180 179 c.users_array = repo_model.get_users_js()
181 180 c.users_groups_array = repo_model.get_users_groups_js()
182 181
183 182 def index(self, revision, method='show'):
184 183 c.anchor_url = anchor_url
185 184 c.ignorews_url = _ignorews_url
186 185 c.context_url = _context_url
187 186 c.fulldiff = fulldiff = request.GET.get('fulldiff')
188 187 #get ranges of revisions if preset
189 188 rev_range = revision.split('...')[:2]
190 189 enable_comments = True
191 190 try:
192 191 if len(rev_range) == 2:
193 192 enable_comments = False
194 193 rev_start = rev_range[0]
195 194 rev_end = rev_range[1]
196 195 rev_ranges = c.rhodecode_repo.get_changesets(start=rev_start,
197 196 end=rev_end)
198 197 else:
199 198 rev_ranges = [c.rhodecode_repo.get_changeset(revision)]
200 199
201 200 c.cs_ranges = list(rev_ranges)
202 201 if not c.cs_ranges:
203 202 raise RepositoryError('Changeset range returned empty result')
204 203
205 204 except (RepositoryError, ChangesetDoesNotExistError, Exception), e:
206 205 log.error(traceback.format_exc())
207 206 h.flash(str(e), category='warning')
208 207 return redirect(url('home'))
209 208
210 209 c.changes = OrderedDict()
211 210
212 211 c.lines_added = 0 # count of lines added
213 212 c.lines_deleted = 0 # count of lines removes
214 213
215 214 c.changeset_statuses = ChangesetStatus.STATUSES
216 215 c.comments = []
217 216 c.statuses = []
218 217 c.inline_comments = []
219 218 c.inline_cnt = 0
220 219
221 220 # Iterate over ranges (default changeset view is always one changeset)
222 221 for changeset in c.cs_ranges:
223 222 inlines = []
224 223 if method == 'show':
225 224 c.statuses.extend([ChangesetStatusModel()\
226 225 .get_status(c.rhodecode_db_repo.repo_id,
227 226 changeset.raw_id)])
228 227
229 228 c.comments.extend(ChangesetCommentsModel()\
230 229 .get_comments(c.rhodecode_db_repo.repo_id,
231 230 revision=changeset.raw_id))
232 231 inlines = ChangesetCommentsModel()\
233 232 .get_inline_comments(c.rhodecode_db_repo.repo_id,
234 233 revision=changeset.raw_id)
235 234 c.inline_comments.extend(inlines)
236 235
237 236 c.changes[changeset.raw_id] = []
238 237
239 238 cs2 = changeset.raw_id
240 239 cs1 = changeset.parents[0].raw_id if changeset.parents else EmptyChangeset()
241 240 context_lcl = get_line_ctx('', request.GET)
242 241 ign_whitespace_lcl = ign_whitespace_lcl = get_ignore_ws('', request.GET)
243 242
244 243 _diff = c.rhodecode_repo.get_diff(cs1, cs2,
245 244 ignore_whitespace=ign_whitespace_lcl, context=context_lcl)
246 245 diff_limit = self.cut_off_limit if not fulldiff else None
247 246 diff_processor = diffs.DiffProcessor(_diff,
248 247 vcs=c.rhodecode_repo.alias,
249 248 format='gitdiff',
250 249 diff_limit=diff_limit)
251 250 cs_changes = OrderedDict()
252 251 if method == 'show':
253 252 _parsed = diff_processor.prepare()
254 253 c.limited_diff = False
255 254 if isinstance(_parsed, LimitedDiffContainer):
256 255 c.limited_diff = True
257 256 for f in _parsed:
258 257 st = f['stats']
259 258 if st[0] != 'b':
260 259 c.lines_added += st[0]
261 260 c.lines_deleted += st[1]
262 261 fid = h.FID(changeset.raw_id, f['filename'])
263 262 diff = diff_processor.as_html(enable_comments=enable_comments,
264 263 parsed_lines=[f])
265 264 cs_changes[fid] = [cs1, cs2, f['operation'], f['filename'],
266 265 diff, st]
267 266 else:
268 267 # downloads/raw we only need RAW diff nothing else
269 268 diff = diff_processor.as_raw()
270 269 cs_changes[''] = [None, None, None, None, diff, None]
271 270 c.changes[changeset.raw_id] = cs_changes
272 271
273 272 # count inline comments
274 273 for __, lines in c.inline_comments:
275 274 for comments in lines.values():
276 275 c.inline_cnt += len(comments)
277 276
278 277 if len(c.cs_ranges) == 1:
279 278 c.changeset = c.cs_ranges[0]
280 279 c.parent_tmpl = ''.join(['# Parent %s\n' % x.raw_id
281 280 for x in c.changeset.parents])
282 281 if method == 'download':
283 282 response.content_type = 'text/plain'
284 283 response.content_disposition = 'attachment; filename=%s.diff' \
285 284 % revision[:12]
286 285 return diff
287 286 elif method == 'patch':
288 287 response.content_type = 'text/plain'
289 288 c.diff = safe_unicode(diff)
290 289 return render('changeset/patch_changeset.html')
291 290 elif method == 'raw':
292 291 response.content_type = 'text/plain'
293 292 return diff
294 293 elif method == 'show':
295 294 if len(c.cs_ranges) == 1:
296 295 return render('changeset/changeset.html')
297 296 else:
298 297 return render('changeset/changeset_range.html')
299 298
300 299 def changeset_raw(self, revision):
301 300 return self.index(revision, method='raw')
302 301
303 302 def changeset_patch(self, revision):
304 303 return self.index(revision, method='patch')
305 304
306 305 def changeset_download(self, revision):
307 306 return self.index(revision, method='download')
308 307
309 308 @jsonify
310 309 def comment(self, repo_name, revision):
311 310 status = request.POST.get('changeset_status')
312 311 change_status = request.POST.get('change_changeset_status')
313 312 text = request.POST.get('text')
314 313 if status and change_status:
315 314 text = text or (_('Status change -> %s')
316 315 % ChangesetStatus.get_status_lbl(status))
317 316
318 317 comm = ChangesetCommentsModel().create(
319 318 text=text,
320 319 repo=c.rhodecode_db_repo.repo_id,
321 320 user=c.rhodecode_user.user_id,
322 321 revision=revision,
323 322 f_path=request.POST.get('f_path'),
324 323 line_no=request.POST.get('line'),
325 324 status_change=(ChangesetStatus.get_status_lbl(status)
326 325 if status and change_status else None)
327 326 )
328 327
329 328 # get status if set !
330 329 if status and change_status:
331 330 # if latest status was from pull request and it's closed
332 331 # disallow changing status !
333 332 # dont_allow_on_closed_pull_request = True !
334 333
335 334 try:
336 335 ChangesetStatusModel().set_status(
337 336 c.rhodecode_db_repo.repo_id,
338 337 status,
339 338 c.rhodecode_user.user_id,
340 339 comm,
341 340 revision=revision,
342 341 dont_allow_on_closed_pull_request=True
343 342 )
344 343 except StatusChangeOnClosedPullRequestError:
345 344 log.error(traceback.format_exc())
346 345 msg = _('Changing status on a changeset associated with'
347 346 'a closed pull request is not allowed')
348 347 h.flash(msg, category='warning')
349 348 return redirect(h.url('changeset_home', repo_name=repo_name,
350 349 revision=revision))
351 350 action_logger(self.rhodecode_user,
352 351 'user_commented_revision:%s' % revision,
353 352 c.rhodecode_db_repo, self.ip_addr, self.sa)
354 353
355 354 Session().commit()
356 355
357 356 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
358 357 return redirect(h.url('changeset_home', repo_name=repo_name,
359 358 revision=revision))
360 359
361 360 data = {
362 361 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
363 362 }
364 363 if comm:
365 364 c.co = comm
366 365 data.update(comm.get_dict())
367 366 data.update({'rendered_text':
368 367 render('changeset/changeset_comment_block.html')})
369 368
370 369 return data
371 370
372 371 @jsonify
373 372 def delete_comment(self, repo_name, comment_id):
374 373 co = ChangesetComment.get(comment_id)
375 374 owner = lambda: co.author.user_id == c.rhodecode_user.user_id
376 375 if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
377 376 ChangesetCommentsModel().delete(comment=co)
378 377 Session().commit()
379 378 return True
380 379 else:
381 380 raise HTTPForbidden()
382 381
383 382 @jsonify
384 383 def changeset_info(self, repo_name, revision):
385 384 if request.is_xhr:
386 385 try:
387 386 return c.rhodecode_repo.get_changeset(revision)
388 387 except ChangesetDoesNotExistError, e:
389 388 return EmptyChangeset(message=str(e))
390 389 else:
391 390 raise HTTPBadRequest()
@@ -1,594 +1,594
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.files
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Files controller for RhodeCode
7 7
8 8 :created_on: Apr 21, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25 from __future__ import with_statement
26 26 import os
27 27 import logging
28 28 import traceback
29 29 import tempfile
30 30
31 31 from pylons import request, response, tmpl_context as c, url
32 32 from pylons.i18n.translation import _
33 33 from pylons.controllers.util import redirect
34 from pylons.decorators import jsonify
34 from rhodecode.lib.utils import jsonify
35 35
36 36 from rhodecode.lib import diffs
37 37 from rhodecode.lib import helpers as h
38 38
39 39 from rhodecode.lib.compat import OrderedDict
40 40 from rhodecode.lib.utils2 import convert_line_endings, detect_mode, safe_str,\
41 41 str2bool
42 42 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
43 43 from rhodecode.lib.base import BaseRepoController, render
44 44 from rhodecode.lib.vcs.backends.base import EmptyChangeset
45 45 from rhodecode.lib.vcs.conf import settings
46 46 from rhodecode.lib.vcs.exceptions import RepositoryError, \
47 47 ChangesetDoesNotExistError, EmptyRepositoryError, \
48 48 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError,\
49 49 NodeDoesNotExistError, ChangesetError, NodeError
50 50 from rhodecode.lib.vcs.nodes import FileNode
51 51
52 52 from rhodecode.model.repo import RepoModel
53 53 from rhodecode.model.scm import ScmModel
54 54 from rhodecode.model.db import Repository
55 55
56 56 from rhodecode.controllers.changeset import anchor_url, _ignorews_url,\
57 57 _context_url, get_line_ctx, get_ignore_ws
58 58
59 59
60 60 log = logging.getLogger(__name__)
61 61
62 62
63 63 class FilesController(BaseRepoController):
64 64
65 65 def __before__(self):
66 66 super(FilesController, self).__before__()
67 67 c.cut_off_limit = self.cut_off_limit
68 68
69 69 def __get_cs_or_redirect(self, rev, repo_name, redirect_after=True):
70 70 """
71 71 Safe way to get changeset if error occur it redirects to tip with
72 72 proper message
73 73
74 74 :param rev: revision to fetch
75 75 :param repo_name: repo name to redirect after
76 76 """
77 77
78 78 try:
79 79 return c.rhodecode_repo.get_changeset(rev)
80 80 except EmptyRepositoryError, e:
81 81 if not redirect_after:
82 82 return None
83 83 url_ = url('files_add_home',
84 84 repo_name=c.repo_name,
85 85 revision=0, f_path='')
86 86 add_new = '<a href="%s">[%s]</a>' % (url_, _('click here to add new file'))
87 87 h.flash(h.literal(_('There are no files yet %s') % add_new),
88 88 category='warning')
89 89 redirect(h.url('summary_home', repo_name=repo_name))
90 90
91 91 except RepositoryError, e:
92 92 h.flash(str(e), category='warning')
93 93 redirect(h.url('files_home', repo_name=repo_name, revision='tip'))
94 94
95 95 def __get_filenode_or_redirect(self, repo_name, cs, path):
96 96 """
97 97 Returns file_node, if error occurs or given path is directory,
98 98 it'll redirect to top level path
99 99
100 100 :param repo_name: repo_name
101 101 :param cs: given changeset
102 102 :param path: path to lookup
103 103 """
104 104
105 105 try:
106 106 file_node = cs.get_node(path)
107 107 if file_node.is_dir():
108 108 raise RepositoryError('given path is a directory')
109 109 except RepositoryError, e:
110 110 h.flash(str(e), category='warning')
111 111 redirect(h.url('files_home', repo_name=repo_name,
112 112 revision=cs.raw_id))
113 113
114 114 return file_node
115 115
116 116 @LoginRequired()
117 117 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
118 118 'repository.admin')
119 119 def index(self, repo_name, revision, f_path, annotate=False):
120 120 # redirect to given revision from form if given
121 121 post_revision = request.POST.get('at_rev', None)
122 122 if post_revision:
123 123 cs = self.__get_cs_or_redirect(post_revision, repo_name)
124 124 redirect(url('files_home', repo_name=c.repo_name,
125 125 revision=cs.raw_id, f_path=f_path))
126 126
127 127 c.changeset = self.__get_cs_or_redirect(revision, repo_name)
128 128 c.branch = request.GET.get('branch', None)
129 129 c.f_path = f_path
130 130 c.annotate = annotate
131 131 cur_rev = c.changeset.revision
132 132
133 133 # prev link
134 134 try:
135 135 prev_rev = c.rhodecode_repo.get_changeset(cur_rev).prev(c.branch)
136 136 c.url_prev = url('files_home', repo_name=c.repo_name,
137 137 revision=prev_rev.raw_id, f_path=f_path)
138 138 if c.branch:
139 139 c.url_prev += '?branch=%s' % c.branch
140 140 except (ChangesetDoesNotExistError, VCSError):
141 141 c.url_prev = '#'
142 142
143 143 # next link
144 144 try:
145 145 next_rev = c.rhodecode_repo.get_changeset(cur_rev).next(c.branch)
146 146 c.url_next = url('files_home', repo_name=c.repo_name,
147 147 revision=next_rev.raw_id, f_path=f_path)
148 148 if c.branch:
149 149 c.url_next += '?branch=%s' % c.branch
150 150 except (ChangesetDoesNotExistError, VCSError):
151 151 c.url_next = '#'
152 152
153 153 # files or dirs
154 154 try:
155 155 c.file = c.changeset.get_node(f_path)
156 156
157 157 if c.file.is_file():
158 158 c.load_full_history = False
159 159 file_last_cs = c.file.last_changeset
160 160 c.file_changeset = (c.changeset
161 161 if c.changeset.revision < file_last_cs.revision
162 162 else file_last_cs)
163 163 _hist = []
164 164 c.file_history = []
165 165 if c.load_full_history:
166 166 c.file_history, _hist = self._get_node_history(c.changeset, f_path)
167 167
168 168 c.authors = []
169 169 for a in set([x.author for x in _hist]):
170 170 c.authors.append((h.email(a), h.person(a)))
171 171 else:
172 172 c.authors = c.file_history = []
173 173 except RepositoryError, e:
174 174 h.flash(str(e), category='warning')
175 175 redirect(h.url('files_home', repo_name=repo_name,
176 176 revision='tip'))
177 177
178 178 if request.environ.get('HTTP_X_PARTIAL_XHR'):
179 179 return render('files/files_ypjax.html')
180 180
181 181 return render('files/files.html')
182 182
183 183 def history(self, repo_name, revision, f_path, annotate=False):
184 184 if request.environ.get('HTTP_X_PARTIAL_XHR'):
185 185 c.changeset = self.__get_cs_or_redirect(revision, repo_name)
186 186 c.f_path = f_path
187 187 c.annotate = annotate
188 188 c.file = c.changeset.get_node(f_path)
189 189 if c.file.is_file():
190 190 file_last_cs = c.file.last_changeset
191 191 c.file_changeset = (c.changeset
192 192 if c.changeset.revision < file_last_cs.revision
193 193 else file_last_cs)
194 194 c.file_history, _hist = self._get_node_history(c.changeset, f_path)
195 195 c.authors = []
196 196 for a in set([x.author for x in _hist]):
197 197 c.authors.append((h.email(a), h.person(a)))
198 198 return render('files/files_history_box.html')
199 199
200 200 @LoginRequired()
201 201 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
202 202 'repository.admin')
203 203 def rawfile(self, repo_name, revision, f_path):
204 204 cs = self.__get_cs_or_redirect(revision, repo_name)
205 205 file_node = self.__get_filenode_or_redirect(repo_name, cs, f_path)
206 206
207 207 response.content_disposition = 'attachment; filename=%s' % \
208 208 safe_str(f_path.split(Repository.url_sep())[-1])
209 209
210 210 response.content_type = file_node.mimetype
211 211 return file_node.content
212 212
213 213 @LoginRequired()
214 214 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
215 215 'repository.admin')
216 216 def raw(self, repo_name, revision, f_path):
217 217 cs = self.__get_cs_or_redirect(revision, repo_name)
218 218 file_node = self.__get_filenode_or_redirect(repo_name, cs, f_path)
219 219
220 220 raw_mimetype_mapping = {
221 221 # map original mimetype to a mimetype used for "show as raw"
222 222 # you can also provide a content-disposition to override the
223 223 # default "attachment" disposition.
224 224 # orig_type: (new_type, new_dispo)
225 225
226 226 # show images inline:
227 227 'image/x-icon': ('image/x-icon', 'inline'),
228 228 'image/png': ('image/png', 'inline'),
229 229 'image/gif': ('image/gif', 'inline'),
230 230 'image/jpeg': ('image/jpeg', 'inline'),
231 231 'image/svg+xml': ('image/svg+xml', 'inline'),
232 232 }
233 233
234 234 mimetype = file_node.mimetype
235 235 try:
236 236 mimetype, dispo = raw_mimetype_mapping[mimetype]
237 237 except KeyError:
238 238 # we don't know anything special about this, handle it safely
239 239 if file_node.is_binary:
240 240 # do same as download raw for binary files
241 241 mimetype, dispo = 'application/octet-stream', 'attachment'
242 242 else:
243 243 # do not just use the original mimetype, but force text/plain,
244 244 # otherwise it would serve text/html and that might be unsafe.
245 245 # Note: underlying vcs library fakes text/plain mimetype if the
246 246 # mimetype can not be determined and it thinks it is not
247 247 # binary.This might lead to erroneous text display in some
248 248 # cases, but helps in other cases, like with text files
249 249 # without extension.
250 250 mimetype, dispo = 'text/plain', 'inline'
251 251
252 252 if dispo == 'attachment':
253 253 dispo = 'attachment; filename=%s' % \
254 254 safe_str(f_path.split(os.sep)[-1])
255 255
256 256 response.content_disposition = dispo
257 257 response.content_type = mimetype
258 258 return file_node.content
259 259
260 260 @LoginRequired()
261 261 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
262 262 def edit(self, repo_name, revision, f_path):
263 263 repo = Repository.get_by_repo_name(repo_name)
264 264 if repo.enable_locking and repo.locked[0]:
265 265 h.flash(_('This repository is has been locked by %s on %s')
266 266 % (h.person_by_id(repo.locked[0]),
267 267 h.fmt_date(h.time_to_datetime(repo.locked[1]))),
268 268 'warning')
269 269 return redirect(h.url('files_home',
270 270 repo_name=repo_name, revision='tip'))
271 271
272 272 r_post = request.POST
273 273
274 274 c.cs = self.__get_cs_or_redirect(revision, repo_name)
275 275 c.file = self.__get_filenode_or_redirect(repo_name, c.cs, f_path)
276 276
277 277 if c.file.is_binary:
278 278 return redirect(url('files_home', repo_name=c.repo_name,
279 279 revision=c.cs.raw_id, f_path=f_path))
280 280
281 281 c.f_path = f_path
282 282
283 283 if r_post:
284 284
285 285 old_content = c.file.content
286 286 sl = old_content.splitlines(1)
287 287 first_line = sl[0] if sl else ''
288 288 # modes: 0 - Unix, 1 - Mac, 2 - DOS
289 289 mode = detect_mode(first_line, 0)
290 290 content = convert_line_endings(r_post.get('content'), mode)
291 291
292 292 message = r_post.get('message') or (_('Edited %s via RhodeCode')
293 293 % (f_path))
294 294 author = self.rhodecode_user.full_contact
295 295
296 296 if content == old_content:
297 297 h.flash(_('No changes'),
298 298 category='warning')
299 299 return redirect(url('changeset_home', repo_name=c.repo_name,
300 300 revision='tip'))
301 301
302 302 try:
303 303 self.scm_model.commit_change(repo=c.rhodecode_repo,
304 304 repo_name=repo_name, cs=c.cs,
305 305 user=self.rhodecode_user,
306 306 author=author, message=message,
307 307 content=content, f_path=f_path)
308 308 h.flash(_('Successfully committed to %s') % f_path,
309 309 category='success')
310 310
311 311 except Exception:
312 312 log.error(traceback.format_exc())
313 313 h.flash(_('Error occurred during commit'), category='error')
314 314 return redirect(url('changeset_home',
315 315 repo_name=c.repo_name, revision='tip'))
316 316
317 317 return render('files/files_edit.html')
318 318
319 319 @LoginRequired()
320 320 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
321 321 def add(self, repo_name, revision, f_path):
322 322
323 323 repo = Repository.get_by_repo_name(repo_name)
324 324 if repo.enable_locking and repo.locked[0]:
325 325 h.flash(_('This repository is has been locked by %s on %s')
326 326 % (h.person_by_id(repo.locked[0]),
327 327 h.fmt_date(h.time_to_datetime(repo.locked[1]))),
328 328 'warning')
329 329 return redirect(h.url('files_home',
330 330 repo_name=repo_name, revision='tip'))
331 331
332 332 r_post = request.POST
333 333 c.cs = self.__get_cs_or_redirect(revision, repo_name,
334 334 redirect_after=False)
335 335 if c.cs is None:
336 336 c.cs = EmptyChangeset(alias=c.rhodecode_repo.alias)
337 337
338 338 c.f_path = f_path
339 339
340 340 if r_post:
341 341 unix_mode = 0
342 342 content = convert_line_endings(r_post.get('content'), unix_mode)
343 343
344 344 message = r_post.get('message') or (_('Added %s via RhodeCode')
345 345 % (f_path))
346 346 location = r_post.get('location')
347 347 filename = r_post.get('filename')
348 348 file_obj = r_post.get('upload_file', None)
349 349
350 350 if file_obj is not None and hasattr(file_obj, 'filename'):
351 351 filename = file_obj.filename
352 352 content = file_obj.file
353 353
354 354 node_path = os.path.join(location, filename)
355 355 author = self.rhodecode_user.full_contact
356 356
357 357 if not content:
358 358 h.flash(_('No content'), category='warning')
359 359 return redirect(url('changeset_home', repo_name=c.repo_name,
360 360 revision='tip'))
361 361 if not filename:
362 362 h.flash(_('No filename'), category='warning')
363 363 return redirect(url('changeset_home', repo_name=c.repo_name,
364 364 revision='tip'))
365 365
366 366 try:
367 367 self.scm_model.create_node(repo=c.rhodecode_repo,
368 368 repo_name=repo_name, cs=c.cs,
369 369 user=self.rhodecode_user,
370 370 author=author, message=message,
371 371 content=content, f_path=node_path)
372 372 h.flash(_('Successfully committed to %s') % node_path,
373 373 category='success')
374 374 except NodeAlreadyExistsError, e:
375 375 h.flash(_(e), category='error')
376 376 except Exception:
377 377 log.error(traceback.format_exc())
378 378 h.flash(_('Error occurred during commit'), category='error')
379 379 return redirect(url('changeset_home',
380 380 repo_name=c.repo_name, revision='tip'))
381 381
382 382 return render('files/files_add.html')
383 383
384 384 @LoginRequired()
385 385 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
386 386 'repository.admin')
387 387 def archivefile(self, repo_name, fname):
388 388
389 389 fileformat = None
390 390 revision = None
391 391 ext = None
392 392 subrepos = request.GET.get('subrepos') == 'true'
393 393
394 394 for a_type, ext_data in settings.ARCHIVE_SPECS.items():
395 395 archive_spec = fname.split(ext_data[1])
396 396 if len(archive_spec) == 2 and archive_spec[1] == '':
397 397 fileformat = a_type or ext_data[1]
398 398 revision = archive_spec[0]
399 399 ext = ext_data[1]
400 400
401 401 try:
402 402 dbrepo = RepoModel().get_by_repo_name(repo_name)
403 403 if dbrepo.enable_downloads is False:
404 404 return _('downloads disabled')
405 405
406 406 if c.rhodecode_repo.alias == 'hg':
407 407 # patch and reset hooks section of UI config to not run any
408 408 # hooks on fetching archives with subrepos
409 409 for k, v in c.rhodecode_repo._repo.ui.configitems('hooks'):
410 410 c.rhodecode_repo._repo.ui.setconfig('hooks', k, None)
411 411
412 412 cs = c.rhodecode_repo.get_changeset(revision)
413 413 content_type = settings.ARCHIVE_SPECS[fileformat][0]
414 414 except ChangesetDoesNotExistError:
415 415 return _('Unknown revision %s') % revision
416 416 except EmptyRepositoryError:
417 417 return _('Empty repository')
418 418 except (ImproperArchiveTypeError, KeyError):
419 419 return _('Unknown archive type')
420 420
421 421 fd, archive = tempfile.mkstemp()
422 422 t = open(archive, 'wb')
423 423 cs.fill_archive(stream=t, kind=fileformat, subrepos=subrepos)
424 424 t.close()
425 425
426 426 def get_chunked_archive(archive):
427 427 stream = open(archive, 'rb')
428 428 while True:
429 429 data = stream.read(16 * 1024)
430 430 if not data:
431 431 stream.close()
432 432 os.close(fd)
433 433 os.remove(archive)
434 434 break
435 435 yield data
436 436
437 437 response.content_disposition = str('attachment; filename=%s-%s%s' \
438 438 % (repo_name, revision[:12], ext))
439 439 response.content_type = str(content_type)
440 440 return get_chunked_archive(archive)
441 441
442 442 @LoginRequired()
443 443 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
444 444 'repository.admin')
445 445 def diff(self, repo_name, f_path):
446 446 ignore_whitespace = request.GET.get('ignorews') == '1'
447 447 line_context = request.GET.get('context', 3)
448 448 diff1 = request.GET.get('diff1', '')
449 449 diff2 = request.GET.get('diff2', '')
450 450 c.action = request.GET.get('diff')
451 451 c.no_changes = diff1 == diff2
452 452 c.f_path = f_path
453 453 c.big_diff = False
454 454 c.anchor_url = anchor_url
455 455 c.ignorews_url = _ignorews_url
456 456 c.context_url = _context_url
457 457 c.changes = OrderedDict()
458 458 c.changes[diff2] = []
459 459
460 460 #special case if we want a show rev only, it's impl here
461 461 #to reduce JS and callbacks
462 462
463 463 if request.GET.get('show_rev'):
464 464 if str2bool(request.GET.get('annotate', 'False')):
465 465 _url = url('files_annotate_home', repo_name=c.repo_name,
466 466 revision=diff1, f_path=c.f_path)
467 467 else:
468 468 _url = url('files_home', repo_name=c.repo_name,
469 469 revision=diff1, f_path=c.f_path)
470 470
471 471 return redirect(_url)
472 472 try:
473 473 if diff1 not in ['', None, 'None', '0' * 12, '0' * 40]:
474 474 c.changeset_1 = c.rhodecode_repo.get_changeset(diff1)
475 475 try:
476 476 node1 = c.changeset_1.get_node(f_path)
477 477 except NodeDoesNotExistError:
478 478 c.changeset_1 = EmptyChangeset(cs=diff1,
479 479 revision=c.changeset_1.revision,
480 480 repo=c.rhodecode_repo)
481 481 node1 = FileNode(f_path, '', changeset=c.changeset_1)
482 482 else:
483 483 c.changeset_1 = EmptyChangeset(repo=c.rhodecode_repo)
484 484 node1 = FileNode(f_path, '', changeset=c.changeset_1)
485 485
486 486 if diff2 not in ['', None, 'None', '0' * 12, '0' * 40]:
487 487 c.changeset_2 = c.rhodecode_repo.get_changeset(diff2)
488 488 try:
489 489 node2 = c.changeset_2.get_node(f_path)
490 490 except NodeDoesNotExistError:
491 491 c.changeset_2 = EmptyChangeset(cs=diff2,
492 492 revision=c.changeset_2.revision,
493 493 repo=c.rhodecode_repo)
494 494 node2 = FileNode(f_path, '', changeset=c.changeset_2)
495 495 else:
496 496 c.changeset_2 = EmptyChangeset(repo=c.rhodecode_repo)
497 497 node2 = FileNode(f_path, '', changeset=c.changeset_2)
498 498 except (RepositoryError, NodeError):
499 499 log.error(traceback.format_exc())
500 500 return redirect(url('files_home', repo_name=c.repo_name,
501 501 f_path=f_path))
502 502
503 503 if c.action == 'download':
504 504 _diff = diffs.get_gitdiff(node1, node2,
505 505 ignore_whitespace=ignore_whitespace,
506 506 context=line_context)
507 507 diff = diffs.DiffProcessor(_diff, format='gitdiff')
508 508
509 509 diff_name = '%s_vs_%s.diff' % (diff1, diff2)
510 510 response.content_type = 'text/plain'
511 511 response.content_disposition = (
512 512 'attachment; filename=%s' % diff_name
513 513 )
514 514 return diff.as_raw()
515 515
516 516 elif c.action == 'raw':
517 517 _diff = diffs.get_gitdiff(node1, node2,
518 518 ignore_whitespace=ignore_whitespace,
519 519 context=line_context)
520 520 diff = diffs.DiffProcessor(_diff, format='gitdiff')
521 521 response.content_type = 'text/plain'
522 522 return diff.as_raw()
523 523
524 524 else:
525 525 fid = h.FID(diff2, node2.path)
526 526 line_context_lcl = get_line_ctx(fid, request.GET)
527 527 ign_whitespace_lcl = get_ignore_ws(fid, request.GET)
528 528
529 529 lim = request.GET.get('fulldiff') or self.cut_off_limit
530 530 _, cs1, cs2, diff, st = diffs.wrapped_diff(filenode_old=node1,
531 531 filenode_new=node2,
532 532 cut_off_limit=lim,
533 533 ignore_whitespace=ign_whitespace_lcl,
534 534 line_context=line_context_lcl,
535 535 enable_comments=False)
536 536 op = ''
537 537 filename = node1.path
538 538 cs_changes = {
539 539 'fid': [cs1, cs2, op, filename, diff, st]
540 540 }
541 541 c.changes = cs_changes
542 542
543 543 return render('files/file_diff.html')
544 544
545 545 def _get_node_history(self, cs, f_path, changesets=None):
546 546 """
547 547 get changesets history for given node
548 548
549 549 :param cs: changeset to calculate history
550 550 :param f_path: path for node to calculate history for
551 551 :param changesets: if passed don't calculate history and take
552 552 changesets defined in this list
553 553 """
554 554 # calculate history based on tip
555 555 tip_cs = c.rhodecode_repo.get_changeset()
556 556 if changesets is None:
557 557 try:
558 558 changesets = tip_cs.get_file_history(f_path)
559 559 except (NodeDoesNotExistError, ChangesetError):
560 560 #this node is not present at tip !
561 561 changesets = cs.get_file_history(f_path)
562 562 hist_l = []
563 563
564 564 changesets_group = ([], _("Changesets"))
565 565 branches_group = ([], _("Branches"))
566 566 tags_group = ([], _("Tags"))
567 567 _hg = cs.repository.alias == 'hg'
568 568 for chs in changesets:
569 569 #_branch = '(%s)' % chs.branch if _hg else ''
570 570 _branch = chs.branch
571 571 n_desc = 'r%s:%s (%s)' % (chs.revision, chs.short_id, _branch)
572 572 changesets_group[0].append((chs.raw_id, n_desc,))
573 573 hist_l.append(changesets_group)
574 574
575 575 for name, chs in c.rhodecode_repo.branches.items():
576 576 branches_group[0].append((chs, name),)
577 577 hist_l.append(branches_group)
578 578
579 579 for name, chs in c.rhodecode_repo.tags.items():
580 580 tags_group[0].append((chs, name),)
581 581 hist_l.append(tags_group)
582 582
583 583 return hist_l, changesets
584 584
585 585 @LoginRequired()
586 586 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
587 587 'repository.admin')
588 588 @jsonify
589 589 def nodelist(self, repo_name, revision, f_path):
590 590 if request.environ.get('HTTP_X_PARTIAL_XHR'):
591 591 cs = self.__get_cs_or_redirect(revision, repo_name)
592 592 _d, _f = ScmModel().get_nodes(repo_name, cs.raw_id, f_path,
593 593 flat=False)
594 594 return {'nodes': _d + _f}
@@ -1,474 +1,472
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.pullrequests
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 pull requests controller for rhodecode for initializing pull requests
7 7
8 8 :created_on: May 7, 2012
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25 import logging
26 26 import traceback
27 27 import formencode
28 28
29 29 from webob.exc import HTTPNotFound, HTTPForbidden
30 30 from collections import defaultdict
31 31 from itertools import groupby
32 32
33 33 from pylons import request, response, session, tmpl_context as c, url
34 34 from pylons.controllers.util import abort, redirect
35 35 from pylons.i18n.translation import _
36 from pylons.decorators import jsonify
37 36
38 37 from rhodecode.lib.compat import json
39 38 from rhodecode.lib.base import BaseRepoController, render
40 39 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator,\
41 40 NotAnonymous
42 41 from rhodecode.lib import helpers as h
43 42 from rhodecode.lib import diffs
44 from rhodecode.lib.utils import action_logger
43 from rhodecode.lib.utils import action_logger, jsonify
44 from rhodecode.lib.vcs.exceptions import EmptyRepositoryError
45 from rhodecode.lib.vcs.backends.base import EmptyChangeset
46 from rhodecode.lib.diffs import LimitedDiffContainer
45 47 from rhodecode.model.db import User, PullRequest, ChangesetStatus,\
46 48 ChangesetComment
47 49 from rhodecode.model.pull_request import PullRequestModel
48 50 from rhodecode.model.meta import Session
49 51 from rhodecode.model.repo import RepoModel
50 52 from rhodecode.model.comment import ChangesetCommentsModel
51 53 from rhodecode.model.changeset_status import ChangesetStatusModel
52 54 from rhodecode.model.forms import PullRequestForm
53 from rhodecode.lib.vcs.exceptions import EmptyRepositoryError
54 from rhodecode.lib.vcs.backends.base import EmptyChangeset
55 from rhodecode.lib.diffs import LimitedDiffContainer
56 from rhodecode.lib.utils2 import str2bool
57 55
58 56 log = logging.getLogger(__name__)
59 57
60 58
61 59 class PullrequestsController(BaseRepoController):
62 60
63 61 @LoginRequired()
64 62 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
65 63 'repository.admin')
66 64 def __before__(self):
67 65 super(PullrequestsController, self).__before__()
68 66 repo_model = RepoModel()
69 67 c.users_array = repo_model.get_users_js()
70 68 c.users_groups_array = repo_model.get_users_groups_js()
71 69
72 70 def _get_repo_refs(self, repo):
73 71 hist_l = []
74 72
75 73 branches_group = ([('branch:%s:%s' % (k, v), k) for
76 74 k, v in repo.branches.iteritems()], _("Branches"))
77 75 bookmarks_group = ([('book:%s:%s' % (k, v), k) for
78 76 k, v in repo.bookmarks.iteritems()], _("Bookmarks"))
79 77 tags_group = ([('tag:%s:%s' % (k, v), k) for
80 78 k, v in repo.tags.iteritems()], _("Tags"))
81 79
82 80 hist_l.append(bookmarks_group)
83 81 hist_l.append(branches_group)
84 82 hist_l.append(tags_group)
85 83
86 84 return hist_l
87 85
88 86 def _get_default_rev(self, repo):
89 87 """
90 88 Get's default revision to do compare on pull request
91 89
92 90 :param repo:
93 91 """
94 92 repo = repo.scm_instance
95 93 if 'default' in repo.branches:
96 94 return 'default'
97 95 else:
98 96 #if repo doesn't have default branch return first found
99 97 return repo.branches.keys()[0]
100 98
101 99 def show_all(self, repo_name):
102 100 c.pull_requests = PullRequestModel().get_all(repo_name)
103 101 c.repo_name = repo_name
104 102 return render('/pullrequests/pullrequest_show_all.html')
105 103
106 104 @NotAnonymous()
107 105 def index(self):
108 106 org_repo = c.rhodecode_db_repo
109 107
110 108 if org_repo.scm_instance.alias != 'hg':
111 109 log.error('Review not available for GIT REPOS')
112 110 raise HTTPNotFound
113 111
114 112 try:
115 113 org_repo.scm_instance.get_changeset()
116 114 except EmptyRepositoryError, e:
117 115 h.flash(h.literal(_('There are no changesets yet')),
118 116 category='warning')
119 117 redirect(url('summary_home', repo_name=org_repo.repo_name))
120 118
121 119 other_repos_info = {}
122 120
123 121 c.org_refs = self._get_repo_refs(c.rhodecode_repo)
124 122 c.org_repos = []
125 123 c.other_repos = []
126 124 c.org_repos.append((org_repo.repo_name, '%s/%s' % (
127 125 org_repo.user.username, c.repo_name))
128 126 )
129 127
130 128 # add org repo to other so we can open pull request agains itself
131 129 c.other_repos.extend(c.org_repos)
132 130
133 131 c.default_pull_request = org_repo.repo_name # repo name pre-selected
134 132 c.default_pull_request_rev = self._get_default_rev(org_repo) # revision pre-selected
135 133 c.default_revs = self._get_repo_refs(org_repo.scm_instance)
136 134 #add orginal repo
137 135 other_repos_info[org_repo.repo_name] = {
138 136 'gravatar': h.gravatar_url(org_repo.user.email, 24),
139 137 'description': org_repo.description,
140 138 'revs': h.select('other_ref', '', c.default_revs, class_='refs')
141 139 }
142 140
143 141 #gather forks and add to this list
144 142 for fork in org_repo.forks:
145 143 c.other_repos.append((fork.repo_name, '%s/%s' % (
146 144 fork.user.username, fork.repo_name))
147 145 )
148 146 other_repos_info[fork.repo_name] = {
149 147 'gravatar': h.gravatar_url(fork.user.email, 24),
150 148 'description': fork.description,
151 149 'revs': h.select('other_ref', '',
152 150 self._get_repo_refs(fork.scm_instance),
153 151 class_='refs')
154 152 }
155 153 #add parents of this fork also, but only if it's not empty
156 154 if org_repo.parent and org_repo.parent.scm_instance.revisions:
157 155 c.default_pull_request = org_repo.parent.repo_name
158 156 c.default_pull_request_rev = self._get_default_rev(org_repo.parent)
159 157 c.default_revs = self._get_repo_refs(org_repo.parent.scm_instance)
160 158 c.other_repos.append((org_repo.parent.repo_name, '%s/%s' % (
161 159 org_repo.parent.user.username,
162 160 org_repo.parent.repo_name))
163 161 )
164 162 other_repos_info[org_repo.parent.repo_name] = {
165 163 'gravatar': h.gravatar_url(org_repo.parent.user.email, 24),
166 164 'description': org_repo.parent.description,
167 165 'revs': h.select('other_ref', '',
168 166 self._get_repo_refs(org_repo.parent.scm_instance),
169 167 class_='refs')
170 168 }
171 169
172 170 c.other_repos_info = json.dumps(other_repos_info)
173 171 c.review_members = [org_repo.user]
174 172 return render('/pullrequests/pullrequest.html')
175 173
176 174 @NotAnonymous()
177 175 def create(self, repo_name):
178 176 repo = RepoModel()._get_repo(repo_name)
179 177 try:
180 178 _form = PullRequestForm(repo.repo_id)().to_python(request.POST)
181 179 except formencode.Invalid, errors:
182 180 log.error(traceback.format_exc())
183 181 if errors.error_dict.get('revisions'):
184 182 msg = 'Revisions: %s' % errors.error_dict['revisions']
185 183 elif errors.error_dict.get('pullrequest_title'):
186 184 msg = _('Pull request requires a title with min. 3 chars')
187 185 else:
188 186 msg = _('error during creation of pull request')
189 187
190 188 h.flash(msg, 'error')
191 189 return redirect(url('pullrequest_home', repo_name=repo_name))
192 190
193 191 org_repo = _form['org_repo']
194 192 org_ref = _form['org_ref']
195 193 other_repo = _form['other_repo']
196 194 other_ref = _form['other_ref']
197 195 revisions = _form['revisions']
198 196 reviewers = _form['review_members']
199 197
200 198 # if we have cherry picked pull request we don't care what is in
201 199 # org_ref/other_ref
202 200 rev_start = request.POST.get('rev_start')
203 201 rev_end = request.POST.get('rev_end')
204 202
205 203 if rev_start and rev_end:
206 204 # this is swapped to simulate that rev_end is a revision from
207 205 # parent of the fork
208 206 org_ref = 'rev:%s:%s' % (rev_end, rev_end)
209 207 other_ref = 'rev:%s:%s' % (rev_start, rev_start)
210 208
211 209 title = _form['pullrequest_title']
212 210 description = _form['pullrequest_desc']
213 211
214 212 try:
215 213 pull_request = PullRequestModel().create(
216 214 self.rhodecode_user.user_id, org_repo, org_ref, other_repo,
217 215 other_ref, revisions, reviewers, title, description
218 216 )
219 217 Session().commit()
220 218 h.flash(_('Successfully opened new pull request'),
221 219 category='success')
222 220 except Exception:
223 221 h.flash(_('Error occurred during sending pull request'),
224 222 category='error')
225 223 log.error(traceback.format_exc())
226 224 return redirect(url('pullrequest_home', repo_name=repo_name))
227 225
228 226 return redirect(url('pullrequest_show', repo_name=other_repo,
229 227 pull_request_id=pull_request.pull_request_id))
230 228
231 229 @NotAnonymous()
232 230 @jsonify
233 231 def update(self, repo_name, pull_request_id):
234 232 pull_request = PullRequest.get_or_404(pull_request_id)
235 233 if pull_request.is_closed():
236 234 raise HTTPForbidden()
237 235 #only owner or admin can update it
238 236 owner = pull_request.author.user_id == c.rhodecode_user.user_id
239 237 if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
240 238 reviewers_ids = map(int, filter(lambda v: v not in [None, ''],
241 239 request.POST.get('reviewers_ids', '').split(',')))
242 240
243 241 PullRequestModel().update_reviewers(pull_request_id, reviewers_ids)
244 242 Session().commit()
245 243 return True
246 244 raise HTTPForbidden()
247 245
248 246 @NotAnonymous()
249 247 @jsonify
250 248 def delete(self, repo_name, pull_request_id):
251 249 pull_request = PullRequest.get_or_404(pull_request_id)
252 250 #only owner can delete it !
253 251 if pull_request.author.user_id == c.rhodecode_user.user_id:
254 252 PullRequestModel().delete(pull_request)
255 253 Session().commit()
256 254 h.flash(_('Successfully deleted pull request'),
257 255 category='success')
258 256 return redirect(url('admin_settings_my_account', anchor='pullrequests'))
259 257 raise HTTPForbidden()
260 258
261 259 def _load_compare_data(self, pull_request, enable_comments=True):
262 260 """
263 261 Load context data needed for generating compare diff
264 262
265 263 :param pull_request:
266 264 :type pull_request:
267 265 """
268 266 rev_start = request.GET.get('rev_start')
269 267 rev_end = request.GET.get('rev_end')
270 268
271 269 org_repo = pull_request.org_repo
272 270 (org_ref_type,
273 271 org_ref_name,
274 272 org_ref_rev) = pull_request.org_ref.split(':')
275 273
276 274 other_repo = org_repo
277 275 (other_ref_type,
278 276 other_ref_name,
279 277 other_ref_rev) = pull_request.other_ref.split(':')
280 278
281 279 # despite opening revisions for bookmarks/branches/tags, we always
282 280 # convert this to rev to prevent changes after book or branch change
283 281 org_ref = ('rev', org_ref_rev)
284 282 other_ref = ('rev', other_ref_rev)
285 283
286 284 c.org_repo = org_repo
287 285 c.other_repo = other_repo
288 286
289 287 c.fulldiff = fulldiff = request.GET.get('fulldiff')
290 288
291 289 c.cs_ranges = [org_repo.get_changeset(x) for x in pull_request.revisions]
292 290
293 291 other_ref = ('rev', getattr(c.cs_ranges[0].parents[0]
294 292 if c.cs_ranges[0].parents
295 293 else EmptyChangeset(), 'raw_id'))
296 294
297 295 c.statuses = org_repo.statuses([x.raw_id for x in c.cs_ranges])
298 296 c.target_repo = c.repo_name
299 297 # defines that we need hidden inputs with changesets
300 298 c.as_form = request.GET.get('as_form', False)
301 299
302 300 c.org_ref = org_ref[1]
303 301 c.other_ref = other_ref[1]
304 302
305 303 diff_limit = self.cut_off_limit if not fulldiff else None
306 304
307 305 #we swap org/other ref since we run a simple diff on one repo
308 306 _diff = diffs.differ(org_repo, other_ref, other_repo, org_ref)
309 307
310 308 diff_processor = diffs.DiffProcessor(_diff or '', format='gitdiff',
311 309 diff_limit=diff_limit)
312 310 _parsed = diff_processor.prepare()
313 311
314 312 c.limited_diff = False
315 313 if isinstance(_parsed, LimitedDiffContainer):
316 314 c.limited_diff = True
317 315
318 316 c.files = []
319 317 c.changes = {}
320 318 c.lines_added = 0
321 319 c.lines_deleted = 0
322 320 for f in _parsed:
323 321 st = f['stats']
324 322 if st[0] != 'b':
325 323 c.lines_added += st[0]
326 324 c.lines_deleted += st[1]
327 325 fid = h.FID('', f['filename'])
328 326 c.files.append([fid, f['operation'], f['filename'], f['stats']])
329 327 diff = diff_processor.as_html(enable_comments=enable_comments,
330 328 parsed_lines=[f])
331 329 c.changes[fid] = [f['operation'], f['filename'], diff]
332 330
333 331 def show(self, repo_name, pull_request_id):
334 332 repo_model = RepoModel()
335 333 c.users_array = repo_model.get_users_js()
336 334 c.users_groups_array = repo_model.get_users_groups_js()
337 335 c.pull_request = PullRequest.get_or_404(pull_request_id)
338 336 c.target_repo = c.pull_request.org_repo.repo_name
339 337
340 338 cc_model = ChangesetCommentsModel()
341 339 cs_model = ChangesetStatusModel()
342 340 _cs_statuses = cs_model.get_statuses(c.pull_request.org_repo,
343 341 pull_request=c.pull_request,
344 342 with_revisions=True)
345 343
346 344 cs_statuses = defaultdict(list)
347 345 for st in _cs_statuses:
348 346 cs_statuses[st.author.username] += [st]
349 347
350 348 c.pull_request_reviewers = []
351 349 c.pull_request_pending_reviewers = []
352 350 for o in c.pull_request.reviewers:
353 351 st = cs_statuses.get(o.user.username, None)
354 352 if st:
355 353 sorter = lambda k: k.version
356 354 st = [(x, list(y)[0])
357 355 for x, y in (groupby(sorted(st, key=sorter), sorter))]
358 356 else:
359 357 c.pull_request_pending_reviewers.append(o.user)
360 358 c.pull_request_reviewers.append([o.user, st])
361 359
362 360 # pull_requests repo_name we opened it against
363 361 # ie. other_repo must match
364 362 if repo_name != c.pull_request.other_repo.repo_name:
365 363 raise HTTPNotFound
366 364
367 365 # load compare data into template context
368 366 enable_comments = not c.pull_request.is_closed()
369 367 self._load_compare_data(c.pull_request, enable_comments=enable_comments)
370 368
371 369 # inline comments
372 370 c.inline_cnt = 0
373 371 c.inline_comments = cc_model.get_inline_comments(
374 372 c.rhodecode_db_repo.repo_id,
375 373 pull_request=pull_request_id)
376 374 # count inline comments
377 375 for __, lines in c.inline_comments:
378 376 for comments in lines.values():
379 377 c.inline_cnt += len(comments)
380 378 # comments
381 379 c.comments = cc_model.get_comments(c.rhodecode_db_repo.repo_id,
382 380 pull_request=pull_request_id)
383 381
384 382 try:
385 383 cur_status = c.statuses[c.pull_request.revisions[0]][0]
386 384 except:
387 385 log.error(traceback.format_exc())
388 386 cur_status = 'undefined'
389 387 if c.pull_request.is_closed() and 0:
390 388 c.current_changeset_status = cur_status
391 389 else:
392 390 # changeset(pull-request) status calulation based on reviewers
393 391 c.current_changeset_status = cs_model.calculate_status(
394 392 c.pull_request_reviewers,
395 393 )
396 394 c.changeset_statuses = ChangesetStatus.STATUSES
397 395
398 396 return render('/pullrequests/pullrequest_show.html')
399 397
400 398 @NotAnonymous()
401 399 @jsonify
402 400 def comment(self, repo_name, pull_request_id):
403 401 pull_request = PullRequest.get_or_404(pull_request_id)
404 402 if pull_request.is_closed():
405 403 raise HTTPForbidden()
406 404
407 405 status = request.POST.get('changeset_status')
408 406 change_status = request.POST.get('change_changeset_status')
409 407 text = request.POST.get('text')
410 408 if status and change_status:
411 409 text = text or (_('Status change -> %s')
412 410 % ChangesetStatus.get_status_lbl(status))
413 411 comm = ChangesetCommentsModel().create(
414 412 text=text,
415 413 repo=c.rhodecode_db_repo.repo_id,
416 414 user=c.rhodecode_user.user_id,
417 415 pull_request=pull_request_id,
418 416 f_path=request.POST.get('f_path'),
419 417 line_no=request.POST.get('line'),
420 418 status_change=(ChangesetStatus.get_status_lbl(status)
421 419 if status and change_status else None)
422 420 )
423 421
424 422 # get status if set !
425 423 if status and change_status:
426 424 ChangesetStatusModel().set_status(
427 425 c.rhodecode_db_repo.repo_id,
428 426 status,
429 427 c.rhodecode_user.user_id,
430 428 comm,
431 429 pull_request=pull_request_id
432 430 )
433 431 action_logger(self.rhodecode_user,
434 432 'user_commented_pull_request:%s' % pull_request_id,
435 433 c.rhodecode_db_repo, self.ip_addr, self.sa)
436 434
437 435 if request.POST.get('save_close'):
438 436 PullRequestModel().close_pull_request(pull_request_id)
439 437 action_logger(self.rhodecode_user,
440 438 'user_closed_pull_request:%s' % pull_request_id,
441 439 c.rhodecode_db_repo, self.ip_addr, self.sa)
442 440
443 441 Session().commit()
444 442
445 443 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
446 444 return redirect(h.url('pullrequest_show', repo_name=repo_name,
447 445 pull_request_id=pull_request_id))
448 446
449 447 data = {
450 448 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
451 449 }
452 450 if comm:
453 451 c.co = comm
454 452 data.update(comm.get_dict())
455 453 data.update({'rendered_text':
456 454 render('changeset/changeset_comment_block.html')})
457 455
458 456 return data
459 457
460 458 @NotAnonymous()
461 459 @jsonify
462 460 def delete_comment(self, repo_name, comment_id):
463 461 co = ChangesetComment.get(comment_id)
464 462 if co.pull_request.is_closed():
465 463 #don't allow deleting comments on closed pull request
466 464 raise HTTPForbidden()
467 465
468 466 owner = lambda: co.author.user_id == c.rhodecode_user.user_id
469 467 if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
470 468 ChangesetCommentsModel().delete(comment=co)
471 469 Session().commit()
472 470 return True
473 471 else:
474 472 raise HTTPForbidden()
@@ -1,716 +1,742
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.utils
4 4 ~~~~~~~~~~~~~~~~~~~
5 5
6 6 Utilities library for RhodeCode
7 7
8 8 :created_on: Apr 18, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import os
27 27 import re
28 28 import logging
29 29 import datetime
30 30 import traceback
31 31 import paste
32 32 import beaker
33 33 import tarfile
34 34 import shutil
35 import decorator
36 import warnings
35 37 from os.path import abspath
36 38 from os.path import dirname as dn, join as jn
37 39
38 40 from paste.script.command import Command, BadCommand
39 41
40 42 from mercurial import ui, config
41 43
42 44 from webhelpers.text import collapse, remove_formatting, strip_tags
43 45
44 46 from rhodecode.lib.vcs import get_backend
45 47 from rhodecode.lib.vcs.backends.base import BaseChangeset
46 48 from rhodecode.lib.vcs.utils.lazy import LazyProperty
47 49 from rhodecode.lib.vcs.utils.helpers import get_scm
48 50 from rhodecode.lib.vcs.exceptions import VCSError
49 51
50 52 from rhodecode.lib.caching_query import FromCache
51 53
52 54 from rhodecode.model import meta
53 55 from rhodecode.model.db import Repository, User, RhodeCodeUi, \
54 56 UserLog, RepoGroup, RhodeCodeSetting, CacheInvalidation
55 57 from rhodecode.model.meta import Session
56 58 from rhodecode.model.repos_group import ReposGroupModel
57 59 from rhodecode.lib.utils2 import safe_str, safe_unicode
58 60 from rhodecode.lib.vcs.utils.fakemod import create_module
59 61
60 62 log = logging.getLogger(__name__)
61 63
62 64 REMOVED_REPO_PAT = re.compile(r'rm__\d{8}_\d{6}_\d{6}__.*')
63 65
64 66
65 67 def recursive_replace(str_, replace=' '):
66 68 """
67 69 Recursive replace of given sign to just one instance
68 70
69 71 :param str_: given string
70 72 :param replace: char to find and replace multiple instances
71 73
72 74 Examples::
73 75 >>> recursive_replace("Mighty---Mighty-Bo--sstones",'-')
74 76 'Mighty-Mighty-Bo-sstones'
75 77 """
76 78
77 79 if str_.find(replace * 2) == -1:
78 80 return str_
79 81 else:
80 82 str_ = str_.replace(replace * 2, replace)
81 83 return recursive_replace(str_, replace)
82 84
83 85
84 86 def repo_name_slug(value):
85 87 """
86 88 Return slug of name of repository
87 89 This function is called on each creation/modification
88 90 of repository to prevent bad names in repo
89 91 """
90 92
91 93 slug = remove_formatting(value)
92 94 slug = strip_tags(slug)
93 95
94 96 for c in """`?=[]\;'"<>,/~!@#$%^&*()+{}|: """:
95 97 slug = slug.replace(c, '-')
96 98 slug = recursive_replace(slug, '-')
97 99 slug = collapse(slug, '-')
98 100 return slug
99 101
100 102
101 103 def get_repo_slug(request):
102 104 _repo = request.environ['pylons.routes_dict'].get('repo_name')
103 105 if _repo:
104 106 _repo = _repo.rstrip('/')
105 107 return _repo
106 108
107 109
108 110 def get_repos_group_slug(request):
109 111 _group = request.environ['pylons.routes_dict'].get('group_name')
110 112 if _group:
111 113 _group = _group.rstrip('/')
112 114 return _group
113 115
114 116
115 117 def action_logger(user, action, repo, ipaddr='', sa=None, commit=False):
116 118 """
117 119 Action logger for various actions made by users
118 120
119 121 :param user: user that made this action, can be a unique username string or
120 122 object containing user_id attribute
121 123 :param action: action to log, should be on of predefined unique actions for
122 124 easy translations
123 125 :param repo: string name of repository or object containing repo_id,
124 126 that action was made on
125 127 :param ipaddr: optional ip address from what the action was made
126 128 :param sa: optional sqlalchemy session
127 129
128 130 """
129 131
130 132 if not sa:
131 133 sa = meta.Session()
132 134
133 135 try:
134 136 if hasattr(user, 'user_id'):
135 137 user_obj = user
136 138 elif isinstance(user, basestring):
137 139 user_obj = User.get_by_username(user)
138 140 else:
139 141 raise Exception('You have to provide a user object or a username')
140 142
141 143 if hasattr(repo, 'repo_id'):
142 144 repo_obj = Repository.get(repo.repo_id)
143 145 repo_name = repo_obj.repo_name
144 146 elif isinstance(repo, basestring):
145 147 repo_name = repo.lstrip('/')
146 148 repo_obj = Repository.get_by_repo_name(repo_name)
147 149 else:
148 150 repo_obj = None
149 151 repo_name = ''
150 152
151 153 user_log = UserLog()
152 154 user_log.user_id = user_obj.user_id
153 155 user_log.action = safe_unicode(action)
154 156
155 157 user_log.repository = repo_obj
156 158 user_log.repository_name = repo_name
157 159
158 160 user_log.action_date = datetime.datetime.now()
159 161 user_log.user_ip = ipaddr
160 162 sa.add(user_log)
161 163
162 164 log.info(
163 165 'Adding user %s, action %s on %s' % (user_obj, action,
164 166 safe_unicode(repo))
165 167 )
166 168 if commit:
167 169 sa.commit()
168 170 except:
169 171 log.error(traceback.format_exc())
170 172 raise
171 173
172 174
173 175 def get_repos(path, recursive=False):
174 176 """
175 177 Scans given path for repos and return (name,(type,path)) tuple
176 178
177 179 :param path: path to scan for repositories
178 180 :param recursive: recursive search and return names with subdirs in front
179 181 """
180 182
181 183 # remove ending slash for better results
182 184 path = path.rstrip(os.sep)
183 185
184 186 def _get_repos(p):
185 187 if not os.access(p, os.W_OK):
186 188 return
187 189 for dirpath in os.listdir(p):
188 190 if os.path.isfile(os.path.join(p, dirpath)):
189 191 continue
190 192 cur_path = os.path.join(p, dirpath)
191 193 try:
192 194 scm_info = get_scm(cur_path)
193 195 yield scm_info[1].split(path, 1)[-1].lstrip(os.sep), scm_info
194 196 except VCSError:
195 197 if not recursive:
196 198 continue
197 199 #check if this dir containts other repos for recursive scan
198 200 rec_path = os.path.join(p, dirpath)
199 201 if os.path.isdir(rec_path):
200 202 for inner_scm in _get_repos(rec_path):
201 203 yield inner_scm
202 204
203 205 return _get_repos(path)
204 206
205 207
206 208 def is_valid_repo(repo_name, base_path, scm=None):
207 209 """
208 210 Returns True if given path is a valid repository False otherwise.
209 211 If scm param is given also compare if given scm is the same as expected
210 212 from scm parameter
211 213
212 214 :param repo_name:
213 215 :param base_path:
214 216 :param scm:
215 217
216 218 :return True: if given path is a valid repository
217 219 """
218 220 full_path = os.path.join(safe_str(base_path), safe_str(repo_name))
219 221
220 222 try:
221 223 scm_ = get_scm(full_path)
222 224 if scm:
223 225 return scm_[0] == scm
224 226 return True
225 227 except VCSError:
226 228 return False
227 229
228 230
229 231 def is_valid_repos_group(repos_group_name, base_path):
230 232 """
231 233 Returns True if given path is a repos group False otherwise
232 234
233 235 :param repo_name:
234 236 :param base_path:
235 237 """
236 238 full_path = os.path.join(safe_str(base_path), safe_str(repos_group_name))
237 239
238 240 # check if it's not a repo
239 241 if is_valid_repo(repos_group_name, base_path):
240 242 return False
241 243
242 244 try:
243 245 # we need to check bare git repos at higher level
244 246 # since we might match branches/hooks/info/objects or possible
245 247 # other things inside bare git repo
246 248 get_scm(os.path.dirname(full_path))
247 249 return False
248 250 except VCSError:
249 251 pass
250 252
251 253 # check if it's a valid path
252 254 if os.path.isdir(full_path):
253 255 return True
254 256
255 257 return False
256 258
257 259
258 260 def ask_ok(prompt, retries=4, complaint='Yes or no please!'):
259 261 while True:
260 262 ok = raw_input(prompt)
261 263 if ok in ('y', 'ye', 'yes'):
262 264 return True
263 265 if ok in ('n', 'no', 'nop', 'nope'):
264 266 return False
265 267 retries = retries - 1
266 268 if retries < 0:
267 269 raise IOError
268 270 print complaint
269 271
270 272 #propagated from mercurial documentation
271 273 ui_sections = ['alias', 'auth',
272 274 'decode/encode', 'defaults',
273 275 'diff', 'email',
274 276 'extensions', 'format',
275 277 'merge-patterns', 'merge-tools',
276 278 'hooks', 'http_proxy',
277 279 'smtp', 'patch',
278 280 'paths', 'profiling',
279 281 'server', 'trusted',
280 282 'ui', 'web', ]
281 283
282 284
283 285 def make_ui(read_from='file', path=None, checkpaths=True, clear_session=True):
284 286 """
285 287 A function that will read python rc files or database
286 288 and make an mercurial ui object from read options
287 289
288 290 :param path: path to mercurial config file
289 291 :param checkpaths: check the path
290 292 :param read_from: read from 'file' or 'db'
291 293 """
292 294
293 295 baseui = ui.ui()
294 296
295 297 # clean the baseui object
296 298 baseui._ocfg = config.config()
297 299 baseui._ucfg = config.config()
298 300 baseui._tcfg = config.config()
299 301
300 302 if read_from == 'file':
301 303 if not os.path.isfile(path):
302 304 log.debug('hgrc file is not present at %s, skipping...' % path)
303 305 return False
304 306 log.debug('reading hgrc from %s' % path)
305 307 cfg = config.config()
306 308 cfg.read(path)
307 309 for section in ui_sections:
308 310 for k, v in cfg.items(section):
309 311 log.debug('settings ui from file[%s]%s:%s' % (section, k, v))
310 312 baseui.setconfig(safe_str(section), safe_str(k), safe_str(v))
311 313
312 314 elif read_from == 'db':
313 315 sa = meta.Session()
314 316 ret = sa.query(RhodeCodeUi)\
315 317 .options(FromCache("sql_cache_short", "get_hg_ui_settings"))\
316 318 .all()
317 319
318 320 hg_ui = ret
319 321 for ui_ in hg_ui:
320 322 if ui_.ui_active:
321 323 log.debug('settings ui from db[%s]%s:%s', ui_.ui_section,
322 324 ui_.ui_key, ui_.ui_value)
323 325 baseui.setconfig(safe_str(ui_.ui_section), safe_str(ui_.ui_key),
324 326 safe_str(ui_.ui_value))
325 327 if ui_.ui_key == 'push_ssl':
326 328 # force set push_ssl requirement to False, rhodecode
327 329 # handles that
328 330 baseui.setconfig(safe_str(ui_.ui_section), safe_str(ui_.ui_key),
329 331 False)
330 332 if clear_session:
331 333 meta.Session.remove()
332 334 return baseui
333 335
334 336
335 337 def set_rhodecode_config(config):
336 338 """
337 339 Updates pylons config with new settings from database
338 340
339 341 :param config:
340 342 """
341 343 hgsettings = RhodeCodeSetting.get_app_settings()
342 344
343 345 for k, v in hgsettings.items():
344 346 config[k] = v
345 347
346 348
347 349 def invalidate_cache(cache_key, *args):
348 350 """
349 351 Puts cache invalidation task into db for
350 352 further global cache invalidation
351 353 """
352 354
353 355 from rhodecode.model.scm import ScmModel
354 356
355 357 if cache_key.startswith('get_repo_cached_'):
356 358 name = cache_key.split('get_repo_cached_')[-1]
357 359 ScmModel().mark_for_invalidation(name)
358 360
359 361
360 362 def map_groups(path):
361 363 """
362 364 Given a full path to a repository, create all nested groups that this
363 365 repo is inside. This function creates parent-child relationships between
364 366 groups and creates default perms for all new groups.
365 367
366 368 :param paths: full path to repository
367 369 """
368 370 sa = meta.Session()
369 371 groups = path.split(Repository.url_sep())
370 372 parent = None
371 373 group = None
372 374
373 375 # last element is repo in nested groups structure
374 376 groups = groups[:-1]
375 377 rgm = ReposGroupModel(sa)
376 378 for lvl, group_name in enumerate(groups):
377 379 group_name = '/'.join(groups[:lvl] + [group_name])
378 380 group = RepoGroup.get_by_group_name(group_name)
379 381 desc = '%s group' % group_name
380 382
381 383 # skip folders that are now removed repos
382 384 if REMOVED_REPO_PAT.match(group_name):
383 385 break
384 386
385 387 if group is None:
386 388 log.debug('creating group level: %s group_name: %s' % (lvl,
387 389 group_name))
388 390 group = RepoGroup(group_name, parent)
389 391 group.group_description = desc
390 392 sa.add(group)
391 393 rgm._create_default_perms(group)
392 394 sa.flush()
393 395 parent = group
394 396 return group
395 397
396 398
397 399 def repo2db_mapper(initial_repo_list, remove_obsolete=False,
398 400 install_git_hook=False):
399 401 """
400 402 maps all repos given in initial_repo_list, non existing repositories
401 403 are created, if remove_obsolete is True it also check for db entries
402 404 that are not in initial_repo_list and removes them.
403 405
404 406 :param initial_repo_list: list of repositories found by scanning methods
405 407 :param remove_obsolete: check for obsolete entries in database
406 408 :param install_git_hook: if this is True, also check and install githook
407 409 for a repo if missing
408 410 """
409 411 from rhodecode.model.repo import RepoModel
410 412 from rhodecode.model.scm import ScmModel
411 413 sa = meta.Session()
412 414 rm = RepoModel()
413 415 user = sa.query(User).filter(User.admin == True).first()
414 416 if user is None:
415 417 raise Exception('Missing administrative account!')
416 418 added = []
417 419
418 420 # # clear cache keys
419 421 # log.debug("Clearing cache keys now...")
420 422 # CacheInvalidation.clear_cache()
421 423 # sa.commit()
422 424
423 425 for name, repo in initial_repo_list.items():
424 426 group = map_groups(name)
425 427 db_repo = rm.get_by_repo_name(name)
426 428 # found repo that is on filesystem not in RhodeCode database
427 429 if not db_repo:
428 430 log.info('repository %s not found, creating now' % name)
429 431 added.append(name)
430 432 desc = (repo.description
431 433 if repo.description != 'unknown'
432 434 else '%s repository' % name)
433 435 new_repo = rm.create_repo(
434 436 repo_name=name,
435 437 repo_type=repo.alias,
436 438 description=desc,
437 439 repos_group=getattr(group, 'group_id', None),
438 440 owner=user,
439 441 just_db=True
440 442 )
441 443 # we added that repo just now, and make sure it has githook
442 444 # installed
443 445 if new_repo.repo_type == 'git':
444 446 ScmModel().install_git_hook(new_repo.scm_instance)
445 447 elif install_git_hook:
446 448 if db_repo.repo_type == 'git':
447 449 ScmModel().install_git_hook(db_repo.scm_instance)
448 450 # during starting install all cache keys for all repositories in the
449 451 # system, this will register all repos and multiple instances
450 452 key, _prefix, _org_key = CacheInvalidation._get_key(name)
451 453 CacheInvalidation.invalidate(name)
452 454 log.debug("Creating a cache key for %s instance_id=>`%s`"
453 455 % (name, _prefix or '-'))
454 456
455 457 sa.commit()
456 458 removed = []
457 459 if remove_obsolete:
458 460 # remove from database those repositories that are not in the filesystem
459 461 for repo in sa.query(Repository).all():
460 462 if repo.repo_name not in initial_repo_list.keys():
461 463 log.debug("Removing non-existing repository found in db `%s`" %
462 464 repo.repo_name)
463 465 try:
464 466 sa.delete(repo)
465 467 sa.commit()
466 468 removed.append(repo.repo_name)
467 469 except:
468 470 #don't hold further removals on error
469 471 log.error(traceback.format_exc())
470 472 sa.rollback()
471 473
472 474 return added, removed
473 475
474 476
475 477 # set cache regions for beaker so celery can utilise it
476 478 def add_cache(settings):
477 479 cache_settings = {'regions': None}
478 480 for key in settings.keys():
479 481 for prefix in ['beaker.cache.', 'cache.']:
480 482 if key.startswith(prefix):
481 483 name = key.split(prefix)[1].strip()
482 484 cache_settings[name] = settings[key].strip()
483 485 if cache_settings['regions']:
484 486 for region in cache_settings['regions'].split(','):
485 487 region = region.strip()
486 488 region_settings = {}
487 489 for key, value in cache_settings.items():
488 490 if key.startswith(region):
489 491 region_settings[key.split('.')[1]] = value
490 492 region_settings['expire'] = int(region_settings.get('expire',
491 493 60))
492 494 region_settings.setdefault('lock_dir',
493 495 cache_settings.get('lock_dir'))
494 496 region_settings.setdefault('data_dir',
495 497 cache_settings.get('data_dir'))
496 498
497 499 if 'type' not in region_settings:
498 500 region_settings['type'] = cache_settings.get('type',
499 501 'memory')
500 502 beaker.cache.cache_regions[region] = region_settings
501 503
502 504
503 505 def load_rcextensions(root_path):
504 506 import rhodecode
505 507 from rhodecode.config import conf
506 508
507 509 path = os.path.join(root_path, 'rcextensions', '__init__.py')
508 510 if os.path.isfile(path):
509 511 rcext = create_module('rc', path)
510 512 EXT = rhodecode.EXTENSIONS = rcext
511 513 log.debug('Found rcextensions now loading %s...' % rcext)
512 514
513 515 # Additional mappings that are not present in the pygments lexers
514 516 conf.LANGUAGES_EXTENSIONS_MAP.update(getattr(EXT, 'EXTRA_MAPPINGS', {}))
515 517
516 518 #OVERRIDE OUR EXTENSIONS FROM RC-EXTENSIONS (if present)
517 519
518 520 if getattr(EXT, 'INDEX_EXTENSIONS', []) != []:
519 521 log.debug('settings custom INDEX_EXTENSIONS')
520 522 conf.INDEX_EXTENSIONS = getattr(EXT, 'INDEX_EXTENSIONS', [])
521 523
522 524 #ADDITIONAL MAPPINGS
523 525 log.debug('adding extra into INDEX_EXTENSIONS')
524 526 conf.INDEX_EXTENSIONS.extend(getattr(EXT, 'EXTRA_INDEX_EXTENSIONS', []))
525 527
526 528
527 529 #==============================================================================
528 530 # TEST FUNCTIONS AND CREATORS
529 531 #==============================================================================
530 532 def create_test_index(repo_location, config, full_index):
531 533 """
532 534 Makes default test index
533 535
534 536 :param config: test config
535 537 :param full_index:
536 538 """
537 539
538 540 from rhodecode.lib.indexers.daemon import WhooshIndexingDaemon
539 541 from rhodecode.lib.pidlock import DaemonLock, LockHeld
540 542
541 543 repo_location = repo_location
542 544
543 545 index_location = os.path.join(config['app_conf']['index_dir'])
544 546 if not os.path.exists(index_location):
545 547 os.makedirs(index_location)
546 548
547 549 try:
548 550 l = DaemonLock(file_=jn(dn(index_location), 'make_index.lock'))
549 551 WhooshIndexingDaemon(index_location=index_location,
550 552 repo_location=repo_location)\
551 553 .run(full_index=full_index)
552 554 l.release()
553 555 except LockHeld:
554 556 pass
555 557
556 558
557 559 def create_test_env(repos_test_path, config):
558 560 """
559 561 Makes a fresh database and
560 562 install test repository into tmp dir
561 563 """
562 564 from rhodecode.lib.db_manage import DbManage
563 565 from rhodecode.tests import HG_REPO, GIT_REPO, TESTS_TMP_PATH
564 566
565 567 # PART ONE create db
566 568 dbconf = config['sqlalchemy.db1.url']
567 569 log.debug('making test db %s' % dbconf)
568 570
569 571 # create test dir if it doesn't exist
570 572 if not os.path.isdir(repos_test_path):
571 573 log.debug('Creating testdir %s' % repos_test_path)
572 574 os.makedirs(repos_test_path)
573 575
574 576 dbmanage = DbManage(log_sql=True, dbconf=dbconf, root=config['here'],
575 577 tests=True)
576 578 dbmanage.create_tables(override=True)
577 579 dbmanage.create_settings(dbmanage.config_prompt(repos_test_path))
578 580 dbmanage.create_default_user()
579 581 dbmanage.admin_prompt()
580 582 dbmanage.create_permissions()
581 583 dbmanage.populate_default_permissions()
582 584 Session().commit()
583 585 # PART TWO make test repo
584 586 log.debug('making test vcs repositories')
585 587
586 588 idx_path = config['app_conf']['index_dir']
587 589 data_path = config['app_conf']['cache_dir']
588 590
589 591 #clean index and data
590 592 if idx_path and os.path.exists(idx_path):
591 593 log.debug('remove %s' % idx_path)
592 594 shutil.rmtree(idx_path)
593 595
594 596 if data_path and os.path.exists(data_path):
595 597 log.debug('remove %s' % data_path)
596 598 shutil.rmtree(data_path)
597 599
598 600 #CREATE DEFAULT TEST REPOS
599 601 cur_dir = dn(dn(abspath(__file__)))
600 602 tar = tarfile.open(jn(cur_dir, 'tests', "vcs_test_hg.tar.gz"))
601 603 tar.extractall(jn(TESTS_TMP_PATH, HG_REPO))
602 604 tar.close()
603 605
604 606 cur_dir = dn(dn(abspath(__file__)))
605 607 tar = tarfile.open(jn(cur_dir, 'tests', "vcs_test_git.tar.gz"))
606 608 tar.extractall(jn(TESTS_TMP_PATH, GIT_REPO))
607 609 tar.close()
608 610
609 611 #LOAD VCS test stuff
610 612 from rhodecode.tests.vcs import setup_package
611 613 setup_package()
612 614
613 615
614 616 #==============================================================================
615 617 # PASTER COMMANDS
616 618 #==============================================================================
617 619 class BasePasterCommand(Command):
618 620 """
619 621 Abstract Base Class for paster commands.
620 622
621 623 The celery commands are somewhat aggressive about loading
622 624 celery.conf, and since our module sets the `CELERY_LOADER`
623 625 environment variable to our loader, we have to bootstrap a bit and
624 626 make sure we've had a chance to load the pylons config off of the
625 627 command line, otherwise everything fails.
626 628 """
627 629 min_args = 1
628 630 min_args_error = "Please provide a paster config file as an argument."
629 631 takes_config_file = 1
630 632 requires_config_file = True
631 633
632 634 def notify_msg(self, msg, log=False):
633 635 """Make a notification to user, additionally if logger is passed
634 636 it logs this action using given logger
635 637
636 638 :param msg: message that will be printed to user
637 639 :param log: logging instance, to use to additionally log this message
638 640
639 641 """
640 642 if log and isinstance(log, logging):
641 643 log(msg)
642 644
643 645 def run(self, args):
644 646 """
645 647 Overrides Command.run
646 648
647 649 Checks for a config file argument and loads it.
648 650 """
649 651 if len(args) < self.min_args:
650 652 raise BadCommand(
651 653 self.min_args_error % {'min_args': self.min_args,
652 654 'actual_args': len(args)})
653 655
654 656 # Decrement because we're going to lob off the first argument.
655 657 # @@ This is hacky
656 658 self.min_args -= 1
657 659 self.bootstrap_config(args[0])
658 660 self.update_parser()
659 661 return super(BasePasterCommand, self).run(args[1:])
660 662
661 663 def update_parser(self):
662 664 """
663 665 Abstract method. Allows for the class's parser to be updated
664 666 before the superclass's `run` method is called. Necessary to
665 667 allow options/arguments to be passed through to the underlying
666 668 celery command.
667 669 """
668 670 raise NotImplementedError("Abstract Method.")
669 671
670 672 def bootstrap_config(self, conf):
671 673 """
672 674 Loads the pylons configuration.
673 675 """
674 676 from pylons import config as pylonsconfig
675 677
676 678 self.path_to_ini_file = os.path.realpath(conf)
677 679 conf = paste.deploy.appconfig('config:' + self.path_to_ini_file)
678 680 pylonsconfig.init_app(conf.global_conf, conf.local_conf)
679 681
680 682
681 683 def check_git_version():
682 684 """
683 685 Checks what version of git is installed in system, and issues a warning
684 686 if it's too old for RhodeCode to properly work.
685 687 """
686 688 import subprocess
687 689 from distutils.version import StrictVersion
688 690 from rhodecode import BACKENDS
689 691
690 692 p = subprocess.Popen('git --version', shell=True,
691 693 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
692 694 stdout, stderr = p.communicate()
693 695 ver = (stdout.split(' ')[-1] or '').strip() or '0.0.0'
694 696 if len(ver.split('.')) > 3:
695 697 #StrictVersion needs to be only 3 element type
696 698 ver = '.'.join(ver.split('.')[:3])
697 699 try:
698 700 _ver = StrictVersion(ver)
699 701 except:
700 702 _ver = StrictVersion('0.0.0')
701 703 stderr = traceback.format_exc()
702 704
703 705 req_ver = '1.7.4'
704 706 to_old_git = False
705 707 if _ver < StrictVersion(req_ver):
706 708 to_old_git = True
707 709
708 710 if 'git' in BACKENDS:
709 711 log.debug('GIT version detected: %s' % stdout)
710 712 if stderr:
711 713 log.warning('Unable to detect git version org error was:%r' % stderr)
712 714 elif to_old_git:
713 715 log.warning('RhodeCode detected git version %s, which is too old '
714 716 'for the system to function properly. Make sure '
715 717 'its version is at least %s' % (ver, req_ver))
716 718 return _ver
719
720
721 @decorator.decorator
722 def jsonify(func, *args, **kwargs):
723 """Action decorator that formats output for JSON
724
725 Given a function that will return content, this decorator will turn
726 the result into JSON, with a content-type of 'application/json' and
727 output it.
728
729 """
730 from pylons.decorators.util import get_pylons
731 from rhodecode.lib.ext_json import json
732 pylons = get_pylons(args)
733 pylons.response.headers['Content-Type'] = 'application/json; charset=utf-8'
734 data = func(*args, **kwargs)
735 if isinstance(data, (list, tuple)):
736 msg = "JSON responses with Array envelopes are susceptible to " \
737 "cross-site data leak attacks, see " \
738 "http://wiki.pylonshq.com/display/pylonsfaq/Warnings"
739 warnings.warn(msg, Warning, 2)
740 log.warning(msg)
741 log.debug("Returning JSON wrapped action output")
742 return json.dumps(data, encoding='utf-8') No newline at end of file
General Comments 0
You need to be logged in to leave comments. Login now