##// END OF EJS Templates
white space cleanup
marcink -
r2478:8eab8111 beta
parent child Browse files
Show More
@@ -1,428 +1,428 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
29 from webob.exc import HTTPForbidden
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 pylons.decorators import jsonify
34 from pylons.decorators import jsonify
35
35
36 from rhodecode.lib.vcs.exceptions import RepositoryError, ChangesetError, \
36 from rhodecode.lib.vcs.exceptions import RepositoryError, ChangesetError, \
37 ChangesetDoesNotExistError
37 ChangesetDoesNotExistError
38 from rhodecode.lib.vcs.nodes import FileNode
38 from rhodecode.lib.vcs.nodes import FileNode
39
39
40 import rhodecode.lib.helpers as h
40 import rhodecode.lib.helpers as h
41 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
41 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
42 from rhodecode.lib.base import BaseRepoController, render
42 from rhodecode.lib.base import BaseRepoController, render
43 from rhodecode.lib.utils import EmptyChangeset, action_logger
43 from rhodecode.lib.utils import EmptyChangeset, 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.lib.diffs import wrapped_diff
50 from rhodecode.lib.diffs import wrapped_diff
51 from rhodecode.model.repo import RepoModel
51 from rhodecode.model.repo import RepoModel
52
52
53 log = logging.getLogger(__name__)
53 log = logging.getLogger(__name__)
54
54
55
55
56 def _update_with_GET(params, GET):
56 def _update_with_GET(params, GET):
57 for k in ['diff1', 'diff2', 'diff']:
57 for k in ['diff1', 'diff2', 'diff']:
58 params[k] += GET.getall(k)
58 params[k] += GET.getall(k)
59
59
60
60
61 def anchor_url(revision, path, GET):
61 def anchor_url(revision, path, GET):
62 fid = h.FID(revision, path)
62 fid = h.FID(revision, path)
63 return h.url.current(anchor=fid, **dict(GET))
63 return h.url.current(anchor=fid, **dict(GET))
64
64
65
65
66 def get_ignore_ws(fid, GET):
66 def get_ignore_ws(fid, GET):
67 ig_ws_global = GET.get('ignorews')
67 ig_ws_global = GET.get('ignorews')
68 ig_ws = filter(lambda k: k.startswith('WS'), GET.getall(fid))
68 ig_ws = filter(lambda k: k.startswith('WS'), GET.getall(fid))
69 if ig_ws:
69 if ig_ws:
70 try:
70 try:
71 return int(ig_ws[0].split(':')[-1])
71 return int(ig_ws[0].split(':')[-1])
72 except:
72 except:
73 pass
73 pass
74 return ig_ws_global
74 return ig_ws_global
75
75
76
76
77 def _ignorews_url(GET, fileid=None):
77 def _ignorews_url(GET, fileid=None):
78 fileid = str(fileid) if fileid else None
78 fileid = str(fileid) if fileid else None
79 params = defaultdict(list)
79 params = defaultdict(list)
80 _update_with_GET(params, GET)
80 _update_with_GET(params, GET)
81 lbl = _('show white space')
81 lbl = _('show white space')
82 ig_ws = get_ignore_ws(fileid, GET)
82 ig_ws = get_ignore_ws(fileid, GET)
83 ln_ctx = get_line_ctx(fileid, GET)
83 ln_ctx = get_line_ctx(fileid, GET)
84 # global option
84 # global option
85 if fileid is None:
85 if fileid is None:
86 if ig_ws is None:
86 if ig_ws is None:
87 params['ignorews'] += [1]
87 params['ignorews'] += [1]
88 lbl = _('ignore white space')
88 lbl = _('ignore white space')
89 ctx_key = 'context'
89 ctx_key = 'context'
90 ctx_val = ln_ctx
90 ctx_val = ln_ctx
91 # per file options
91 # per file options
92 else:
92 else:
93 if ig_ws is None:
93 if ig_ws is None:
94 params[fileid] += ['WS:1']
94 params[fileid] += ['WS:1']
95 lbl = _('ignore white space')
95 lbl = _('ignore white space')
96
96
97 ctx_key = fileid
97 ctx_key = fileid
98 ctx_val = 'C:%s' % ln_ctx
98 ctx_val = 'C:%s' % ln_ctx
99 # if we have passed in ln_ctx pass it along to our params
99 # if we have passed in ln_ctx pass it along to our params
100 if ln_ctx:
100 if ln_ctx:
101 params[ctx_key] += [ctx_val]
101 params[ctx_key] += [ctx_val]
102
102
103 params['anchor'] = fileid
103 params['anchor'] = fileid
104 img = h.image(h.url('/images/icons/text_strikethrough.png'), lbl, class_='icon')
104 img = h.image(h.url('/images/icons/text_strikethrough.png'), lbl, class_='icon')
105 return h.link_to(img, h.url.current(**params), title=lbl, class_='tooltip')
105 return h.link_to(img, h.url.current(**params), title=lbl, class_='tooltip')
106
106
107
107
108 def get_line_ctx(fid, GET):
108 def get_line_ctx(fid, GET):
109 ln_ctx_global = GET.get('context')
109 ln_ctx_global = GET.get('context')
110 ln_ctx = filter(lambda k: k.startswith('C'), GET.getall(fid))
110 ln_ctx = filter(lambda k: k.startswith('C'), GET.getall(fid))
111
111
112 if ln_ctx:
112 if ln_ctx:
113 retval = ln_ctx[0].split(':')[-1]
113 retval = ln_ctx[0].split(':')[-1]
114 else:
114 else:
115 retval = ln_ctx_global
115 retval = ln_ctx_global
116
116
117 try:
117 try:
118 return int(retval)
118 return int(retval)
119 except:
119 except:
120 return
120 return
121
121
122
122
123 def _context_url(GET, fileid=None):
123 def _context_url(GET, fileid=None):
124 """
124 """
125 Generates url for context lines
125 Generates url for context lines
126
126
127 :param fileid:
127 :param fileid:
128 """
128 """
129
129
130 fileid = str(fileid) if fileid else None
130 fileid = str(fileid) if fileid else None
131 ig_ws = get_ignore_ws(fileid, GET)
131 ig_ws = get_ignore_ws(fileid, GET)
132 ln_ctx = (get_line_ctx(fileid, GET) or 3) * 2
132 ln_ctx = (get_line_ctx(fileid, GET) or 3) * 2
133
133
134 params = defaultdict(list)
134 params = defaultdict(list)
135 _update_with_GET(params, GET)
135 _update_with_GET(params, GET)
136
136
137 # global option
137 # global option
138 if fileid is None:
138 if fileid is None:
139 if ln_ctx > 0:
139 if ln_ctx > 0:
140 params['context'] += [ln_ctx]
140 params['context'] += [ln_ctx]
141
141
142 if ig_ws:
142 if ig_ws:
143 ig_ws_key = 'ignorews'
143 ig_ws_key = 'ignorews'
144 ig_ws_val = 1
144 ig_ws_val = 1
145
145
146 # per file option
146 # per file option
147 else:
147 else:
148 params[fileid] += ['C:%s' % ln_ctx]
148 params[fileid] += ['C:%s' % ln_ctx]
149 ig_ws_key = fileid
149 ig_ws_key = fileid
150 ig_ws_val = 'WS:%s' % 1
150 ig_ws_val = 'WS:%s' % 1
151
151
152 if ig_ws:
152 if ig_ws:
153 params[ig_ws_key] += [ig_ws_val]
153 params[ig_ws_key] += [ig_ws_val]
154
154
155 lbl = _('%s line context') % ln_ctx
155 lbl = _('%s line context') % ln_ctx
156
156
157 params['anchor'] = fileid
157 params['anchor'] = fileid
158 img = h.image(h.url('/images/icons/table_add.png'), lbl, class_='icon')
158 img = h.image(h.url('/images/icons/table_add.png'), lbl, class_='icon')
159 return h.link_to(img, h.url.current(**params), title=lbl, class_='tooltip')
159 return h.link_to(img, h.url.current(**params), title=lbl, class_='tooltip')
160
160
161
161
162 class ChangesetController(BaseRepoController):
162 class ChangesetController(BaseRepoController):
163
163
164 @LoginRequired()
164 @LoginRequired()
165 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
165 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
166 'repository.admin')
166 'repository.admin')
167 def __before__(self):
167 def __before__(self):
168 super(ChangesetController, self).__before__()
168 super(ChangesetController, self).__before__()
169 c.affected_files_cut_off = 60
169 c.affected_files_cut_off = 60
170 repo_model = RepoModel()
170 repo_model = RepoModel()
171 c.users_array = repo_model.get_users_js()
171 c.users_array = repo_model.get_users_js()
172 c.users_groups_array = repo_model.get_users_groups_js()
172 c.users_groups_array = repo_model.get_users_groups_js()
173
173
174 def index(self, revision):
174 def index(self, revision):
175
175
176 c.anchor_url = anchor_url
176 c.anchor_url = anchor_url
177 c.ignorews_url = _ignorews_url
177 c.ignorews_url = _ignorews_url
178 c.context_url = _context_url
178 c.context_url = _context_url
179 limit_off = request.GET.get('fulldiff')
179 limit_off = request.GET.get('fulldiff')
180 #get ranges of revisions if preset
180 #get ranges of revisions if preset
181 rev_range = revision.split('...')[:2]
181 rev_range = revision.split('...')[:2]
182 enable_comments = True
182 enable_comments = True
183 try:
183 try:
184 if len(rev_range) == 2:
184 if len(rev_range) == 2:
185 enable_comments = False
185 enable_comments = False
186 rev_start = rev_range[0]
186 rev_start = rev_range[0]
187 rev_end = rev_range[1]
187 rev_end = rev_range[1]
188 rev_ranges = c.rhodecode_repo.get_changesets(start=rev_start,
188 rev_ranges = c.rhodecode_repo.get_changesets(start=rev_start,
189 end=rev_end)
189 end=rev_end)
190 else:
190 else:
191 rev_ranges = [c.rhodecode_repo.get_changeset(revision)]
191 rev_ranges = [c.rhodecode_repo.get_changeset(revision)]
192
192
193 c.cs_ranges = list(rev_ranges)
193 c.cs_ranges = list(rev_ranges)
194 if not c.cs_ranges:
194 if not c.cs_ranges:
195 raise RepositoryError('Changeset range returned empty result')
195 raise RepositoryError('Changeset range returned empty result')
196
196
197 except (RepositoryError, ChangesetDoesNotExistError, Exception), e:
197 except (RepositoryError, ChangesetDoesNotExistError, Exception), e:
198 log.error(traceback.format_exc())
198 log.error(traceback.format_exc())
199 h.flash(str(e), category='warning')
199 h.flash(str(e), category='warning')
200 return redirect(url('home'))
200 return redirect(url('home'))
201
201
202 c.changes = OrderedDict()
202 c.changes = OrderedDict()
203
203
204 c.lines_added = 0 # count of lines added
204 c.lines_added = 0 # count of lines added
205 c.lines_deleted = 0 # count of lines removes
205 c.lines_deleted = 0 # count of lines removes
206
206
207 cumulative_diff = 0
207 cumulative_diff = 0
208 c.cut_off = False # defines if cut off limit is reached
208 c.cut_off = False # defines if cut off limit is reached
209 c.changeset_statuses = ChangesetStatus.STATUSES
209 c.changeset_statuses = ChangesetStatus.STATUSES
210 c.comments = []
210 c.comments = []
211 c.statuses = []
211 c.statuses = []
212 c.inline_comments = []
212 c.inline_comments = []
213 c.inline_cnt = 0
213 c.inline_cnt = 0
214 # Iterate over ranges (default changeset view is always one changeset)
214 # Iterate over ranges (default changeset view is always one changeset)
215 for changeset in c.cs_ranges:
215 for changeset in c.cs_ranges:
216
216
217 c.statuses.extend([ChangesetStatusModel()\
217 c.statuses.extend([ChangesetStatusModel()\
218 .get_status(c.rhodecode_db_repo.repo_id,
218 .get_status(c.rhodecode_db_repo.repo_id,
219 changeset.raw_id)])
219 changeset.raw_id)])
220
220
221 c.comments.extend(ChangesetCommentsModel()\
221 c.comments.extend(ChangesetCommentsModel()\
222 .get_comments(c.rhodecode_db_repo.repo_id,
222 .get_comments(c.rhodecode_db_repo.repo_id,
223 revision=changeset.raw_id))
223 revision=changeset.raw_id))
224 inlines = ChangesetCommentsModel()\
224 inlines = ChangesetCommentsModel()\
225 .get_inline_comments(c.rhodecode_db_repo.repo_id,
225 .get_inline_comments(c.rhodecode_db_repo.repo_id,
226 revision=changeset.raw_id)
226 revision=changeset.raw_id)
227 c.inline_comments.extend(inlines)
227 c.inline_comments.extend(inlines)
228 c.changes[changeset.raw_id] = []
228 c.changes[changeset.raw_id] = []
229 try:
229 try:
230 changeset_parent = changeset.parents[0]
230 changeset_parent = changeset.parents[0]
231 except IndexError:
231 except IndexError:
232 changeset_parent = None
232 changeset_parent = None
233
233
234 #==================================================================
234 #==================================================================
235 # ADDED FILES
235 # ADDED FILES
236 #==================================================================
236 #==================================================================
237 for node in changeset.added:
237 for node in changeset.added:
238 fid = h.FID(revision, node.path)
238 fid = h.FID(revision, node.path)
239 line_context_lcl = get_line_ctx(fid, request.GET)
239 line_context_lcl = get_line_ctx(fid, request.GET)
240 ign_whitespace_lcl = get_ignore_ws(fid, request.GET)
240 ign_whitespace_lcl = get_ignore_ws(fid, request.GET)
241 lim = self.cut_off_limit
241 lim = self.cut_off_limit
242 if cumulative_diff > self.cut_off_limit:
242 if cumulative_diff > self.cut_off_limit:
243 lim = -1 if limit_off is None else None
243 lim = -1 if limit_off is None else None
244 size, cs1, cs2, diff, st = wrapped_diff(
244 size, cs1, cs2, diff, st = wrapped_diff(
245 filenode_old=None,
245 filenode_old=None,
246 filenode_new=node,
246 filenode_new=node,
247 cut_off_limit=lim,
247 cut_off_limit=lim,
248 ignore_whitespace=ign_whitespace_lcl,
248 ignore_whitespace=ign_whitespace_lcl,
249 line_context=line_context_lcl,
249 line_context=line_context_lcl,
250 enable_comments=enable_comments
250 enable_comments=enable_comments
251 )
251 )
252 cumulative_diff += size
252 cumulative_diff += size
253 c.lines_added += st[0]
253 c.lines_added += st[0]
254 c.lines_deleted += st[1]
254 c.lines_deleted += st[1]
255 c.changes[changeset.raw_id].append(
255 c.changes[changeset.raw_id].append(
256 ('added', node, diff, cs1, cs2, st)
256 ('added', node, diff, cs1, cs2, st)
257 )
257 )
258
258
259 #==================================================================
259 #==================================================================
260 # CHANGED FILES
260 # CHANGED FILES
261 #==================================================================
261 #==================================================================
262 for node in changeset.changed:
262 for node in changeset.changed:
263 try:
263 try:
264 filenode_old = changeset_parent.get_node(node.path)
264 filenode_old = changeset_parent.get_node(node.path)
265 except ChangesetError:
265 except ChangesetError:
266 log.warning('Unable to fetch parent node for diff')
266 log.warning('Unable to fetch parent node for diff')
267 filenode_old = FileNode(node.path, '', EmptyChangeset())
267 filenode_old = FileNode(node.path, '', EmptyChangeset())
268
268
269 fid = h.FID(revision, node.path)
269 fid = h.FID(revision, node.path)
270 line_context_lcl = get_line_ctx(fid, request.GET)
270 line_context_lcl = get_line_ctx(fid, request.GET)
271 ign_whitespace_lcl = get_ignore_ws(fid, request.GET)
271 ign_whitespace_lcl = get_ignore_ws(fid, request.GET)
272 lim = self.cut_off_limit
272 lim = self.cut_off_limit
273 if cumulative_diff > self.cut_off_limit:
273 if cumulative_diff > self.cut_off_limit:
274 lim = -1 if limit_off is None else None
274 lim = -1 if limit_off is None else None
275 size, cs1, cs2, diff, st = wrapped_diff(
275 size, cs1, cs2, diff, st = wrapped_diff(
276 filenode_old=filenode_old,
276 filenode_old=filenode_old,
277 filenode_new=node,
277 filenode_new=node,
278 cut_off_limit=lim,
278 cut_off_limit=lim,
279 ignore_whitespace=ign_whitespace_lcl,
279 ignore_whitespace=ign_whitespace_lcl,
280 line_context=line_context_lcl,
280 line_context=line_context_lcl,
281 enable_comments=enable_comments
281 enable_comments=enable_comments
282 )
282 )
283 cumulative_diff += size
283 cumulative_diff += size
284 c.lines_added += st[0]
284 c.lines_added += st[0]
285 c.lines_deleted += st[1]
285 c.lines_deleted += st[1]
286 c.changes[changeset.raw_id].append(
286 c.changes[changeset.raw_id].append(
287 ('changed', node, diff, cs1, cs2, st)
287 ('changed', node, diff, cs1, cs2, st)
288 )
288 )
289 #==================================================================
289 #==================================================================
290 # REMOVED FILES
290 # REMOVED FILES
291 #==================================================================
291 #==================================================================
292 for node in changeset.removed:
292 for node in changeset.removed:
293 c.changes[changeset.raw_id].append(
293 c.changes[changeset.raw_id].append(
294 ('removed', node, None, None, None, (0, 0))
294 ('removed', node, None, None, None, (0, 0))
295 )
295 )
296
296
297 # count inline comments
297 # count inline comments
298 for __, lines in c.inline_comments:
298 for __, lines in c.inline_comments:
299 for comments in lines.values():
299 for comments in lines.values():
300 c.inline_cnt += len(comments)
300 c.inline_cnt += len(comments)
301
301
302 if len(c.cs_ranges) == 1:
302 if len(c.cs_ranges) == 1:
303 c.changeset = c.cs_ranges[0]
303 c.changeset = c.cs_ranges[0]
304 c.changes = c.changes[c.changeset.raw_id]
304 c.changes = c.changes[c.changeset.raw_id]
305
305
306 return render('changeset/changeset.html')
306 return render('changeset/changeset.html')
307 else:
307 else:
308 return render('changeset/changeset_range.html')
308 return render('changeset/changeset_range.html')
309
309
310 def raw_changeset(self, revision):
310 def raw_changeset(self, revision):
311
311
312 method = request.GET.get('diff', 'show')
312 method = request.GET.get('diff', 'show')
313 ignore_whitespace = request.GET.get('ignorews') == '1'
313 ignore_whitespace = request.GET.get('ignorews') == '1'
314 line_context = request.GET.get('context', 3)
314 line_context = request.GET.get('context', 3)
315 try:
315 try:
316 c.scm_type = c.rhodecode_repo.alias
316 c.scm_type = c.rhodecode_repo.alias
317 c.changeset = c.rhodecode_repo.get_changeset(revision)
317 c.changeset = c.rhodecode_repo.get_changeset(revision)
318 except RepositoryError:
318 except RepositoryError:
319 log.error(traceback.format_exc())
319 log.error(traceback.format_exc())
320 return redirect(url('home'))
320 return redirect(url('home'))
321 else:
321 else:
322 try:
322 try:
323 c.changeset_parent = c.changeset.parents[0]
323 c.changeset_parent = c.changeset.parents[0]
324 except IndexError:
324 except IndexError:
325 c.changeset_parent = None
325 c.changeset_parent = None
326 c.changes = []
326 c.changes = []
327
327
328 for node in c.changeset.added:
328 for node in c.changeset.added:
329 filenode_old = FileNode(node.path, '')
329 filenode_old = FileNode(node.path, '')
330 if filenode_old.is_binary or node.is_binary:
330 if filenode_old.is_binary or node.is_binary:
331 diff = _('binary file') + '\n'
331 diff = _('binary file') + '\n'
332 else:
332 else:
333 f_gitdiff = diffs.get_gitdiff(filenode_old, node,
333 f_gitdiff = diffs.get_gitdiff(filenode_old, node,
334 ignore_whitespace=ignore_whitespace,
334 ignore_whitespace=ignore_whitespace,
335 context=line_context)
335 context=line_context)
336 diff = diffs.DiffProcessor(f_gitdiff,
336 diff = diffs.DiffProcessor(f_gitdiff,
337 format='gitdiff').raw_diff()
337 format='gitdiff').raw_diff()
338
338
339 cs1 = None
339 cs1 = None
340 cs2 = node.changeset.raw_id
340 cs2 = node.changeset.raw_id
341 c.changes.append(('added', node, diff, cs1, cs2))
341 c.changes.append(('added', node, diff, cs1, cs2))
342
342
343 for node in c.changeset.changed:
343 for node in c.changeset.changed:
344 filenode_old = c.changeset_parent.get_node(node.path)
344 filenode_old = c.changeset_parent.get_node(node.path)
345 if filenode_old.is_binary or node.is_binary:
345 if filenode_old.is_binary or node.is_binary:
346 diff = _('binary file')
346 diff = _('binary file')
347 else:
347 else:
348 f_gitdiff = diffs.get_gitdiff(filenode_old, node,
348 f_gitdiff = diffs.get_gitdiff(filenode_old, node,
349 ignore_whitespace=ignore_whitespace,
349 ignore_whitespace=ignore_whitespace,
350 context=line_context)
350 context=line_context)
351 diff = diffs.DiffProcessor(f_gitdiff,
351 diff = diffs.DiffProcessor(f_gitdiff,
352 format='gitdiff').raw_diff()
352 format='gitdiff').raw_diff()
353
353
354 cs1 = filenode_old.changeset.raw_id
354 cs1 = filenode_old.changeset.raw_id
355 cs2 = node.changeset.raw_id
355 cs2 = node.changeset.raw_id
356 c.changes.append(('changed', node, diff, cs1, cs2))
356 c.changes.append(('changed', node, diff, cs1, cs2))
357
357
358 response.content_type = 'text/plain'
358 response.content_type = 'text/plain'
359
359
360 if method == 'download':
360 if method == 'download':
361 response.content_disposition = 'attachment; filename=%s.patch' \
361 response.content_disposition = 'attachment; filename=%s.patch' \
362 % revision
362 % revision
363
363
364 c.parent_tmpl = ''.join(['# Parent %s\n' % x.raw_id
364 c.parent_tmpl = ''.join(['# Parent %s\n' % x.raw_id
365 for x in c.changeset.parents])
365 for x in c.changeset.parents])
366
366
367 c.diffs = ''
367 c.diffs = ''
368 for x in c.changes:
368 for x in c.changes:
369 c.diffs += x[2]
369 c.diffs += x[2]
370
370
371 return render('changeset/raw_changeset.html')
371 return render('changeset/raw_changeset.html')
372
372
373 @jsonify
373 @jsonify
374 def comment(self, repo_name, revision):
374 def comment(self, repo_name, revision):
375 status = request.POST.get('changeset_status')
375 status = request.POST.get('changeset_status')
376 change_status = request.POST.get('change_changeset_status')
376 change_status = request.POST.get('change_changeset_status')
377
377
378 comm = ChangesetCommentsModel().create(
378 comm = ChangesetCommentsModel().create(
379 text=request.POST.get('text'),
379 text=request.POST.get('text'),
380 repo_id=c.rhodecode_db_repo.repo_id,
380 repo_id=c.rhodecode_db_repo.repo_id,
381 user_id=c.rhodecode_user.user_id,
381 user_id=c.rhodecode_user.user_id,
382 revision=revision,
382 revision=revision,
383 f_path=request.POST.get('f_path'),
383 f_path=request.POST.get('f_path'),
384 line_no=request.POST.get('line'),
384 line_no=request.POST.get('line'),
385 status_change=(ChangesetStatus.get_status_lbl(status)
385 status_change=(ChangesetStatus.get_status_lbl(status)
386 if status and change_status else None)
386 if status and change_status else None)
387 )
387 )
388
388
389 # get status if set !
389 # get status if set !
390 if status and change_status:
390 if status and change_status:
391 ChangesetStatusModel().set_status(
391 ChangesetStatusModel().set_status(
392 c.rhodecode_db_repo.repo_id,
392 c.rhodecode_db_repo.repo_id,
393 status,
393 status,
394 c.rhodecode_user.user_id,
394 c.rhodecode_user.user_id,
395 comm,
395 comm,
396 revision=revision,
396 revision=revision,
397 )
397 )
398 action_logger(self.rhodecode_user,
398 action_logger(self.rhodecode_user,
399 'user_commented_revision:%s' % revision,
399 'user_commented_revision:%s' % revision,
400 c.rhodecode_db_repo, self.ip_addr, self.sa)
400 c.rhodecode_db_repo, self.ip_addr, self.sa)
401
401
402 Session.commit()
402 Session.commit()
403
403
404 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
404 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
405 return redirect(h.url('changeset_home', repo_name=repo_name,
405 return redirect(h.url('changeset_home', repo_name=repo_name,
406 revision=revision))
406 revision=revision))
407
407
408 data = {
408 data = {
409 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
409 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
410 }
410 }
411 if comm:
411 if comm:
412 c.co = comm
412 c.co = comm
413 data.update(comm.get_dict())
413 data.update(comm.get_dict())
414 data.update({'rendered_text':
414 data.update({'rendered_text':
415 render('changeset/changeset_comment_block.html')})
415 render('changeset/changeset_comment_block.html')})
416
416
417 return data
417 return data
418
418
419 @jsonify
419 @jsonify
420 def delete_comment(self, repo_name, comment_id):
420 def delete_comment(self, repo_name, comment_id):
421 co = ChangesetComment.get(comment_id)
421 co = ChangesetComment.get(comment_id)
422 owner = lambda: co.author.user_id == c.rhodecode_user.user_id
422 owner = lambda: co.author.user_id == c.rhodecode_user.user_id
423 if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
423 if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
424 ChangesetCommentsModel().delete(comment=co)
424 ChangesetCommentsModel().delete(comment=co)
425 Session.commit()
425 Session.commit()
426 return True
426 return True
427 else:
427 else:
428 raise HTTPForbidden()
428 raise HTTPForbidden()
@@ -1,277 +1,277 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.controllers.pullrequests
3 rhodecode.controllers.pullrequests
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5
5
6 pull requests controller for rhodecode for initializing pull requests
6 pull requests controller for rhodecode for initializing pull requests
7
7
8 :created_on: May 7, 2012
8 :created_on: May 7, 2012
9 :author: marcink
9 :author: marcink
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 :license: GPLv3, see COPYING for more details.
11 :license: GPLv3, see COPYING for more details.
12 """
12 """
13 # This program is free software: you can redistribute it and/or modify
13 # This program is free software: you can redistribute it and/or modify
14 # it under the terms of the GNU General Public License as published by
14 # it under the terms of the GNU General Public License as published by
15 # the Free Software Foundation, either version 3 of the License, or
15 # the Free Software Foundation, either version 3 of the License, or
16 # (at your option) any later version.
16 # (at your option) any later version.
17 #
17 #
18 # This program is distributed in the hope that it will be useful,
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # GNU General Public License for more details.
21 # GNU General Public License for more details.
22 #
22 #
23 # You should have received a copy of the GNU General Public License
23 # You should have received a copy of the GNU General Public License
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 import logging
25 import logging
26 import traceback
26 import traceback
27
27
28 from webob.exc import HTTPNotFound
28 from webob.exc import HTTPNotFound
29
29
30 from pylons import request, response, session, tmpl_context as c, url
30 from pylons import request, response, session, tmpl_context as c, url
31 from pylons.controllers.util import abort, redirect
31 from pylons.controllers.util import abort, redirect
32 from pylons.i18n.translation import _
32 from pylons.i18n.translation import _
33 from pylons.decorators import jsonify
33 from pylons.decorators import jsonify
34
34
35 from rhodecode.lib.base import BaseRepoController, render
35 from rhodecode.lib.base import BaseRepoController, render
36 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
36 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
37 from rhodecode.lib import helpers as h
37 from rhodecode.lib import helpers as h
38 from rhodecode.lib import diffs
38 from rhodecode.lib import diffs
39 from rhodecode.lib.utils import action_logger
39 from rhodecode.lib.utils import action_logger
40 from rhodecode.model.db import User, PullRequest, ChangesetStatus
40 from rhodecode.model.db import User, PullRequest, ChangesetStatus
41 from rhodecode.model.pull_request import PullRequestModel
41 from rhodecode.model.pull_request import PullRequestModel
42 from rhodecode.model.meta import Session
42 from rhodecode.model.meta import Session
43 from rhodecode.model.repo import RepoModel
43 from rhodecode.model.repo import RepoModel
44 from rhodecode.model.comment import ChangesetCommentsModel
44 from rhodecode.model.comment import ChangesetCommentsModel
45 from rhodecode.model.changeset_status import ChangesetStatusModel
45 from rhodecode.model.changeset_status import ChangesetStatusModel
46
46
47 log = logging.getLogger(__name__)
47 log = logging.getLogger(__name__)
48
48
49
49
50 class PullrequestsController(BaseRepoController):
50 class PullrequestsController(BaseRepoController):
51
51
52 @LoginRequired()
52 @LoginRequired()
53 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
53 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
54 'repository.admin')
54 'repository.admin')
55 def __before__(self):
55 def __before__(self):
56 super(PullrequestsController, self).__before__()
56 super(PullrequestsController, self).__before__()
57
57
58 def _get_repo_refs(self, repo):
58 def _get_repo_refs(self, repo):
59 hist_l = []
59 hist_l = []
60
60
61 branches_group = ([('branch:%s:%s' % (k, v), k) for
61 branches_group = ([('branch:%s:%s' % (k, v), k) for
62 k, v in repo.branches.iteritems()], _("Branches"))
62 k, v in repo.branches.iteritems()], _("Branches"))
63 bookmarks_group = ([('book:%s:%s' % (k, v), k) for
63 bookmarks_group = ([('book:%s:%s' % (k, v), k) for
64 k, v in repo.bookmarks.iteritems()], _("Bookmarks"))
64 k, v in repo.bookmarks.iteritems()], _("Bookmarks"))
65 tags_group = ([('tag:%s:%s' % (k, v), k) for
65 tags_group = ([('tag:%s:%s' % (k, v), k) for
66 k, v in repo.tags.iteritems()], _("Tags"))
66 k, v in repo.tags.iteritems()], _("Tags"))
67
67
68 hist_l.append(bookmarks_group)
68 hist_l.append(bookmarks_group)
69 hist_l.append(branches_group)
69 hist_l.append(branches_group)
70 hist_l.append(tags_group)
70 hist_l.append(tags_group)
71
71
72 return hist_l
72 return hist_l
73
73
74 def show_all(self, repo_name):
74 def show_all(self, repo_name):
75 c.pull_requests = PullRequestModel().get_all(repo_name)
75 c.pull_requests = PullRequestModel().get_all(repo_name)
76 c.repo_name = repo_name
76 c.repo_name = repo_name
77 return render('/pullrequests/pullrequest_show_all.html')
77 return render('/pullrequests/pullrequest_show_all.html')
78
78
79 def index(self):
79 def index(self):
80 org_repo = c.rhodecode_db_repo
80 org_repo = c.rhodecode_db_repo
81
81
82 if org_repo.scm_instance.alias != 'hg':
82 if org_repo.scm_instance.alias != 'hg':
83 log.error('Review not available for GIT REPOS')
83 log.error('Review not available for GIT REPOS')
84 raise HTTPNotFound
84 raise HTTPNotFound
85
85
86 c.org_refs = self._get_repo_refs(c.rhodecode_repo)
86 c.org_refs = self._get_repo_refs(c.rhodecode_repo)
87 c.org_repos = []
87 c.org_repos = []
88 c.other_repos = []
88 c.other_repos = []
89 c.org_repos.append((org_repo.repo_name, '%s/%s' % (
89 c.org_repos.append((org_repo.repo_name, '%s/%s' % (
90 org_repo.user.username, c.repo_name))
90 org_repo.user.username, c.repo_name))
91 )
91 )
92
92
93 c.other_refs = c.org_refs
93 c.other_refs = c.org_refs
94 c.other_repos.extend(c.org_repos)
94 c.other_repos.extend(c.org_repos)
95 c.default_pull_request = org_repo.repo_name
95 c.default_pull_request = org_repo.repo_name
96 #gather forks and add to this list
96 #gather forks and add to this list
97 for fork in org_repo.forks:
97 for fork in org_repo.forks:
98 c.other_repos.append((fork.repo_name, '%s/%s' % (
98 c.other_repos.append((fork.repo_name, '%s/%s' % (
99 fork.user.username, fork.repo_name))
99 fork.user.username, fork.repo_name))
100 )
100 )
101 #add parents of this fork also
101 #add parents of this fork also
102 if org_repo.parent:
102 if org_repo.parent:
103 c.default_pull_request = org_repo.parent.repo_name
103 c.default_pull_request = org_repo.parent.repo_name
104 c.other_repos.append((org_repo.parent.repo_name, '%s/%s' % (
104 c.other_repos.append((org_repo.parent.repo_name, '%s/%s' % (
105 org_repo.parent.user.username,
105 org_repo.parent.user.username,
106 org_repo.parent.repo_name))
106 org_repo.parent.repo_name))
107 )
107 )
108
108
109 c.review_members = []
109 c.review_members = []
110 c.available_members = []
110 c.available_members = []
111 for u in User.query().filter(User.username != 'default').all():
111 for u in User.query().filter(User.username != 'default').all():
112 uname = u.username
112 uname = u.username
113 if org_repo.user == u:
113 if org_repo.user == u:
114 uname = _('%s (owner)' % u.username)
114 uname = _('%s (owner)' % u.username)
115 # auto add owner to pull-request recipients
115 # auto add owner to pull-request recipients
116 c.review_members.append([u.user_id, uname])
116 c.review_members.append([u.user_id, uname])
117 c.available_members.append([u.user_id, uname])
117 c.available_members.append([u.user_id, uname])
118 return render('/pullrequests/pullrequest.html')
118 return render('/pullrequests/pullrequest.html')
119
119
120 def create(self, repo_name):
120 def create(self, repo_name):
121 req_p = request.POST
121 req_p = request.POST
122 org_repo = req_p['org_repo']
122 org_repo = req_p['org_repo']
123 org_ref = req_p['org_ref']
123 org_ref = req_p['org_ref']
124 other_repo = req_p['other_repo']
124 other_repo = req_p['other_repo']
125 other_ref = req_p['other_ref']
125 other_ref = req_p['other_ref']
126 revisions = req_p.getall('revisions')
126 revisions = req_p.getall('revisions')
127 reviewers = req_p.getall('review_members')
127 reviewers = req_p.getall('review_members')
128 #TODO: wrap this into a FORM !!!
128 #TODO: wrap this into a FORM !!!
129
129
130 title = req_p['pullrequest_title']
130 title = req_p['pullrequest_title']
131 description = req_p['pullrequest_desc']
131 description = req_p['pullrequest_desc']
132
132
133 try:
133 try:
134 model = PullRequestModel()
134 model = PullRequestModel()
135 model.create(self.rhodecode_user.user_id, org_repo,
135 model.create(self.rhodecode_user.user_id, org_repo,
136 org_ref, other_repo, other_ref, revisions,
136 org_ref, other_repo, other_ref, revisions,
137 reviewers, title, description)
137 reviewers, title, description)
138 Session.commit()
138 Session.commit()
139 h.flash(_('Pull request send'), category='success')
139 h.flash(_('Pull request send'), category='success')
140 except Exception:
140 except Exception:
141 raise
141 raise
142 h.flash(_('Error occured during sending pull request'),
142 h.flash(_('Error occured during sending pull request'),
143 category='error')
143 category='error')
144 log.error(traceback.format_exc())
144 log.error(traceback.format_exc())
145
145
146 return redirect(url('changelog_home', repo_name=repo_name))
146 return redirect(url('changelog_home', repo_name=repo_name))
147
147
148 def _load_compare_data(self, pull_request):
148 def _load_compare_data(self, pull_request):
149 """
149 """
150 Load context data needed for generating compare diff
150 Load context data needed for generating compare diff
151
151
152 :param pull_request:
152 :param pull_request:
153 :type pull_request:
153 :type pull_request:
154 """
154 """
155
155
156 org_repo = pull_request.org_repo
156 org_repo = pull_request.org_repo
157 org_ref_type, org_ref_, org_ref = pull_request.org_ref.split(':')
157 org_ref_type, org_ref_, org_ref = pull_request.org_ref.split(':')
158 other_repo = pull_request.other_repo
158 other_repo = pull_request.other_repo
159 other_ref_type, other_ref, other_ref_ = pull_request.other_ref.split(':')
159 other_ref_type, other_ref, other_ref_ = pull_request.other_ref.split(':')
160
160
161 org_ref = (org_ref_type, org_ref)
161 org_ref = (org_ref_type, org_ref)
162 other_ref = (other_ref_type, other_ref)
162 other_ref = (other_ref_type, other_ref)
163
163
164 c.org_repo = org_repo
164 c.org_repo = org_repo
165 c.other_repo = other_repo
165 c.other_repo = other_repo
166
166
167 c.cs_ranges, discovery_data = PullRequestModel().get_compare_data(
167 c.cs_ranges, discovery_data = PullRequestModel().get_compare_data(
168 org_repo, org_ref, other_repo, other_ref
168 org_repo, org_ref, other_repo, other_ref
169 )
169 )
170
170
171 c.statuses = c.rhodecode_db_repo.statuses([x.raw_id for x in
171 c.statuses = c.rhodecode_db_repo.statuses([x.raw_id for x in
172 c.cs_ranges])
172 c.cs_ranges])
173 # defines that we need hidden inputs with changesets
173 # defines that we need hidden inputs with changesets
174 c.as_form = request.GET.get('as_form', False)
174 c.as_form = request.GET.get('as_form', False)
175
175
176 c.org_ref = org_ref[1]
176 c.org_ref = org_ref[1]
177 c.other_ref = other_ref[1]
177 c.other_ref = other_ref[1]
178 # diff needs to have swapped org with other to generate proper diff
178 # diff needs to have swapped org with other to generate proper diff
179 _diff = diffs.differ(other_repo, other_ref, org_repo, org_ref,
179 _diff = diffs.differ(other_repo, other_ref, org_repo, org_ref,
180 discovery_data)
180 discovery_data)
181 diff_processor = diffs.DiffProcessor(_diff, format='gitdiff')
181 diff_processor = diffs.DiffProcessor(_diff, format='gitdiff')
182 _parsed = diff_processor.prepare()
182 _parsed = diff_processor.prepare()
183
183
184 c.files = []
184 c.files = []
185 c.changes = {}
185 c.changes = {}
186
186
187 for f in _parsed:
187 for f in _parsed:
188 fid = h.FID('', f['filename'])
188 fid = h.FID('', f['filename'])
189 c.files.append([fid, f['operation'], f['filename'], f['stats']])
189 c.files.append([fid, f['operation'], f['filename'], f['stats']])
190 diff = diff_processor.as_html(enable_comments=False, diff_lines=[f])
190 diff = diff_processor.as_html(enable_comments=False, diff_lines=[f])
191 c.changes[fid] = [f['operation'], f['filename'], diff]
191 c.changes[fid] = [f['operation'], f['filename'], diff]
192
192
193 def show(self, repo_name, pull_request_id):
193 def show(self, repo_name, pull_request_id):
194 repo_model = RepoModel()
194 repo_model = RepoModel()
195 c.users_array = repo_model.get_users_js()
195 c.users_array = repo_model.get_users_js()
196 c.users_groups_array = repo_model.get_users_groups_js()
196 c.users_groups_array = repo_model.get_users_groups_js()
197 c.pull_request = PullRequest.get(pull_request_id)
197 c.pull_request = PullRequest.get(pull_request_id)
198
198
199 # valid ID
199 # valid ID
200 if not c.pull_request:
200 if not c.pull_request:
201 raise HTTPNotFound
201 raise HTTPNotFound
202
202
203 # pull_requests repo_name we opened it against
203 # pull_requests repo_name we opened it against
204 # ie. other_repo must match
204 # ie. other_repo must match
205 if repo_name != c.pull_request.other_repo.repo_name:
205 if repo_name != c.pull_request.other_repo.repo_name:
206 raise HTTPNotFound
206 raise HTTPNotFound
207
207
208 # load compare data into template context
208 # load compare data into template context
209 self._load_compare_data(c.pull_request)
209 self._load_compare_data(c.pull_request)
210
210
211 # inline comments
211 # inline comments
212 c.inline_cnt = 0
212 c.inline_cnt = 0
213 c.inline_comments = ChangesetCommentsModel()\
213 c.inline_comments = ChangesetCommentsModel()\
214 .get_inline_comments(c.rhodecode_db_repo.repo_id,
214 .get_inline_comments(c.rhodecode_db_repo.repo_id,
215 pull_request=pull_request_id)
215 pull_request=pull_request_id)
216 # count inline comments
216 # count inline comments
217 for __, lines in c.inline_comments:
217 for __, lines in c.inline_comments:
218 for comments in lines.values():
218 for comments in lines.values():
219 c.inline_cnt += len(comments)
219 c.inline_cnt += len(comments)
220 # comments
220 # comments
221 c.comments = ChangesetCommentsModel()\
221 c.comments = ChangesetCommentsModel()\
222 .get_comments(c.rhodecode_db_repo.repo_id,
222 .get_comments(c.rhodecode_db_repo.repo_id,
223 pull_request=pull_request_id)
223 pull_request=pull_request_id)
224
224
225 # changeset(pull-request) status
225 # changeset(pull-request) status
226 c.current_changeset_status = ChangesetStatusModel()\
226 c.current_changeset_status = ChangesetStatusModel()\
227 .get_status(c.pull_request.org_repo,
227 .get_status(c.pull_request.org_repo,
228 pull_request=c.pull_request)
228 pull_request=c.pull_request)
229 c.changeset_statuses = ChangesetStatus.STATUSES
229 c.changeset_statuses = ChangesetStatus.STATUSES
230 return render('/pullrequests/pullrequest_show.html')
230 return render('/pullrequests/pullrequest_show.html')
231
231
232 @jsonify
232 @jsonify
233 def comment(self, repo_name, pull_request_id):
233 def comment(self, repo_name, pull_request_id):
234
234
235 status = request.POST.get('changeset_status')
235 status = request.POST.get('changeset_status')
236 change_status = request.POST.get('change_changeset_status')
236 change_status = request.POST.get('change_changeset_status')
237
237
238 comm = ChangesetCommentsModel().create(
238 comm = ChangesetCommentsModel().create(
239 text=request.POST.get('text'),
239 text=request.POST.get('text'),
240 repo_id=c.rhodecode_db_repo.repo_id,
240 repo_id=c.rhodecode_db_repo.repo_id,
241 user_id=c.rhodecode_user.user_id,
241 user_id=c.rhodecode_user.user_id,
242 pull_request=pull_request_id,
242 pull_request=pull_request_id,
243 f_path=request.POST.get('f_path'),
243 f_path=request.POST.get('f_path'),
244 line_no=request.POST.get('line'),
244 line_no=request.POST.get('line'),
245 status_change=(ChangesetStatus.get_status_lbl(status)
245 status_change=(ChangesetStatus.get_status_lbl(status)
246 if status and change_status else None)
246 if status and change_status else None)
247 )
247 )
248
248
249 # get status if set !
249 # get status if set !
250 if status and change_status:
250 if status and change_status:
251 ChangesetStatusModel().set_status(
251 ChangesetStatusModel().set_status(
252 c.rhodecode_db_repo.repo_id,
252 c.rhodecode_db_repo.repo_id,
253 status,
253 status,
254 c.rhodecode_user.user_id,
254 c.rhodecode_user.user_id,
255 comm,
255 comm,
256 pull_request=pull_request_id
256 pull_request=pull_request_id
257 )
257 )
258 action_logger(self.rhodecode_user,
258 action_logger(self.rhodecode_user,
259 'user_commented_pull_request:%s' % pull_request_id,
259 'user_commented_pull_request:%s' % pull_request_id,
260 c.rhodecode_db_repo, self.ip_addr, self.sa)
260 c.rhodecode_db_repo, self.ip_addr, self.sa)
261
261
262 Session.commit()
262 Session.commit()
263
263
264 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
264 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
265 return redirect(h.url('pullrequest_show', repo_name=repo_name,
265 return redirect(h.url('pullrequest_show', repo_name=repo_name,
266 pull_request_id=pull_request_id))
266 pull_request_id=pull_request_id))
267
267
268 data = {
268 data = {
269 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
269 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
270 }
270 }
271 if comm:
271 if comm:
272 c.co = comm
272 c.co = comm
273 data.update(comm.get_dict())
273 data.update(comm.get_dict())
274 data.update({'rendered_text':
274 data.update({'rendered_text':
275 render('changeset/changeset_comment_block.html')})
275 render('changeset/changeset_comment_block.html')})
276
276
277 return data
277 return data
@@ -1,219 +1,219 b''
1 """The base Controller API
1 """The base Controller API
2
2
3 Provides the BaseController class for subclassing.
3 Provides the BaseController class for subclassing.
4 """
4 """
5 import logging
5 import logging
6 import time
6 import time
7 import traceback
7 import traceback
8
8
9 from paste.auth.basic import AuthBasicAuthenticator
9 from paste.auth.basic import AuthBasicAuthenticator
10 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden
10 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden
11 from paste.httpheaders import WWW_AUTHENTICATE
11 from paste.httpheaders import WWW_AUTHENTICATE
12
12
13 from pylons import config, tmpl_context as c, request, session, url
13 from pylons import config, tmpl_context as c, request, session, url
14 from pylons.controllers import WSGIController
14 from pylons.controllers import WSGIController
15 from pylons.controllers.util import redirect
15 from pylons.controllers.util import redirect
16 from pylons.templating import render_mako as render
16 from pylons.templating import render_mako as render
17
17
18 from rhodecode import __version__, BACKENDS
18 from rhodecode import __version__, BACKENDS
19
19
20 from rhodecode.lib.utils2 import str2bool, safe_unicode
20 from rhodecode.lib.utils2 import str2bool, safe_unicode
21 from rhodecode.lib.auth import AuthUser, get_container_username, authfunc,\
21 from rhodecode.lib.auth import AuthUser, get_container_username, authfunc,\
22 HasPermissionAnyMiddleware, CookieStoreWrapper
22 HasPermissionAnyMiddleware, CookieStoreWrapper
23 from rhodecode.lib.utils import get_repo_slug, invalidate_cache
23 from rhodecode.lib.utils import get_repo_slug, invalidate_cache
24 from rhodecode.model import meta
24 from rhodecode.model import meta
25
25
26 from rhodecode.model.db import Repository
26 from rhodecode.model.db import Repository
27 from rhodecode.model.notification import NotificationModel
27 from rhodecode.model.notification import NotificationModel
28 from rhodecode.model.scm import ScmModel
28 from rhodecode.model.scm import ScmModel
29
29
30 log = logging.getLogger(__name__)
30 log = logging.getLogger(__name__)
31
31
32
32
33 def _get_ip_addr(environ):
33 def _get_ip_addr(environ):
34 proxy_key = 'HTTP_X_REAL_IP'
34 proxy_key = 'HTTP_X_REAL_IP'
35 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
35 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
36 def_key = 'REMOTE_ADDR'
36 def_key = 'REMOTE_ADDR'
37
37
38 return environ.get(proxy_key2,
38 return environ.get(proxy_key2,
39 environ.get(proxy_key, environ.get(def_key, '0.0.0.0'))
39 environ.get(proxy_key, environ.get(def_key, '0.0.0.0'))
40 )
40 )
41
41
42
42
43 class BasicAuth(AuthBasicAuthenticator):
43 class BasicAuth(AuthBasicAuthenticator):
44
44
45 def __init__(self, realm, authfunc, auth_http_code=None):
45 def __init__(self, realm, authfunc, auth_http_code=None):
46 self.realm = realm
46 self.realm = realm
47 self.authfunc = authfunc
47 self.authfunc = authfunc
48 self._rc_auth_http_code = auth_http_code
48 self._rc_auth_http_code = auth_http_code
49
49
50 def build_authentication(self):
50 def build_authentication(self):
51 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
51 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
52 if self._rc_auth_http_code and self._rc_auth_http_code == '403':
52 if self._rc_auth_http_code and self._rc_auth_http_code == '403':
53 # return 403 if alternative http return code is specified in
53 # return 403 if alternative http return code is specified in
54 # RhodeCode config
54 # RhodeCode config
55 return HTTPForbidden(headers=head)
55 return HTTPForbidden(headers=head)
56 return HTTPUnauthorized(headers=head)
56 return HTTPUnauthorized(headers=head)
57
57
58
58
59 class BaseVCSController(object):
59 class BaseVCSController(object):
60
60
61 def __init__(self, application, config):
61 def __init__(self, application, config):
62 self.application = application
62 self.application = application
63 self.config = config
63 self.config = config
64 # base path of repo locations
64 # base path of repo locations
65 self.basepath = self.config['base_path']
65 self.basepath = self.config['base_path']
66 #authenticate this mercurial request using authfunc
66 #authenticate this mercurial request using authfunc
67 self.authenticate = BasicAuth('', authfunc,
67 self.authenticate = BasicAuth('', authfunc,
68 config.get('auth_ret_code'))
68 config.get('auth_ret_code'))
69 self.ipaddr = '0.0.0.0'
69 self.ipaddr = '0.0.0.0'
70
70
71 def _handle_request(self, environ, start_response):
71 def _handle_request(self, environ, start_response):
72 raise NotImplementedError()
72 raise NotImplementedError()
73
73
74 def _get_by_id(self, repo_name):
74 def _get_by_id(self, repo_name):
75 """
75 """
76 Get's a special pattern _<ID> from clone url and tries to replace it
76 Get's a special pattern _<ID> from clone url and tries to replace it
77 with a repository_name for support of _<ID> non changable urls
77 with a repository_name for support of _<ID> non changable urls
78
78
79 :param repo_name:
79 :param repo_name:
80 """
80 """
81 try:
81 try:
82 data = repo_name.split('/')
82 data = repo_name.split('/')
83 if len(data) >= 2:
83 if len(data) >= 2:
84 by_id = data[1].split('_')
84 by_id = data[1].split('_')
85 if len(by_id) == 2 and by_id[1].isdigit():
85 if len(by_id) == 2 and by_id[1].isdigit():
86 _repo_name = Repository.get(by_id[1]).repo_name
86 _repo_name = Repository.get(by_id[1]).repo_name
87 data[1] = _repo_name
87 data[1] = _repo_name
88 except:
88 except:
89 log.debug('Failed to extract repo_name from id %s' % (
89 log.debug('Failed to extract repo_name from id %s' % (
90 traceback.format_exc()
90 traceback.format_exc()
91 )
91 )
92 )
92 )
93
93
94 return '/'.join(data)
94 return '/'.join(data)
95
95
96 def _invalidate_cache(self, repo_name):
96 def _invalidate_cache(self, repo_name):
97 """
97 """
98 Set's cache for this repository for invalidation on next access
98 Set's cache for this repository for invalidation on next access
99
99
100 :param repo_name: full repo name, also a cache key
100 :param repo_name: full repo name, also a cache key
101 """
101 """
102 invalidate_cache('get_repo_cached_%s' % repo_name)
102 invalidate_cache('get_repo_cached_%s' % repo_name)
103
103
104 def _check_permission(self, action, user, repo_name):
104 def _check_permission(self, action, user, repo_name):
105 """
105 """
106 Checks permissions using action (push/pull) user and repository
106 Checks permissions using action (push/pull) user and repository
107 name
107 name
108
108
109 :param action: push or pull action
109 :param action: push or pull action
110 :param user: user instance
110 :param user: user instance
111 :param repo_name: repository name
111 :param repo_name: repository name
112 """
112 """
113 if action == 'push':
113 if action == 'push':
114 if not HasPermissionAnyMiddleware('repository.write',
114 if not HasPermissionAnyMiddleware('repository.write',
115 'repository.admin')(user,
115 'repository.admin')(user,
116 repo_name):
116 repo_name):
117 return False
117 return False
118
118
119 else:
119 else:
120 #any other action need at least read permission
120 #any other action need at least read permission
121 if not HasPermissionAnyMiddleware('repository.read',
121 if not HasPermissionAnyMiddleware('repository.read',
122 'repository.write',
122 'repository.write',
123 'repository.admin')(user,
123 'repository.admin')(user,
124 repo_name):
124 repo_name):
125 return False
125 return False
126
126
127 return True
127 return True
128
128
129 def _get_ip_addr(self, environ):
129 def _get_ip_addr(self, environ):
130 return _get_ip_addr(environ)
130 return _get_ip_addr(environ)
131
131
132 def __call__(self, environ, start_response):
132 def __call__(self, environ, start_response):
133 start = time.time()
133 start = time.time()
134 try:
134 try:
135 return self._handle_request(environ, start_response)
135 return self._handle_request(environ, start_response)
136 finally:
136 finally:
137 log = logging.getLogger('rhodecode.' + self.__class__.__name__)
137 log = logging.getLogger('rhodecode.' + self.__class__.__name__)
138 log.debug('Request time: %.3fs' % (time.time() - start))
138 log.debug('Request time: %.3fs' % (time.time() - start))
139 meta.Session.remove()
139 meta.Session.remove()
140
140
141
141
142 class BaseController(WSGIController):
142 class BaseController(WSGIController):
143
143
144 def __before__(self):
144 def __before__(self):
145 c.rhodecode_version = __version__
145 c.rhodecode_version = __version__
146 c.rhodecode_instanceid = config.get('instance_id')
146 c.rhodecode_instanceid = config.get('instance_id')
147 c.rhodecode_name = config.get('rhodecode_title')
147 c.rhodecode_name = config.get('rhodecode_title')
148 c.use_gravatar = str2bool(config.get('use_gravatar'))
148 c.use_gravatar = str2bool(config.get('use_gravatar'))
149 c.ga_code = config.get('rhodecode_ga_code')
149 c.ga_code = config.get('rhodecode_ga_code')
150 c.repo_name = get_repo_slug(request)
150 c.repo_name = get_repo_slug(request)
151 c.backends = BACKENDS.keys()
151 c.backends = BACKENDS.keys()
152 c.unread_notifications = NotificationModel()\
152 c.unread_notifications = NotificationModel()\
153 .get_unread_cnt_for_user(c.rhodecode_user.user_id)
153 .get_unread_cnt_for_user(c.rhodecode_user.user_id)
154 self.cut_off_limit = int(config.get('cut_off_limit'))
154 self.cut_off_limit = int(config.get('cut_off_limit'))
155
155
156 self.sa = meta.Session
156 self.sa = meta.Session
157 self.scm_model = ScmModel(self.sa)
157 self.scm_model = ScmModel(self.sa)
158 self.ip_addr = ''
158 self.ip_addr = ''
159
159
160 def __call__(self, environ, start_response):
160 def __call__(self, environ, start_response):
161 """Invoke the Controller"""
161 """Invoke the Controller"""
162 # WSGIController.__call__ dispatches to the Controller method
162 # WSGIController.__call__ dispatches to the Controller method
163 # the request is routed to. This routing information is
163 # the request is routed to. This routing information is
164 # available in environ['pylons.routes_dict']
164 # available in environ['pylons.routes_dict']
165 start = time.time()
165 start = time.time()
166 try:
166 try:
167 self.ip_addr = _get_ip_addr(environ)
167 self.ip_addr = _get_ip_addr(environ)
168 # make sure that we update permissions each time we call controller
168 # make sure that we update permissions each time we call controller
169 api_key = request.GET.get('api_key')
169 api_key = request.GET.get('api_key')
170 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
170 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
171 user_id = cookie_store.get('user_id', None)
171 user_id = cookie_store.get('user_id', None)
172 username = get_container_username(environ, config)
172 username = get_container_username(environ, config)
173 auth_user = AuthUser(user_id, api_key, username)
173 auth_user = AuthUser(user_id, api_key, username)
174 request.user = auth_user
174 request.user = auth_user
175 self.rhodecode_user = c.rhodecode_user = auth_user
175 self.rhodecode_user = c.rhodecode_user = auth_user
176 if not self.rhodecode_user.is_authenticated and \
176 if not self.rhodecode_user.is_authenticated and \
177 self.rhodecode_user.user_id is not None:
177 self.rhodecode_user.user_id is not None:
178 self.rhodecode_user.set_authenticated(
178 self.rhodecode_user.set_authenticated(
179 cookie_store.get('is_authenticated')
179 cookie_store.get('is_authenticated')
180 )
180 )
181 log.info('User: %s accessed %s' % (
181 log.info('User: %s accessed %s' % (
182 auth_user, safe_unicode(environ.get('PATH_INFO')))
182 auth_user, safe_unicode(environ.get('PATH_INFO')))
183 )
183 )
184 return WSGIController.__call__(self, environ, start_response)
184 return WSGIController.__call__(self, environ, start_response)
185 finally:
185 finally:
186 log.info('Request to %s time: %.3fs' % (
186 log.info('Request to %s time: %.3fs' % (
187 safe_unicode(environ.get('PATH_INFO')), time.time() - start)
187 safe_unicode(environ.get('PATH_INFO')), time.time() - start)
188 )
188 )
189 meta.Session.remove()
189 meta.Session.remove()
190
190
191
191
192 class BaseRepoController(BaseController):
192 class BaseRepoController(BaseController):
193 """
193 """
194 Base class for controllers responsible for loading all needed data for
194 Base class for controllers responsible for loading all needed data for
195 repository loaded items are
195 repository loaded items are
196
196
197 c.rhodecode_repo: instance of scm repository
197 c.rhodecode_repo: instance of scm repository
198 c.rhodecode_db_repo: instance of db
198 c.rhodecode_db_repo: instance of db
199 c.repository_followers: number of followers
199 c.repository_followers: number of followers
200 c.repository_forks: number of forks
200 c.repository_forks: number of forks
201 """
201 """
202
202
203 def __before__(self):
203 def __before__(self):
204 super(BaseRepoController, self).__before__()
204 super(BaseRepoController, self).__before__()
205 if c.repo_name:
205 if c.repo_name:
206
206
207 dbr = c.rhodecode_db_repo = Repository.get_by_repo_name(c.repo_name)
207 dbr = c.rhodecode_db_repo = Repository.get_by_repo_name(c.repo_name)
208 c.rhodecode_repo = c.rhodecode_db_repo.scm_instance
208 c.rhodecode_repo = c.rhodecode_db_repo.scm_instance
209
209
210 if c.rhodecode_repo is None:
210 if c.rhodecode_repo is None:
211 log.error('%s this repository is present in database but it '
211 log.error('%s this repository is present in database but it '
212 'cannot be created as an scm instance', c.repo_name)
212 'cannot be created as an scm instance', c.repo_name)
213
213
214 redirect(url('home'))
214 redirect(url('home'))
215
215
216 # some globals counter for menu
216 # some globals counter for menu
217 c.repository_followers = self.scm_model.get_followers(dbr)
217 c.repository_followers = self.scm_model.get_followers(dbr)
218 c.repository_forks = self.scm_model.get_forks(dbr)
218 c.repository_forks = self.scm_model.get_forks(dbr)
219 c.repository_pull_requests = self.scm_model.get_pull_requests(dbr) No newline at end of file
219 c.repository_pull_requests = self.scm_model.get_pull_requests(dbr)
@@ -1,28 +1,28 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.model.db_1_4_0
3 rhodecode.model.db_1_4_0
4 ~~~~~~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~~~~~~
5
5
6 Database Models for RhodeCode <=1.4.X
6 Database Models for RhodeCode <=1.4.X
7
7
8 :created_on: Apr 08, 2010
8 :created_on: Apr 08, 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 #TODO: replace that will db.py content after 1.5 Release
26 #TODO: replace that will db.py content after 1.5 Release
27
27
28 from rhodecode.model.db import * No newline at end of file
28 from rhodecode.model.db import *
@@ -1,627 +1,627 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.lib.diffs
3 rhodecode.lib.diffs
4 ~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~
5
5
6 Set of diffing helpers, previously part of vcs
6 Set of diffing helpers, previously part of vcs
7
7
8
8
9 :created_on: Dec 4, 2011
9 :created_on: Dec 4, 2011
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 :original copyright: 2007-2008 by Armin Ronacher
12 :original copyright: 2007-2008 by Armin Ronacher
13 :license: GPLv3, see COPYING for more details.
13 :license: GPLv3, see COPYING for more details.
14 """
14 """
15 # This program is free software: you can redistribute it and/or modify
15 # This program is free software: you can redistribute it and/or modify
16 # it under the terms of the GNU General Public License as published by
16 # it under the terms of the GNU General Public License as published by
17 # the Free Software Foundation, either version 3 of the License, or
17 # the Free Software Foundation, either version 3 of the License, or
18 # (at your option) any later version.
18 # (at your option) any later version.
19 #
19 #
20 # This program is distributed in the hope that it will be useful,
20 # This program is distributed in the hope that it will be useful,
21 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 # but WITHOUT ANY WARRANTY; without even the implied warranty of
22 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23 # GNU General Public License for more details.
23 # GNU General Public License for more details.
24 #
24 #
25 # You should have received a copy of the GNU General Public License
25 # You should have received a copy of the GNU General Public License
26 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 # along with this program. If not, see <http://www.gnu.org/licenses/>.
27
27
28 import re
28 import re
29 import io
29 import io
30 import difflib
30 import difflib
31 import markupsafe
31 import markupsafe
32
32
33 from itertools import tee, imap
33 from itertools import tee, imap
34
34
35 from mercurial import patch
35 from mercurial import patch
36 from mercurial.mdiff import diffopts
36 from mercurial.mdiff import diffopts
37 from mercurial.bundlerepo import bundlerepository
37 from mercurial.bundlerepo import bundlerepository
38 from mercurial import localrepo
38 from mercurial import localrepo
39
39
40 from pylons.i18n.translation import _
40 from pylons.i18n.translation import _
41
41
42 from rhodecode.lib.vcs.exceptions import VCSError
42 from rhodecode.lib.vcs.exceptions import VCSError
43 from rhodecode.lib.vcs.nodes import FileNode, SubModuleNode
43 from rhodecode.lib.vcs.nodes import FileNode, SubModuleNode
44 from rhodecode.lib.helpers import escape
44 from rhodecode.lib.helpers import escape
45 from rhodecode.lib.utils import EmptyChangeset, make_ui
45 from rhodecode.lib.utils import EmptyChangeset, make_ui
46
46
47
47
48 def wrap_to_table(str_):
48 def wrap_to_table(str_):
49 return '''<table class="code-difftable">
49 return '''<table class="code-difftable">
50 <tr class="line no-comment">
50 <tr class="line no-comment">
51 <td class="lineno new"></td>
51 <td class="lineno new"></td>
52 <td class="code no-comment"><pre>%s</pre></td>
52 <td class="code no-comment"><pre>%s</pre></td>
53 </tr>
53 </tr>
54 </table>''' % str_
54 </table>''' % str_
55
55
56
56
57 def wrapped_diff(filenode_old, filenode_new, cut_off_limit=None,
57 def wrapped_diff(filenode_old, filenode_new, cut_off_limit=None,
58 ignore_whitespace=True, line_context=3,
58 ignore_whitespace=True, line_context=3,
59 enable_comments=False):
59 enable_comments=False):
60 """
60 """
61 returns a wrapped diff into a table, checks for cut_off_limit and presents
61 returns a wrapped diff into a table, checks for cut_off_limit and presents
62 proper message
62 proper message
63 """
63 """
64
64
65 if filenode_old is None:
65 if filenode_old is None:
66 filenode_old = FileNode(filenode_new.path, '', EmptyChangeset())
66 filenode_old = FileNode(filenode_new.path, '', EmptyChangeset())
67
67
68 if filenode_old.is_binary or filenode_new.is_binary:
68 if filenode_old.is_binary or filenode_new.is_binary:
69 diff = wrap_to_table(_('binary file'))
69 diff = wrap_to_table(_('binary file'))
70 stats = (0, 0)
70 stats = (0, 0)
71 size = 0
71 size = 0
72
72
73 elif cut_off_limit != -1 and (cut_off_limit is None or
73 elif cut_off_limit != -1 and (cut_off_limit is None or
74 (filenode_old.size < cut_off_limit and filenode_new.size < cut_off_limit)):
74 (filenode_old.size < cut_off_limit and filenode_new.size < cut_off_limit)):
75
75
76 f_gitdiff = get_gitdiff(filenode_old, filenode_new,
76 f_gitdiff = get_gitdiff(filenode_old, filenode_new,
77 ignore_whitespace=ignore_whitespace,
77 ignore_whitespace=ignore_whitespace,
78 context=line_context)
78 context=line_context)
79 diff_processor = DiffProcessor(f_gitdiff, format='gitdiff')
79 diff_processor = DiffProcessor(f_gitdiff, format='gitdiff')
80
80
81 diff = diff_processor.as_html(enable_comments=enable_comments)
81 diff = diff_processor.as_html(enable_comments=enable_comments)
82 stats = diff_processor.stat()
82 stats = diff_processor.stat()
83 size = len(diff or '')
83 size = len(diff or '')
84 else:
84 else:
85 diff = wrap_to_table(_('Changeset was too big and was cut off, use '
85 diff = wrap_to_table(_('Changeset was too big and was cut off, use '
86 'diff menu to display this diff'))
86 'diff menu to display this diff'))
87 stats = (0, 0)
87 stats = (0, 0)
88 size = 0
88 size = 0
89 if not diff:
89 if not diff:
90 submodules = filter(lambda o: isinstance(o, SubModuleNode),
90 submodules = filter(lambda o: isinstance(o, SubModuleNode),
91 [filenode_new, filenode_old])
91 [filenode_new, filenode_old])
92 if submodules:
92 if submodules:
93 diff = wrap_to_table(escape('Submodule %r' % submodules[0]))
93 diff = wrap_to_table(escape('Submodule %r' % submodules[0]))
94 else:
94 else:
95 diff = wrap_to_table(_('No changes detected'))
95 diff = wrap_to_table(_('No changes detected'))
96
96
97 cs1 = filenode_old.changeset.raw_id
97 cs1 = filenode_old.changeset.raw_id
98 cs2 = filenode_new.changeset.raw_id
98 cs2 = filenode_new.changeset.raw_id
99
99
100 return size, cs1, cs2, diff, stats
100 return size, cs1, cs2, diff, stats
101
101
102
102
103 def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True, context=3):
103 def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True, context=3):
104 """
104 """
105 Returns git style diff between given ``filenode_old`` and ``filenode_new``.
105 Returns git style diff between given ``filenode_old`` and ``filenode_new``.
106
106
107 :param ignore_whitespace: ignore whitespaces in diff
107 :param ignore_whitespace: ignore whitespaces in diff
108 """
108 """
109 # make sure we pass in default context
109 # make sure we pass in default context
110 context = context or 3
110 context = context or 3
111 submodules = filter(lambda o: isinstance(o, SubModuleNode),
111 submodules = filter(lambda o: isinstance(o, SubModuleNode),
112 [filenode_new, filenode_old])
112 [filenode_new, filenode_old])
113 if submodules:
113 if submodules:
114 return ''
114 return ''
115
115
116 for filenode in (filenode_old, filenode_new):
116 for filenode in (filenode_old, filenode_new):
117 if not isinstance(filenode, FileNode):
117 if not isinstance(filenode, FileNode):
118 raise VCSError("Given object should be FileNode object, not %s"
118 raise VCSError("Given object should be FileNode object, not %s"
119 % filenode.__class__)
119 % filenode.__class__)
120
120
121 repo = filenode_new.changeset.repository
121 repo = filenode_new.changeset.repository
122 old_raw_id = getattr(filenode_old.changeset, 'raw_id', repo.EMPTY_CHANGESET)
122 old_raw_id = getattr(filenode_old.changeset, 'raw_id', repo.EMPTY_CHANGESET)
123 new_raw_id = getattr(filenode_new.changeset, 'raw_id', repo.EMPTY_CHANGESET)
123 new_raw_id = getattr(filenode_new.changeset, 'raw_id', repo.EMPTY_CHANGESET)
124
124
125 vcs_gitdiff = repo.get_diff(old_raw_id, new_raw_id, filenode_new.path,
125 vcs_gitdiff = repo.get_diff(old_raw_id, new_raw_id, filenode_new.path,
126 ignore_whitespace, context)
126 ignore_whitespace, context)
127 return vcs_gitdiff
127 return vcs_gitdiff
128
128
129
129
130 class DiffProcessor(object):
130 class DiffProcessor(object):
131 """
131 """
132 Give it a unified diff and it returns a list of the files that were
132 Give it a unified diff and it returns a list of the files that were
133 mentioned in the diff together with a dict of meta information that
133 mentioned in the diff together with a dict of meta information that
134 can be used to render it in a HTML template.
134 can be used to render it in a HTML template.
135 """
135 """
136 _chunk_re = re.compile(r'@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
136 _chunk_re = re.compile(r'@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
137
137
138 def __init__(self, diff, differ='diff', format='gitdiff'):
138 def __init__(self, diff, differ='diff', format='gitdiff'):
139 """
139 """
140 :param diff: a text in diff format or generator
140 :param diff: a text in diff format or generator
141 :param format: format of diff passed, `udiff` or `gitdiff`
141 :param format: format of diff passed, `udiff` or `gitdiff`
142 """
142 """
143 if isinstance(diff, basestring):
143 if isinstance(diff, basestring):
144 diff = [diff]
144 diff = [diff]
145
145
146 self.__udiff = diff
146 self.__udiff = diff
147 self.__format = format
147 self.__format = format
148 self.adds = 0
148 self.adds = 0
149 self.removes = 0
149 self.removes = 0
150
150
151 if isinstance(self.__udiff, basestring):
151 if isinstance(self.__udiff, basestring):
152 self.lines = iter(self.__udiff.splitlines(1))
152 self.lines = iter(self.__udiff.splitlines(1))
153
153
154 elif self.__format == 'gitdiff':
154 elif self.__format == 'gitdiff':
155 udiff_copy = self.copy_iterator()
155 udiff_copy = self.copy_iterator()
156 self.lines = imap(self.escaper, self._parse_gitdiff(udiff_copy))
156 self.lines = imap(self.escaper, self._parse_gitdiff(udiff_copy))
157 else:
157 else:
158 udiff_copy = self.copy_iterator()
158 udiff_copy = self.copy_iterator()
159 self.lines = imap(self.escaper, udiff_copy)
159 self.lines = imap(self.escaper, udiff_copy)
160
160
161 # Select a differ.
161 # Select a differ.
162 if differ == 'difflib':
162 if differ == 'difflib':
163 self.differ = self._highlight_line_difflib
163 self.differ = self._highlight_line_difflib
164 else:
164 else:
165 self.differ = self._highlight_line_udiff
165 self.differ = self._highlight_line_udiff
166
166
167 def escaper(self, string):
167 def escaper(self, string):
168 return markupsafe.escape(string)
168 return markupsafe.escape(string)
169
169
170 def copy_iterator(self):
170 def copy_iterator(self):
171 """
171 """
172 make a fresh copy of generator, we should not iterate thru
172 make a fresh copy of generator, we should not iterate thru
173 an original as it's needed for repeating operations on
173 an original as it's needed for repeating operations on
174 this instance of DiffProcessor
174 this instance of DiffProcessor
175 """
175 """
176 self.__udiff, iterator_copy = tee(self.__udiff)
176 self.__udiff, iterator_copy = tee(self.__udiff)
177 return iterator_copy
177 return iterator_copy
178
178
179 def _extract_rev(self, line1, line2):
179 def _extract_rev(self, line1, line2):
180 """
180 """
181 Extract the operation (A/M/D), filename and revision hint from a line.
181 Extract the operation (A/M/D), filename and revision hint from a line.
182 """
182 """
183
183
184 try:
184 try:
185 if line1.startswith('--- ') and line2.startswith('+++ '):
185 if line1.startswith('--- ') and line2.startswith('+++ '):
186 l1 = line1[4:].split(None, 1)
186 l1 = line1[4:].split(None, 1)
187 old_filename = (l1[0].replace('a/', '', 1)
187 old_filename = (l1[0].replace('a/', '', 1)
188 if len(l1) >= 1 else None)
188 if len(l1) >= 1 else None)
189 old_rev = l1[1] if len(l1) == 2 else 'old'
189 old_rev = l1[1] if len(l1) == 2 else 'old'
190
190
191 l2 = line2[4:].split(None, 1)
191 l2 = line2[4:].split(None, 1)
192 new_filename = (l2[0].replace('b/', '', 1)
192 new_filename = (l2[0].replace('b/', '', 1)
193 if len(l1) >= 1 else None)
193 if len(l1) >= 1 else None)
194 new_rev = l2[1] if len(l2) == 2 else 'new'
194 new_rev = l2[1] if len(l2) == 2 else 'new'
195
195
196 filename = (old_filename
196 filename = (old_filename
197 if old_filename != '/dev/null' else new_filename)
197 if old_filename != '/dev/null' else new_filename)
198
198
199 operation = 'D' if new_filename == '/dev/null' else None
199 operation = 'D' if new_filename == '/dev/null' else None
200 if not operation:
200 if not operation:
201 operation = 'M' if old_filename != '/dev/null' else 'A'
201 operation = 'M' if old_filename != '/dev/null' else 'A'
202
202
203 return operation, filename, new_rev, old_rev
203 return operation, filename, new_rev, old_rev
204 except (ValueError, IndexError):
204 except (ValueError, IndexError):
205 pass
205 pass
206
206
207 return None, None, None, None
207 return None, None, None, None
208
208
209 def _parse_gitdiff(self, diffiterator):
209 def _parse_gitdiff(self, diffiterator):
210 def line_decoder(l):
210 def line_decoder(l):
211 if l.startswith('+') and not l.startswith('+++'):
211 if l.startswith('+') and not l.startswith('+++'):
212 self.adds += 1
212 self.adds += 1
213 elif l.startswith('-') and not l.startswith('---'):
213 elif l.startswith('-') and not l.startswith('---'):
214 self.removes += 1
214 self.removes += 1
215 return l.decode('utf8', 'replace')
215 return l.decode('utf8', 'replace')
216
216
217 output = list(diffiterator)
217 output = list(diffiterator)
218 size = len(output)
218 size = len(output)
219
219
220 if size == 2:
220 if size == 2:
221 l = []
221 l = []
222 l.extend([output[0]])
222 l.extend([output[0]])
223 l.extend(output[1].splitlines(1))
223 l.extend(output[1].splitlines(1))
224 return map(line_decoder, l)
224 return map(line_decoder, l)
225 elif size == 1:
225 elif size == 1:
226 return map(line_decoder, output[0].splitlines(1))
226 return map(line_decoder, output[0].splitlines(1))
227 elif size == 0:
227 elif size == 0:
228 return []
228 return []
229
229
230 raise Exception('wrong size of diff %s' % size)
230 raise Exception('wrong size of diff %s' % size)
231
231
232 def _highlight_line_difflib(self, line, next_):
232 def _highlight_line_difflib(self, line, next_):
233 """
233 """
234 Highlight inline changes in both lines.
234 Highlight inline changes in both lines.
235 """
235 """
236
236
237 if line['action'] == 'del':
237 if line['action'] == 'del':
238 old, new = line, next_
238 old, new = line, next_
239 else:
239 else:
240 old, new = next_, line
240 old, new = next_, line
241
241
242 oldwords = re.split(r'(\W)', old['line'])
242 oldwords = re.split(r'(\W)', old['line'])
243 newwords = re.split(r'(\W)', new['line'])
243 newwords = re.split(r'(\W)', new['line'])
244
244
245 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
245 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
246
246
247 oldfragments, newfragments = [], []
247 oldfragments, newfragments = [], []
248 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
248 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
249 oldfrag = ''.join(oldwords[i1:i2])
249 oldfrag = ''.join(oldwords[i1:i2])
250 newfrag = ''.join(newwords[j1:j2])
250 newfrag = ''.join(newwords[j1:j2])
251 if tag != 'equal':
251 if tag != 'equal':
252 if oldfrag:
252 if oldfrag:
253 oldfrag = '<del>%s</del>' % oldfrag
253 oldfrag = '<del>%s</del>' % oldfrag
254 if newfrag:
254 if newfrag:
255 newfrag = '<ins>%s</ins>' % newfrag
255 newfrag = '<ins>%s</ins>' % newfrag
256 oldfragments.append(oldfrag)
256 oldfragments.append(oldfrag)
257 newfragments.append(newfrag)
257 newfragments.append(newfrag)
258
258
259 old['line'] = "".join(oldfragments)
259 old['line'] = "".join(oldfragments)
260 new['line'] = "".join(newfragments)
260 new['line'] = "".join(newfragments)
261
261
262 def _highlight_line_udiff(self, line, next_):
262 def _highlight_line_udiff(self, line, next_):
263 """
263 """
264 Highlight inline changes in both lines.
264 Highlight inline changes in both lines.
265 """
265 """
266 start = 0
266 start = 0
267 limit = min(len(line['line']), len(next_['line']))
267 limit = min(len(line['line']), len(next_['line']))
268 while start < limit and line['line'][start] == next_['line'][start]:
268 while start < limit and line['line'][start] == next_['line'][start]:
269 start += 1
269 start += 1
270 end = -1
270 end = -1
271 limit -= start
271 limit -= start
272 while -end <= limit and line['line'][end] == next_['line'][end]:
272 while -end <= limit and line['line'][end] == next_['line'][end]:
273 end -= 1
273 end -= 1
274 end += 1
274 end += 1
275 if start or end:
275 if start or end:
276 def do(l):
276 def do(l):
277 last = end + len(l['line'])
277 last = end + len(l['line'])
278 if l['action'] == 'add':
278 if l['action'] == 'add':
279 tag = 'ins'
279 tag = 'ins'
280 else:
280 else:
281 tag = 'del'
281 tag = 'del'
282 l['line'] = '%s<%s>%s</%s>%s' % (
282 l['line'] = '%s<%s>%s</%s>%s' % (
283 l['line'][:start],
283 l['line'][:start],
284 tag,
284 tag,
285 l['line'][start:last],
285 l['line'][start:last],
286 tag,
286 tag,
287 l['line'][last:]
287 l['line'][last:]
288 )
288 )
289 do(line)
289 do(line)
290 do(next_)
290 do(next_)
291
291
292 def _parse_udiff(self, inline_diff=True):
292 def _parse_udiff(self, inline_diff=True):
293 """
293 """
294 Parse the diff an return data for the template.
294 Parse the diff an return data for the template.
295 """
295 """
296 lineiter = self.lines
296 lineiter = self.lines
297 files = []
297 files = []
298 try:
298 try:
299 line = lineiter.next()
299 line = lineiter.next()
300 while 1:
300 while 1:
301 # continue until we found the old file
301 # continue until we found the old file
302 if not line.startswith('--- '):
302 if not line.startswith('--- '):
303 line = lineiter.next()
303 line = lineiter.next()
304 continue
304 continue
305
305
306 chunks = []
306 chunks = []
307 stats = [0, 0]
307 stats = [0, 0]
308 operation, filename, old_rev, new_rev = \
308 operation, filename, old_rev, new_rev = \
309 self._extract_rev(line, lineiter.next())
309 self._extract_rev(line, lineiter.next())
310 files.append({
310 files.append({
311 'filename': filename,
311 'filename': filename,
312 'old_revision': old_rev,
312 'old_revision': old_rev,
313 'new_revision': new_rev,
313 'new_revision': new_rev,
314 'chunks': chunks,
314 'chunks': chunks,
315 'operation': operation,
315 'operation': operation,
316 'stats': stats,
316 'stats': stats,
317 })
317 })
318
318
319 line = lineiter.next()
319 line = lineiter.next()
320 while line:
320 while line:
321 match = self._chunk_re.match(line)
321 match = self._chunk_re.match(line)
322 if not match:
322 if not match:
323 break
323 break
324
324
325 lines = []
325 lines = []
326 chunks.append(lines)
326 chunks.append(lines)
327
327
328 old_line, old_end, new_line, new_end = \
328 old_line, old_end, new_line, new_end = \
329 [int(x or 1) for x in match.groups()[:-1]]
329 [int(x or 1) for x in match.groups()[:-1]]
330 old_line -= 1
330 old_line -= 1
331 new_line -= 1
331 new_line -= 1
332 gr = match.groups()
332 gr = match.groups()
333 context = len(gr) == 5
333 context = len(gr) == 5
334 old_end += old_line
334 old_end += old_line
335 new_end += new_line
335 new_end += new_line
336
336
337 if context:
337 if context:
338 # skip context only if it's first line
338 # skip context only if it's first line
339 if int(gr[0]) > 1:
339 if int(gr[0]) > 1:
340 lines.append({
340 lines.append({
341 'old_lineno': '...',
341 'old_lineno': '...',
342 'new_lineno': '...',
342 'new_lineno': '...',
343 'action': 'context',
343 'action': 'context',
344 'line': line,
344 'line': line,
345 })
345 })
346
346
347 line = lineiter.next()
347 line = lineiter.next()
348 while old_line < old_end or new_line < new_end:
348 while old_line < old_end or new_line < new_end:
349 if line:
349 if line:
350 command, line = line[0], line[1:]
350 command, line = line[0], line[1:]
351 else:
351 else:
352 command = ' '
352 command = ' '
353 affects_old = affects_new = False
353 affects_old = affects_new = False
354
354
355 # ignore those if we don't expect them
355 # ignore those if we don't expect them
356 if command in '#@':
356 if command in '#@':
357 continue
357 continue
358 elif command == '+':
358 elif command == '+':
359 affects_new = True
359 affects_new = True
360 action = 'add'
360 action = 'add'
361 stats[0] += 1
361 stats[0] += 1
362 elif command == '-':
362 elif command == '-':
363 affects_old = True
363 affects_old = True
364 action = 'del'
364 action = 'del'
365 stats[1] += 1
365 stats[1] += 1
366 else:
366 else:
367 affects_old = affects_new = True
367 affects_old = affects_new = True
368 action = 'unmod'
368 action = 'unmod'
369
369
370 if line.find('No newline at end of file') != -1:
370 if line.find('No newline at end of file') != -1:
371 lines.append({
371 lines.append({
372 'old_lineno': '...',
372 'old_lineno': '...',
373 'new_lineno': '...',
373 'new_lineno': '...',
374 'action': 'context',
374 'action': 'context',
375 'line': line
375 'line': line
376 })
376 })
377
377
378 else:
378 else:
379 old_line += affects_old
379 old_line += affects_old
380 new_line += affects_new
380 new_line += affects_new
381 lines.append({
381 lines.append({
382 'old_lineno': affects_old and old_line or '',
382 'old_lineno': affects_old and old_line or '',
383 'new_lineno': affects_new and new_line or '',
383 'new_lineno': affects_new and new_line or '',
384 'action': action,
384 'action': action,
385 'line': line
385 'line': line
386 })
386 })
387
387
388 line = lineiter.next()
388 line = lineiter.next()
389
389
390 except StopIteration:
390 except StopIteration:
391 pass
391 pass
392
392
393 sorter = lambda info: {'A': 0, 'M': 1, 'D': 2}.get(info['operation'])
393 sorter = lambda info: {'A': 0, 'M': 1, 'D': 2}.get(info['operation'])
394 if inline_diff is False:
394 if inline_diff is False:
395 return sorted(files, key=sorter)
395 return sorted(files, key=sorter)
396
396
397 # highlight inline changes
397 # highlight inline changes
398 for diff_data in files:
398 for diff_data in files:
399 for chunk in diff_data['chunks']:
399 for chunk in diff_data['chunks']:
400 lineiter = iter(chunk)
400 lineiter = iter(chunk)
401 try:
401 try:
402 while 1:
402 while 1:
403 line = lineiter.next()
403 line = lineiter.next()
404 if line['action'] != 'unmod':
404 if line['action'] != 'unmod':
405 nextline = lineiter.next()
405 nextline = lineiter.next()
406 if nextline['action'] in ['unmod', 'context'] or \
406 if nextline['action'] in ['unmod', 'context'] or \
407 nextline['action'] == line['action']:
407 nextline['action'] == line['action']:
408 continue
408 continue
409 self.differ(line, nextline)
409 self.differ(line, nextline)
410 except StopIteration:
410 except StopIteration:
411 pass
411 pass
412
412
413 return sorted(files, key=sorter)
413 return sorted(files, key=sorter)
414
414
415 def prepare(self, inline_diff=True):
415 def prepare(self, inline_diff=True):
416 """
416 """
417 Prepare the passed udiff for HTML rendering. It'l return a list
417 Prepare the passed udiff for HTML rendering. It'l return a list
418 of dicts
418 of dicts
419 """
419 """
420 return self._parse_udiff(inline_diff=inline_diff)
420 return self._parse_udiff(inline_diff=inline_diff)
421
421
422 def _safe_id(self, idstring):
422 def _safe_id(self, idstring):
423 """Make a string safe for including in an id attribute.
423 """Make a string safe for including in an id attribute.
424
424
425 The HTML spec says that id attributes 'must begin with
425 The HTML spec says that id attributes 'must begin with
426 a letter ([A-Za-z]) and may be followed by any number
426 a letter ([A-Za-z]) and may be followed by any number
427 of letters, digits ([0-9]), hyphens ("-"), underscores
427 of letters, digits ([0-9]), hyphens ("-"), underscores
428 ("_"), colons (":"), and periods (".")'. These regexps
428 ("_"), colons (":"), and periods (".")'. These regexps
429 are slightly over-zealous, in that they remove colons
429 are slightly over-zealous, in that they remove colons
430 and periods unnecessarily.
430 and periods unnecessarily.
431
431
432 Whitespace is transformed into underscores, and then
432 Whitespace is transformed into underscores, and then
433 anything which is not a hyphen or a character that
433 anything which is not a hyphen or a character that
434 matches \w (alphanumerics and underscore) is removed.
434 matches \w (alphanumerics and underscore) is removed.
435
435
436 """
436 """
437 # Transform all whitespace to underscore
437 # Transform all whitespace to underscore
438 idstring = re.sub(r'\s', "_", '%s' % idstring)
438 idstring = re.sub(r'\s', "_", '%s' % idstring)
439 # Remove everything that is not a hyphen or a member of \w
439 # Remove everything that is not a hyphen or a member of \w
440 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
440 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
441 return idstring
441 return idstring
442
442
443 def raw_diff(self):
443 def raw_diff(self):
444 """
444 """
445 Returns raw string as udiff
445 Returns raw string as udiff
446 """
446 """
447 udiff_copy = self.copy_iterator()
447 udiff_copy = self.copy_iterator()
448 if self.__format == 'gitdiff':
448 if self.__format == 'gitdiff':
449 udiff_copy = self._parse_gitdiff(udiff_copy)
449 udiff_copy = self._parse_gitdiff(udiff_copy)
450 return u''.join(udiff_copy)
450 return u''.join(udiff_copy)
451
451
452 def as_html(self, table_class='code-difftable', line_class='line',
452 def as_html(self, table_class='code-difftable', line_class='line',
453 new_lineno_class='lineno old', old_lineno_class='lineno new',
453 new_lineno_class='lineno old', old_lineno_class='lineno new',
454 code_class='code', enable_comments=False, diff_lines=None):
454 code_class='code', enable_comments=False, diff_lines=None):
455 """
455 """
456 Return given diff as html table with customized css classes
456 Return given diff as html table with customized css classes
457 """
457 """
458 def _link_to_if(condition, label, url):
458 def _link_to_if(condition, label, url):
459 """
459 """
460 Generates a link if condition is meet or just the label if not.
460 Generates a link if condition is meet or just the label if not.
461 """
461 """
462
462
463 if condition:
463 if condition:
464 return '''<a href="%(url)s">%(label)s</a>''' % {
464 return '''<a href="%(url)s">%(label)s</a>''' % {
465 'url': url,
465 'url': url,
466 'label': label
466 'label': label
467 }
467 }
468 else:
468 else:
469 return label
469 return label
470 if diff_lines is None:
470 if diff_lines is None:
471 diff_lines = self.prepare()
471 diff_lines = self.prepare()
472 _html_empty = True
472 _html_empty = True
473 _html = []
473 _html = []
474 _html.append('''<table class="%(table_class)s">\n''' % {
474 _html.append('''<table class="%(table_class)s">\n''' % {
475 'table_class': table_class
475 'table_class': table_class
476 })
476 })
477 for diff in diff_lines:
477 for diff in diff_lines:
478 for line in diff['chunks']:
478 for line in diff['chunks']:
479 _html_empty = False
479 _html_empty = False
480 for change in line:
480 for change in line:
481 _html.append('''<tr class="%(lc)s %(action)s">\n''' % {
481 _html.append('''<tr class="%(lc)s %(action)s">\n''' % {
482 'lc': line_class,
482 'lc': line_class,
483 'action': change['action']
483 'action': change['action']
484 })
484 })
485 anchor_old_id = ''
485 anchor_old_id = ''
486 anchor_new_id = ''
486 anchor_new_id = ''
487 anchor_old = "%(filename)s_o%(oldline_no)s" % {
487 anchor_old = "%(filename)s_o%(oldline_no)s" % {
488 'filename': self._safe_id(diff['filename']),
488 'filename': self._safe_id(diff['filename']),
489 'oldline_no': change['old_lineno']
489 'oldline_no': change['old_lineno']
490 }
490 }
491 anchor_new = "%(filename)s_n%(oldline_no)s" % {
491 anchor_new = "%(filename)s_n%(oldline_no)s" % {
492 'filename': self._safe_id(diff['filename']),
492 'filename': self._safe_id(diff['filename']),
493 'oldline_no': change['new_lineno']
493 'oldline_no': change['new_lineno']
494 }
494 }
495 cond_old = (change['old_lineno'] != '...' and
495 cond_old = (change['old_lineno'] != '...' and
496 change['old_lineno'])
496 change['old_lineno'])
497 cond_new = (change['new_lineno'] != '...' and
497 cond_new = (change['new_lineno'] != '...' and
498 change['new_lineno'])
498 change['new_lineno'])
499 if cond_old:
499 if cond_old:
500 anchor_old_id = 'id="%s"' % anchor_old
500 anchor_old_id = 'id="%s"' % anchor_old
501 if cond_new:
501 if cond_new:
502 anchor_new_id = 'id="%s"' % anchor_new
502 anchor_new_id = 'id="%s"' % anchor_new
503 ###########################################################
503 ###########################################################
504 # OLD LINE NUMBER
504 # OLD LINE NUMBER
505 ###########################################################
505 ###########################################################
506 _html.append('''\t<td %(a_id)s class="%(olc)s">''' % {
506 _html.append('''\t<td %(a_id)s class="%(olc)s">''' % {
507 'a_id': anchor_old_id,
507 'a_id': anchor_old_id,
508 'olc': old_lineno_class
508 'olc': old_lineno_class
509 })
509 })
510
510
511 _html.append('''%(link)s''' % {
511 _html.append('''%(link)s''' % {
512 'link': _link_to_if(True, change['old_lineno'],
512 'link': _link_to_if(True, change['old_lineno'],
513 '#%s' % anchor_old)
513 '#%s' % anchor_old)
514 })
514 })
515 _html.append('''</td>\n''')
515 _html.append('''</td>\n''')
516 ###########################################################
516 ###########################################################
517 # NEW LINE NUMBER
517 # NEW LINE NUMBER
518 ###########################################################
518 ###########################################################
519
519
520 _html.append('''\t<td %(a_id)s class="%(nlc)s">''' % {
520 _html.append('''\t<td %(a_id)s class="%(nlc)s">''' % {
521 'a_id': anchor_new_id,
521 'a_id': anchor_new_id,
522 'nlc': new_lineno_class
522 'nlc': new_lineno_class
523 })
523 })
524
524
525 _html.append('''%(link)s''' % {
525 _html.append('''%(link)s''' % {
526 'link': _link_to_if(True, change['new_lineno'],
526 'link': _link_to_if(True, change['new_lineno'],
527 '#%s' % anchor_new)
527 '#%s' % anchor_new)
528 })
528 })
529 _html.append('''</td>\n''')
529 _html.append('''</td>\n''')
530 ###########################################################
530 ###########################################################
531 # CODE
531 # CODE
532 ###########################################################
532 ###########################################################
533 comments = '' if enable_comments else 'no-comment'
533 comments = '' if enable_comments else 'no-comment'
534 _html.append('''\t<td class="%(cc)s %(inc)s">''' % {
534 _html.append('''\t<td class="%(cc)s %(inc)s">''' % {
535 'cc': code_class,
535 'cc': code_class,
536 'inc': comments
536 'inc': comments
537 })
537 })
538 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' % {
538 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' % {
539 'code': change['line']
539 'code': change['line']
540 })
540 })
541 _html.append('''\t</td>''')
541 _html.append('''\t</td>''')
542 _html.append('''\n</tr>\n''')
542 _html.append('''\n</tr>\n''')
543 _html.append('''</table>''')
543 _html.append('''</table>''')
544 if _html_empty:
544 if _html_empty:
545 return None
545 return None
546 return ''.join(_html)
546 return ''.join(_html)
547
547
548 def stat(self):
548 def stat(self):
549 """
549 """
550 Returns tuple of added, and removed lines for this instance
550 Returns tuple of added, and removed lines for this instance
551 """
551 """
552 return self.adds, self.removes
552 return self.adds, self.removes
553
553
554
554
555 class InMemoryBundleRepo(bundlerepository):
555 class InMemoryBundleRepo(bundlerepository):
556 def __init__(self, ui, path, bundlestream):
556 def __init__(self, ui, path, bundlestream):
557 self._tempparent = None
557 self._tempparent = None
558 localrepo.localrepository.__init__(self, ui, path)
558 localrepo.localrepository.__init__(self, ui, path)
559 self.ui.setconfig('phases', 'publish', False)
559 self.ui.setconfig('phases', 'publish', False)
560
560
561 self.bundle = bundlestream
561 self.bundle = bundlestream
562
562
563 # dict with the mapping 'filename' -> position in the bundle
563 # dict with the mapping 'filename' -> position in the bundle
564 self.bundlefilespos = {}
564 self.bundlefilespos = {}
565
565
566
566
567 def differ(org_repo, org_ref, other_repo, other_ref, discovery_data=None):
567 def differ(org_repo, org_ref, other_repo, other_ref, discovery_data=None):
568 """
568 """
569 General differ between branches, bookmarks or separate but releated
569 General differ between branches, bookmarks or separate but releated
570 repositories
570 repositories
571
571
572 :param org_repo:
572 :param org_repo:
573 :type org_repo:
573 :type org_repo:
574 :param org_ref:
574 :param org_ref:
575 :type org_ref:
575 :type org_ref:
576 :param other_repo:
576 :param other_repo:
577 :type other_repo:
577 :type other_repo:
578 :param other_ref:
578 :param other_ref:
579 :type other_ref:
579 :type other_ref:
580 """
580 """
581
581
582 bundlerepo = None
582 bundlerepo = None
583 ignore_whitespace = False
583 ignore_whitespace = False
584 context = 3
584 context = 3
585 org_repo = org_repo.scm_instance._repo
585 org_repo = org_repo.scm_instance._repo
586 other_repo = other_repo.scm_instance._repo
586 other_repo = other_repo.scm_instance._repo
587 opts = diffopts(git=True, ignorews=ignore_whitespace, context=context)
587 opts = diffopts(git=True, ignorews=ignore_whitespace, context=context)
588 org_ref = org_ref[1]
588 org_ref = org_ref[1]
589 other_ref = other_ref[1]
589 other_ref = other_ref[1]
590
590
591 if org_repo != other_repo:
591 if org_repo != other_repo:
592
592
593 common, incoming, rheads = discovery_data
593 common, incoming, rheads = discovery_data
594
594
595 # create a bundle (uncompressed if other repo is not local)
595 # create a bundle (uncompressed if other repo is not local)
596 if other_repo.capable('getbundle') and incoming:
596 if other_repo.capable('getbundle') and incoming:
597 # disable repo hooks here since it's just bundle !
597 # disable repo hooks here since it's just bundle !
598 # patch and reset hooks section of UI config to not run any
598 # patch and reset hooks section of UI config to not run any
599 # hooks on fetching archives with subrepos
599 # hooks on fetching archives with subrepos
600 for k, _ in other_repo.ui.configitems('hooks'):
600 for k, _ in other_repo.ui.configitems('hooks'):
601 other_repo.ui.setconfig('hooks', k, None)
601 other_repo.ui.setconfig('hooks', k, None)
602
602
603 unbundle = other_repo.getbundle('incoming', common=common,
603 unbundle = other_repo.getbundle('incoming', common=common,
604 heads=rheads)
604 heads=rheads)
605
605
606 buf = io.BytesIO()
606 buf = io.BytesIO()
607 while True:
607 while True:
608 chunk = unbundle._stream.read(1024 * 4)
608 chunk = unbundle._stream.read(1024 * 4)
609 if not chunk:
609 if not chunk:
610 break
610 break
611 buf.write(chunk)
611 buf.write(chunk)
612
612
613 buf.seek(0)
613 buf.seek(0)
614 # replace chunked _stream with data that can do tell() and seek()
614 # replace chunked _stream with data that can do tell() and seek()
615 unbundle._stream = buf
615 unbundle._stream = buf
616
616
617 ui = make_ui('db')
617 ui = make_ui('db')
618 bundlerepo = InMemoryBundleRepo(ui, path=org_repo.root,
618 bundlerepo = InMemoryBundleRepo(ui, path=org_repo.root,
619 bundlestream=unbundle)
619 bundlestream=unbundle)
620
620
621 return ''.join(patch.diff(bundlerepo or org_repo,
621 return ''.join(patch.diff(bundlerepo or org_repo,
622 node1=org_repo[org_ref].node(),
622 node1=org_repo[org_ref].node(),
623 node2=other_repo[other_ref].node(),
623 node2=other_repo[other_ref].node(),
624 opts=opts))
624 opts=opts))
625 else:
625 else:
626 return ''.join(patch.diff(org_repo, node1=org_ref, node2=other_ref,
626 return ''.join(patch.diff(org_repo, node1=org_ref, node2=other_ref,
627 opts=opts))
627 opts=opts))
@@ -1,613 +1,613 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 vcs.nodes
3 vcs.nodes
4 ~~~~~~~~~
4 ~~~~~~~~~
5
5
6 Module holding everything related to vcs nodes.
6 Module holding everything related to vcs nodes.
7
7
8 :created_on: Apr 8, 2010
8 :created_on: Apr 8, 2010
9 :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
9 :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
10 """
10 """
11 import os
11 import os
12 import stat
12 import stat
13 import posixpath
13 import posixpath
14 import mimetypes
14 import mimetypes
15
15
16 from pygments import lexers
16 from pygments import lexers
17
17
18 from rhodecode.lib.vcs.utils.lazy import LazyProperty
18 from rhodecode.lib.vcs.utils.lazy import LazyProperty
19 from rhodecode.lib.vcs.utils import safe_unicode, safe_str
19 from rhodecode.lib.vcs.utils import safe_unicode, safe_str
20 from rhodecode.lib.vcs.exceptions import NodeError
20 from rhodecode.lib.vcs.exceptions import NodeError
21 from rhodecode.lib.vcs.exceptions import RemovedFileNodeError
21 from rhodecode.lib.vcs.exceptions import RemovedFileNodeError
22 from rhodecode.lib.vcs.backends.base import EmptyChangeset
22 from rhodecode.lib.vcs.backends.base import EmptyChangeset
23
23
24
24
25 class NodeKind:
25 class NodeKind:
26 SUBMODULE = -1
26 SUBMODULE = -1
27 DIR = 1
27 DIR = 1
28 FILE = 2
28 FILE = 2
29
29
30
30
31 class NodeState:
31 class NodeState:
32 ADDED = u'added'
32 ADDED = u'added'
33 CHANGED = u'changed'
33 CHANGED = u'changed'
34 NOT_CHANGED = u'not changed'
34 NOT_CHANGED = u'not changed'
35 REMOVED = u'removed'
35 REMOVED = u'removed'
36
36
37
37
38 class NodeGeneratorBase(object):
38 class NodeGeneratorBase(object):
39 """
39 """
40 Base class for removed added and changed filenodes, it's a lazy generator
40 Base class for removed added and changed filenodes, it's a lazy generator
41 class that will create filenodes only on iteration or call
41 class that will create filenodes only on iteration or call
42
42
43 The len method doesn't need to create filenodes at all
43 The len method doesn't need to create filenodes at all
44 """
44 """
45
45
46 def __init__(self, current_paths, cs):
46 def __init__(self, current_paths, cs):
47 self.cs = cs
47 self.cs = cs
48 self.current_paths = current_paths
48 self.current_paths = current_paths
49
49
50 def __call__(self):
50 def __call__(self):
51 return [n for n in self]
51 return [n for n in self]
52
52
53 def __getslice__(self, i, j):
53 def __getslice__(self, i, j):
54 for p in self.current_paths[i:j]:
54 for p in self.current_paths[i:j]:
55 yield self.cs.get_node(p)
55 yield self.cs.get_node(p)
56
56
57 def __len__(self):
57 def __len__(self):
58 return len(self.current_paths)
58 return len(self.current_paths)
59
59
60 def __iter__(self):
60 def __iter__(self):
61 for p in self.current_paths:
61 for p in self.current_paths:
62 yield self.cs.get_node(p)
62 yield self.cs.get_node(p)
63
63
64
64
65 class AddedFileNodesGenerator(NodeGeneratorBase):
65 class AddedFileNodesGenerator(NodeGeneratorBase):
66 """
66 """
67 Class holding Added files for current changeset
67 Class holding Added files for current changeset
68 """
68 """
69 pass
69 pass
70
70
71
71
72 class ChangedFileNodesGenerator(NodeGeneratorBase):
72 class ChangedFileNodesGenerator(NodeGeneratorBase):
73 """
73 """
74 Class holding Changed files for current changeset
74 Class holding Changed files for current changeset
75 """
75 """
76 pass
76 pass
77
77
78
78
79 class RemovedFileNodesGenerator(NodeGeneratorBase):
79 class RemovedFileNodesGenerator(NodeGeneratorBase):
80 """
80 """
81 Class holding removed files for current changeset
81 Class holding removed files for current changeset
82 """
82 """
83 def __iter__(self):
83 def __iter__(self):
84 for p in self.current_paths:
84 for p in self.current_paths:
85 yield RemovedFileNode(path=p)
85 yield RemovedFileNode(path=p)
86
86
87 def __getslice__(self, i, j):
87 def __getslice__(self, i, j):
88 for p in self.current_paths[i:j]:
88 for p in self.current_paths[i:j]:
89 yield RemovedFileNode(path=p)
89 yield RemovedFileNode(path=p)
90
90
91
91
92 class Node(object):
92 class Node(object):
93 """
93 """
94 Simplest class representing file or directory on repository. SCM backends
94 Simplest class representing file or directory on repository. SCM backends
95 should use ``FileNode`` and ``DirNode`` subclasses rather than ``Node``
95 should use ``FileNode`` and ``DirNode`` subclasses rather than ``Node``
96 directly.
96 directly.
97
97
98 Node's ``path`` cannot start with slash as we operate on *relative* paths
98 Node's ``path`` cannot start with slash as we operate on *relative* paths
99 only. Moreover, every single node is identified by the ``path`` attribute,
99 only. Moreover, every single node is identified by the ``path`` attribute,
100 so it cannot end with slash, too. Otherwise, path could lead to mistakes.
100 so it cannot end with slash, too. Otherwise, path could lead to mistakes.
101 """
101 """
102
102
103 def __init__(self, path, kind):
103 def __init__(self, path, kind):
104 if path.startswith('/'):
104 if path.startswith('/'):
105 raise NodeError("Cannot initialize Node objects with slash at "
105 raise NodeError("Cannot initialize Node objects with slash at "
106 "the beginning as only relative paths are supported")
106 "the beginning as only relative paths are supported")
107 self.path = path.rstrip('/')
107 self.path = path.rstrip('/')
108 if path == '' and kind != NodeKind.DIR:
108 if path == '' and kind != NodeKind.DIR:
109 raise NodeError("Only DirNode and its subclasses may be "
109 raise NodeError("Only DirNode and its subclasses may be "
110 "initialized with empty path")
110 "initialized with empty path")
111 self.kind = kind
111 self.kind = kind
112 #self.dirs, self.files = [], []
112 #self.dirs, self.files = [], []
113 if self.is_root() and not self.is_dir():
113 if self.is_root() and not self.is_dir():
114 raise NodeError("Root node cannot be FILE kind")
114 raise NodeError("Root node cannot be FILE kind")
115
115
116 @LazyProperty
116 @LazyProperty
117 def parent(self):
117 def parent(self):
118 parent_path = self.get_parent_path()
118 parent_path = self.get_parent_path()
119 if parent_path:
119 if parent_path:
120 if self.changeset:
120 if self.changeset:
121 return self.changeset.get_node(parent_path)
121 return self.changeset.get_node(parent_path)
122 return DirNode(parent_path)
122 return DirNode(parent_path)
123 return None
123 return None
124
124
125 @LazyProperty
125 @LazyProperty
126 def unicode_path(self):
126 def unicode_path(self):
127 return safe_unicode(self.path)
127 return safe_unicode(self.path)
128
128
129 @LazyProperty
129 @LazyProperty
130 def name(self):
130 def name(self):
131 """
131 """
132 Returns name of the node so if its path
132 Returns name of the node so if its path
133 then only last part is returned.
133 then only last part is returned.
134 """
134 """
135 return safe_unicode(self.path.rstrip('/').split('/')[-1])
135 return safe_unicode(self.path.rstrip('/').split('/')[-1])
136
136
137 def _get_kind(self):
137 def _get_kind(self):
138 return self._kind
138 return self._kind
139
139
140 def _set_kind(self, kind):
140 def _set_kind(self, kind):
141 if hasattr(self, '_kind'):
141 if hasattr(self, '_kind'):
142 raise NodeError("Cannot change node's kind")
142 raise NodeError("Cannot change node's kind")
143 else:
143 else:
144 self._kind = kind
144 self._kind = kind
145 # Post setter check (path's trailing slash)
145 # Post setter check (path's trailing slash)
146 if self.path.endswith('/'):
146 if self.path.endswith('/'):
147 raise NodeError("Node's path cannot end with slash")
147 raise NodeError("Node's path cannot end with slash")
148
148
149 kind = property(_get_kind, _set_kind)
149 kind = property(_get_kind, _set_kind)
150
150
151 def __cmp__(self, other):
151 def __cmp__(self, other):
152 """
152 """
153 Comparator using name of the node, needed for quick list sorting.
153 Comparator using name of the node, needed for quick list sorting.
154 """
154 """
155 kind_cmp = cmp(self.kind, other.kind)
155 kind_cmp = cmp(self.kind, other.kind)
156 if kind_cmp:
156 if kind_cmp:
157 return kind_cmp
157 return kind_cmp
158 return cmp(self.name, other.name)
158 return cmp(self.name, other.name)
159
159
160 def __eq__(self, other):
160 def __eq__(self, other):
161 for attr in ['name', 'path', 'kind']:
161 for attr in ['name', 'path', 'kind']:
162 if getattr(self, attr) != getattr(other, attr):
162 if getattr(self, attr) != getattr(other, attr):
163 return False
163 return False
164 if self.is_file():
164 if self.is_file():
165 if self.content != other.content:
165 if self.content != other.content:
166 return False
166 return False
167 else:
167 else:
168 # For DirNode's check without entering each dir
168 # For DirNode's check without entering each dir
169 self_nodes_paths = list(sorted(n.path for n in self.nodes))
169 self_nodes_paths = list(sorted(n.path for n in self.nodes))
170 other_nodes_paths = list(sorted(n.path for n in self.nodes))
170 other_nodes_paths = list(sorted(n.path for n in self.nodes))
171 if self_nodes_paths != other_nodes_paths:
171 if self_nodes_paths != other_nodes_paths:
172 return False
172 return False
173 return True
173 return True
174
174
175 def __nq__(self, other):
175 def __nq__(self, other):
176 return not self.__eq__(other)
176 return not self.__eq__(other)
177
177
178 def __repr__(self):
178 def __repr__(self):
179 return '<%s %r>' % (self.__class__.__name__, self.path)
179 return '<%s %r>' % (self.__class__.__name__, self.path)
180
180
181 def __str__(self):
181 def __str__(self):
182 return self.__repr__()
182 return self.__repr__()
183
183
184 def __unicode__(self):
184 def __unicode__(self):
185 return self.name
185 return self.name
186
186
187 def get_parent_path(self):
187 def get_parent_path(self):
188 """
188 """
189 Returns node's parent path or empty string if node is root.
189 Returns node's parent path or empty string if node is root.
190 """
190 """
191 if self.is_root():
191 if self.is_root():
192 return ''
192 return ''
193 return posixpath.dirname(self.path.rstrip('/')) + '/'
193 return posixpath.dirname(self.path.rstrip('/')) + '/'
194
194
195 def is_file(self):
195 def is_file(self):
196 """
196 """
197 Returns ``True`` if node's kind is ``NodeKind.FILE``, ``False``
197 Returns ``True`` if node's kind is ``NodeKind.FILE``, ``False``
198 otherwise.
198 otherwise.
199 """
199 """
200 return self.kind == NodeKind.FILE
200 return self.kind == NodeKind.FILE
201
201
202 def is_dir(self):
202 def is_dir(self):
203 """
203 """
204 Returns ``True`` if node's kind is ``NodeKind.DIR``, ``False``
204 Returns ``True`` if node's kind is ``NodeKind.DIR``, ``False``
205 otherwise.
205 otherwise.
206 """
206 """
207 return self.kind == NodeKind.DIR
207 return self.kind == NodeKind.DIR
208
208
209 def is_root(self):
209 def is_root(self):
210 """
210 """
211 Returns ``True`` if node is a root node and ``False`` otherwise.
211 Returns ``True`` if node is a root node and ``False`` otherwise.
212 """
212 """
213 return self.kind == NodeKind.DIR and self.path == ''
213 return self.kind == NodeKind.DIR and self.path == ''
214
214
215 def is_submodule(self):
215 def is_submodule(self):
216 """
216 """
217 Returns ``True`` if node's kind is ``NodeKind.SUBMODULE``, ``False``
217 Returns ``True`` if node's kind is ``NodeKind.SUBMODULE``, ``False``
218 otherwise.
218 otherwise.
219 """
219 """
220 return self.kind == NodeKind.SUBMODULE
220 return self.kind == NodeKind.SUBMODULE
221
221
222 @LazyProperty
222 @LazyProperty
223 def added(self):
223 def added(self):
224 return self.state is NodeState.ADDED
224 return self.state is NodeState.ADDED
225
225
226 @LazyProperty
226 @LazyProperty
227 def changed(self):
227 def changed(self):
228 return self.state is NodeState.CHANGED
228 return self.state is NodeState.CHANGED
229
229
230 @LazyProperty
230 @LazyProperty
231 def not_changed(self):
231 def not_changed(self):
232 return self.state is NodeState.NOT_CHANGED
232 return self.state is NodeState.NOT_CHANGED
233
233
234 @LazyProperty
234 @LazyProperty
235 def removed(self):
235 def removed(self):
236 return self.state is NodeState.REMOVED
236 return self.state is NodeState.REMOVED
237
237
238
238
239 class FileNode(Node):
239 class FileNode(Node):
240 """
240 """
241 Class representing file nodes.
241 Class representing file nodes.
242
242
243 :attribute: path: path to the node, relative to repostiory's root
243 :attribute: path: path to the node, relative to repostiory's root
244 :attribute: content: if given arbitrary sets content of the file
244 :attribute: content: if given arbitrary sets content of the file
245 :attribute: changeset: if given, first time content is accessed, callback
245 :attribute: changeset: if given, first time content is accessed, callback
246 :attribute: mode: octal stat mode for a node. Default is 0100644.
246 :attribute: mode: octal stat mode for a node. Default is 0100644.
247 """
247 """
248
248
249 def __init__(self, path, content=None, changeset=None, mode=None):
249 def __init__(self, path, content=None, changeset=None, mode=None):
250 """
250 """
251 Only one of ``content`` and ``changeset`` may be given. Passing both
251 Only one of ``content`` and ``changeset`` may be given. Passing both
252 would raise ``NodeError`` exception.
252 would raise ``NodeError`` exception.
253
253
254 :param path: relative path to the node
254 :param path: relative path to the node
255 :param content: content may be passed to constructor
255 :param content: content may be passed to constructor
256 :param changeset: if given, will use it to lazily fetch content
256 :param changeset: if given, will use it to lazily fetch content
257 :param mode: octal representation of ST_MODE (i.e. 0100644)
257 :param mode: octal representation of ST_MODE (i.e. 0100644)
258 """
258 """
259
259
260 if content and changeset:
260 if content and changeset:
261 raise NodeError("Cannot use both content and changeset")
261 raise NodeError("Cannot use both content and changeset")
262 super(FileNode, self).__init__(path, kind=NodeKind.FILE)
262 super(FileNode, self).__init__(path, kind=NodeKind.FILE)
263 self.changeset = changeset
263 self.changeset = changeset
264 self._content = content
264 self._content = content
265 self._mode = mode or 0100644
265 self._mode = mode or 0100644
266
266
267 @LazyProperty
267 @LazyProperty
268 def mode(self):
268 def mode(self):
269 """
269 """
270 Returns lazily mode of the FileNode. If ``changeset`` is not set, would
270 Returns lazily mode of the FileNode. If ``changeset`` is not set, would
271 use value given at initialization or 0100644 (default).
271 use value given at initialization or 0100644 (default).
272 """
272 """
273 if self.changeset:
273 if self.changeset:
274 mode = self.changeset.get_file_mode(self.path)
274 mode = self.changeset.get_file_mode(self.path)
275 else:
275 else:
276 mode = self._mode
276 mode = self._mode
277 return mode
277 return mode
278
278
279 @property
279 @property
280 def content(self):
280 def content(self):
281 """
281 """
282 Returns lazily content of the FileNode. If possible, would try to
282 Returns lazily content of the FileNode. If possible, would try to
283 decode content from UTF-8.
283 decode content from UTF-8.
284 """
284 """
285 if self.changeset:
285 if self.changeset:
286 content = self.changeset.get_file_content(self.path)
286 content = self.changeset.get_file_content(self.path)
287 else:
287 else:
288 content = self._content
288 content = self._content
289
289
290 if bool(content and '\0' in content):
290 if bool(content and '\0' in content):
291 return content
291 return content
292 return safe_unicode(content)
292 return safe_unicode(content)
293
293
294 @LazyProperty
294 @LazyProperty
295 def size(self):
295 def size(self):
296 if self.changeset:
296 if self.changeset:
297 return self.changeset.get_file_size(self.path)
297 return self.changeset.get_file_size(self.path)
298 raise NodeError("Cannot retrieve size of the file without related "
298 raise NodeError("Cannot retrieve size of the file without related "
299 "changeset attribute")
299 "changeset attribute")
300
300
301 @LazyProperty
301 @LazyProperty
302 def message(self):
302 def message(self):
303 if self.changeset:
303 if self.changeset:
304 return self.last_changeset.message
304 return self.last_changeset.message
305 raise NodeError("Cannot retrieve message of the file without related "
305 raise NodeError("Cannot retrieve message of the file without related "
306 "changeset attribute")
306 "changeset attribute")
307
307
308 @LazyProperty
308 @LazyProperty
309 def last_changeset(self):
309 def last_changeset(self):
310 if self.changeset:
310 if self.changeset:
311 return self.changeset.get_file_changeset(self.path)
311 return self.changeset.get_file_changeset(self.path)
312 raise NodeError("Cannot retrieve last changeset of the file without "
312 raise NodeError("Cannot retrieve last changeset of the file without "
313 "related changeset attribute")
313 "related changeset attribute")
314
314
315 def get_mimetype(self):
315 def get_mimetype(self):
316 """
316 """
317 Mimetype is calculated based on the file's content. If ``_mimetype``
317 Mimetype is calculated based on the file's content. If ``_mimetype``
318 attribute is available, it will be returned (backends which store
318 attribute is available, it will be returned (backends which store
319 mimetypes or can easily recognize them, should set this private
319 mimetypes or can easily recognize them, should set this private
320 attribute to indicate that type should *NOT* be calculated).
320 attribute to indicate that type should *NOT* be calculated).
321 """
321 """
322 if hasattr(self, '_mimetype'):
322 if hasattr(self, '_mimetype'):
323 if (isinstance(self._mimetype, (tuple, list,)) and
323 if (isinstance(self._mimetype, (tuple, list,)) and
324 len(self._mimetype) == 2):
324 len(self._mimetype) == 2):
325 return self._mimetype
325 return self._mimetype
326 else:
326 else:
327 raise NodeError('given _mimetype attribute must be an 2 '
327 raise NodeError('given _mimetype attribute must be an 2 '
328 'element list or tuple')
328 'element list or tuple')
329
329
330 mtype, encoding = mimetypes.guess_type(self.name)
330 mtype, encoding = mimetypes.guess_type(self.name)
331
331
332 if mtype is None:
332 if mtype is None:
333 if self.is_binary:
333 if self.is_binary:
334 mtype = 'application/octet-stream'
334 mtype = 'application/octet-stream'
335 encoding = None
335 encoding = None
336 else:
336 else:
337 mtype = 'text/plain'
337 mtype = 'text/plain'
338 encoding = None
338 encoding = None
339 return mtype, encoding
339 return mtype, encoding
340
340
341 @LazyProperty
341 @LazyProperty
342 def mimetype(self):
342 def mimetype(self):
343 """
343 """
344 Wrapper around full mimetype info. It returns only type of fetched
344 Wrapper around full mimetype info. It returns only type of fetched
345 mimetype without the encoding part. use get_mimetype function to fetch
345 mimetype without the encoding part. use get_mimetype function to fetch
346 full set of (type,encoding)
346 full set of (type,encoding)
347 """
347 """
348 return self.get_mimetype()[0]
348 return self.get_mimetype()[0]
349
349
350 @LazyProperty
350 @LazyProperty
351 def mimetype_main(self):
351 def mimetype_main(self):
352 return self.mimetype.split('/')[0]
352 return self.mimetype.split('/')[0]
353
353
354 @LazyProperty
354 @LazyProperty
355 def lexer(self):
355 def lexer(self):
356 """
356 """
357 Returns pygment's lexer class. Would try to guess lexer taking file's
357 Returns pygment's lexer class. Would try to guess lexer taking file's
358 content, name and mimetype.
358 content, name and mimetype.
359 """
359 """
360 try:
360 try:
361 lexer = lexers.guess_lexer_for_filename(self.name, self.content)
361 lexer = lexers.guess_lexer_for_filename(self.name, self.content)
362 except lexers.ClassNotFound:
362 except lexers.ClassNotFound:
363 lexer = lexers.TextLexer()
363 lexer = lexers.TextLexer()
364 # returns first alias
364 # returns first alias
365 return lexer
365 return lexer
366
366
367 @LazyProperty
367 @LazyProperty
368 def lexer_alias(self):
368 def lexer_alias(self):
369 """
369 """
370 Returns first alias of the lexer guessed for this file.
370 Returns first alias of the lexer guessed for this file.
371 """
371 """
372 return self.lexer.aliases[0]
372 return self.lexer.aliases[0]
373
373
374 @LazyProperty
374 @LazyProperty
375 def history(self):
375 def history(self):
376 """
376 """
377 Returns a list of changeset for this file in which the file was changed
377 Returns a list of changeset for this file in which the file was changed
378 """
378 """
379 if self.changeset is None:
379 if self.changeset is None:
380 raise NodeError('Unable to get changeset for this FileNode')
380 raise NodeError('Unable to get changeset for this FileNode')
381 return self.changeset.get_file_history(self.path)
381 return self.changeset.get_file_history(self.path)
382
382
383 @LazyProperty
383 @LazyProperty
384 def annotate(self):
384 def annotate(self):
385 """
385 """
386 Returns a list of three element tuples with lineno,changeset and line
386 Returns a list of three element tuples with lineno,changeset and line
387 """
387 """
388 if self.changeset is None:
388 if self.changeset is None:
389 raise NodeError('Unable to get changeset for this FileNode')
389 raise NodeError('Unable to get changeset for this FileNode')
390 return self.changeset.get_file_annotate(self.path)
390 return self.changeset.get_file_annotate(self.path)
391
391
392 @LazyProperty
392 @LazyProperty
393 def state(self):
393 def state(self):
394 if not self.changeset:
394 if not self.changeset:
395 raise NodeError("Cannot check state of the node if it's not "
395 raise NodeError("Cannot check state of the node if it's not "
396 "linked with changeset")
396 "linked with changeset")
397 elif self.path in (node.path for node in self.changeset.added):
397 elif self.path in (node.path for node in self.changeset.added):
398 return NodeState.ADDED
398 return NodeState.ADDED
399 elif self.path in (node.path for node in self.changeset.changed):
399 elif self.path in (node.path for node in self.changeset.changed):
400 return NodeState.CHANGED
400 return NodeState.CHANGED
401 else:
401 else:
402 return NodeState.NOT_CHANGED
402 return NodeState.NOT_CHANGED
403
403
404 @property
404 @property
405 def is_binary(self):
405 def is_binary(self):
406 """
406 """
407 Returns True if file has binary content.
407 Returns True if file has binary content.
408 """
408 """
409 _bin = '\0' in self.content
409 _bin = '\0' in self.content
410 return _bin
410 return _bin
411
411
412 @LazyProperty
412 @LazyProperty
413 def extension(self):
413 def extension(self):
414 """Returns filenode extension"""
414 """Returns filenode extension"""
415 return self.name.split('.')[-1]
415 return self.name.split('.')[-1]
416
416
417 def is_executable(self):
417 def is_executable(self):
418 """
418 """
419 Returns ``True`` if file has executable flag turned on.
419 Returns ``True`` if file has executable flag turned on.
420 """
420 """
421 return bool(self.mode & stat.S_IXUSR)
421 return bool(self.mode & stat.S_IXUSR)
422
422
423 def __repr__(self):
423 def __repr__(self):
424 return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
424 return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
425 getattr(self.changeset, 'short_id', ''))
425 getattr(self.changeset, 'short_id', ''))
426
426
427
427
428 class RemovedFileNode(FileNode):
428 class RemovedFileNode(FileNode):
429 """
429 """
430 Dummy FileNode class - trying to access any public attribute except path,
430 Dummy FileNode class - trying to access any public attribute except path,
431 name, kind or state (or methods/attributes checking those two) would raise
431 name, kind or state (or methods/attributes checking those two) would raise
432 RemovedFileNodeError.
432 RemovedFileNodeError.
433 """
433 """
434 ALLOWED_ATTRIBUTES = [
434 ALLOWED_ATTRIBUTES = [
435 'name', 'path', 'state', 'is_root', 'is_file', 'is_dir', 'kind',
435 'name', 'path', 'state', 'is_root', 'is_file', 'is_dir', 'kind',
436 'added', 'changed', 'not_changed', 'removed'
436 'added', 'changed', 'not_changed', 'removed'
437 ]
437 ]
438
438
439 def __init__(self, path):
439 def __init__(self, path):
440 """
440 """
441 :param path: relative path to the node
441 :param path: relative path to the node
442 """
442 """
443 super(RemovedFileNode, self).__init__(path=path)
443 super(RemovedFileNode, self).__init__(path=path)
444
444
445 def __getattribute__(self, attr):
445 def __getattribute__(self, attr):
446 if attr.startswith('_') or attr in RemovedFileNode.ALLOWED_ATTRIBUTES:
446 if attr.startswith('_') or attr in RemovedFileNode.ALLOWED_ATTRIBUTES:
447 return super(RemovedFileNode, self).__getattribute__(attr)
447 return super(RemovedFileNode, self).__getattribute__(attr)
448 raise RemovedFileNodeError("Cannot access attribute %s on "
448 raise RemovedFileNodeError("Cannot access attribute %s on "
449 "RemovedFileNode" % attr)
449 "RemovedFileNode" % attr)
450
450
451 @LazyProperty
451 @LazyProperty
452 def state(self):
452 def state(self):
453 return NodeState.REMOVED
453 return NodeState.REMOVED
454
454
455
455
456 class DirNode(Node):
456 class DirNode(Node):
457 """
457 """
458 DirNode stores list of files and directories within this node.
458 DirNode stores list of files and directories within this node.
459 Nodes may be used standalone but within repository context they
459 Nodes may be used standalone but within repository context they
460 lazily fetch data within same repositorty's changeset.
460 lazily fetch data within same repositorty's changeset.
461 """
461 """
462
462
463 def __init__(self, path, nodes=(), changeset=None):
463 def __init__(self, path, nodes=(), changeset=None):
464 """
464 """
465 Only one of ``nodes`` and ``changeset`` may be given. Passing both
465 Only one of ``nodes`` and ``changeset`` may be given. Passing both
466 would raise ``NodeError`` exception.
466 would raise ``NodeError`` exception.
467
467
468 :param path: relative path to the node
468 :param path: relative path to the node
469 :param nodes: content may be passed to constructor
469 :param nodes: content may be passed to constructor
470 :param changeset: if given, will use it to lazily fetch content
470 :param changeset: if given, will use it to lazily fetch content
471 :param size: always 0 for ``DirNode``
471 :param size: always 0 for ``DirNode``
472 """
472 """
473 if nodes and changeset:
473 if nodes and changeset:
474 raise NodeError("Cannot use both nodes and changeset")
474 raise NodeError("Cannot use both nodes and changeset")
475 super(DirNode, self).__init__(path, NodeKind.DIR)
475 super(DirNode, self).__init__(path, NodeKind.DIR)
476 self.changeset = changeset
476 self.changeset = changeset
477 self._nodes = nodes
477 self._nodes = nodes
478
478
479 @LazyProperty
479 @LazyProperty
480 def content(self):
480 def content(self):
481 raise NodeError("%s represents a dir and has no ``content`` attribute"
481 raise NodeError("%s represents a dir and has no ``content`` attribute"
482 % self)
482 % self)
483
483
484 @LazyProperty
484 @LazyProperty
485 def nodes(self):
485 def nodes(self):
486 if self.changeset:
486 if self.changeset:
487 nodes = self.changeset.get_nodes(self.path)
487 nodes = self.changeset.get_nodes(self.path)
488 else:
488 else:
489 nodes = self._nodes
489 nodes = self._nodes
490 self._nodes_dict = dict((node.path, node) for node in nodes)
490 self._nodes_dict = dict((node.path, node) for node in nodes)
491 return sorted(nodes)
491 return sorted(nodes)
492
492
493 @LazyProperty
493 @LazyProperty
494 def files(self):
494 def files(self):
495 return sorted((node for node in self.nodes if node.is_file()))
495 return sorted((node for node in self.nodes if node.is_file()))
496
496
497 @LazyProperty
497 @LazyProperty
498 def dirs(self):
498 def dirs(self):
499 return sorted((node for node in self.nodes if node.is_dir()))
499 return sorted((node for node in self.nodes if node.is_dir()))
500
500
501 def __iter__(self):
501 def __iter__(self):
502 for node in self.nodes:
502 for node in self.nodes:
503 yield node
503 yield node
504
504
505 def get_node(self, path):
505 def get_node(self, path):
506 """
506 """
507 Returns node from within this particular ``DirNode``, so it is now
507 Returns node from within this particular ``DirNode``, so it is now
508 allowed to fetch, i.e. node located at 'docs/api/index.rst' from node
508 allowed to fetch, i.e. node located at 'docs/api/index.rst' from node
509 'docs'. In order to access deeper nodes one must fetch nodes between
509 'docs'. In order to access deeper nodes one must fetch nodes between
510 them first - this would work::
510 them first - this would work::
511
511
512 docs = root.get_node('docs')
512 docs = root.get_node('docs')
513 docs.get_node('api').get_node('index.rst')
513 docs.get_node('api').get_node('index.rst')
514
514
515 :param: path - relative to the current node
515 :param: path - relative to the current node
516
516
517 .. note::
517 .. note::
518 To access lazily (as in example above) node have to be initialized
518 To access lazily (as in example above) node have to be initialized
519 with related changeset object - without it node is out of
519 with related changeset object - without it node is out of
520 context and may know nothing about anything else than nearest
520 context and may know nothing about anything else than nearest
521 (located at same level) nodes.
521 (located at same level) nodes.
522 """
522 """
523 try:
523 try:
524 path = path.rstrip('/')
524 path = path.rstrip('/')
525 if path == '':
525 if path == '':
526 raise NodeError("Cannot retrieve node without path")
526 raise NodeError("Cannot retrieve node without path")
527 self.nodes # access nodes first in order to set _nodes_dict
527 self.nodes # access nodes first in order to set _nodes_dict
528 paths = path.split('/')
528 paths = path.split('/')
529 if len(paths) == 1:
529 if len(paths) == 1:
530 if not self.is_root():
530 if not self.is_root():
531 path = '/'.join((self.path, paths[0]))
531 path = '/'.join((self.path, paths[0]))
532 else:
532 else:
533 path = paths[0]
533 path = paths[0]
534 return self._nodes_dict[path]
534 return self._nodes_dict[path]
535 elif len(paths) > 1:
535 elif len(paths) > 1:
536 if self.changeset is None:
536 if self.changeset is None:
537 raise NodeError("Cannot access deeper "
537 raise NodeError("Cannot access deeper "
538 "nodes without changeset")
538 "nodes without changeset")
539 else:
539 else:
540 path1, path2 = paths[0], '/'.join(paths[1:])
540 path1, path2 = paths[0], '/'.join(paths[1:])
541 return self.get_node(path1).get_node(path2)
541 return self.get_node(path1).get_node(path2)
542 else:
542 else:
543 raise KeyError
543 raise KeyError
544 except KeyError:
544 except KeyError:
545 raise NodeError("Node does not exist at %s" % path)
545 raise NodeError("Node does not exist at %s" % path)
546
546
547 @LazyProperty
547 @LazyProperty
548 def state(self):
548 def state(self):
549 raise NodeError("Cannot access state of DirNode")
549 raise NodeError("Cannot access state of DirNode")
550
550
551 @LazyProperty
551 @LazyProperty
552 def size(self):
552 def size(self):
553 size = 0
553 size = 0
554 for root, dirs, files in self.changeset.walk(self.path):
554 for root, dirs, files in self.changeset.walk(self.path):
555 for f in files:
555 for f in files:
556 size += f.size
556 size += f.size
557
557
558 return size
558 return size
559
559
560 def __repr__(self):
560 def __repr__(self):
561 return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
561 return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
562 getattr(self.changeset, 'short_id', ''))
562 getattr(self.changeset, 'short_id', ''))
563
563
564
564
565 class RootNode(DirNode):
565 class RootNode(DirNode):
566 """
566 """
567 DirNode being the root node of the repository.
567 DirNode being the root node of the repository.
568 """
568 """
569
569
570 def __init__(self, nodes=(), changeset=None):
570 def __init__(self, nodes=(), changeset=None):
571 super(RootNode, self).__init__(path='', nodes=nodes,
571 super(RootNode, self).__init__(path='', nodes=nodes,
572 changeset=changeset)
572 changeset=changeset)
573
573
574 def __repr__(self):
574 def __repr__(self):
575 return '<%s>' % self.__class__.__name__
575 return '<%s>' % self.__class__.__name__
576
576
577
577
578 class SubModuleNode(Node):
578 class SubModuleNode(Node):
579 """
579 """
580 represents a SubModule of Git or SubRepo of Mercurial
580 represents a SubModule of Git or SubRepo of Mercurial
581 """
581 """
582 is_binary = False
582 is_binary = False
583 size = 0
583 size = 0
584
584
585 def __init__(self, name, url=None, changeset=None, alias=None):
585 def __init__(self, name, url=None, changeset=None, alias=None):
586 self.path = name
586 self.path = name
587 self.kind = NodeKind.SUBMODULE
587 self.kind = NodeKind.SUBMODULE
588 self.alias = alias
588 self.alias = alias
589 # we have to use emptyChangeset here since this can point to svn/git/hg
589 # we have to use emptyChangeset here since this can point to svn/git/hg
590 # submodules we cannot get from repository
590 # submodules we cannot get from repository
591 self.changeset = EmptyChangeset(str(changeset), alias=alias)
591 self.changeset = EmptyChangeset(str(changeset), alias=alias)
592 self.url = url or self._extract_submodule_url()
592 self.url = url or self._extract_submodule_url()
593
593
594 def __repr__(self):
594 def __repr__(self):
595 return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
595 return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
596 getattr(self.changeset, 'short_id', ''))
596 getattr(self.changeset, 'short_id', ''))
597
597
598 def _extract_submodule_url(self):
598 def _extract_submodule_url(self):
599 if self.alias == 'git':
599 if self.alias == 'git':
600 #TODO: find a way to parse gits submodule file and extract the
600 #TODO: find a way to parse gits submodule file and extract the
601 # linking URL
601 # linking URL
602 return self.path
602 return self.path
603 if self.alias == 'hg':
603 if self.alias == 'hg':
604 return self.path
604 return self.path
605
605
606 @LazyProperty
606 @LazyProperty
607 def name(self):
607 def name(self):
608 """
608 """
609 Returns name of the node so if its path
609 Returns name of the node so if its path
610 then only last part is returned.
610 then only last part is returned.
611 """
611 """
612 org = safe_unicode(self.path.rstrip('/').split('/')[-1])
612 org = safe_unicode(self.path.rstrip('/').split('/')[-1])
613 return u'%s @ %s' % (org, self.changeset.short_id)
613 return u'%s @ %s' % (org, self.changeset.short_id)
@@ -1,15 +1,15 b''
1 """
1 """
2 Mercurial libs compatibility
2 Mercurial libs compatibility
3 """
3 """
4
4
5 from mercurial import archival, merge as hg_merge, patch, ui
5 from mercurial import archival, merge as hg_merge, patch, ui
6 from mercurial.commands import clone, nullid, pull
6 from mercurial.commands import clone, nullid, pull
7 from mercurial.context import memctx, memfilectx
7 from mercurial.context import memctx, memfilectx
8 from mercurial.error import RepoError, RepoLookupError, Abort
8 from mercurial.error import RepoError, RepoLookupError, Abort
9 from mercurial.hgweb.common import get_contact
9 from mercurial.hgweb.common import get_contact
10 from mercurial.localrepo import localrepository
10 from mercurial.localrepo import localrepository
11 from mercurial.match import match
11 from mercurial.match import match
12 from mercurial.mdiff import diffopts
12 from mercurial.mdiff import diffopts
13 from mercurial.node import hex
13 from mercurial.node import hex
14 from mercurial.encoding import tolocal
14 from mercurial.encoding import tolocal
15 from mercurial import discovery No newline at end of file
15 from mercurial import discovery
@@ -1,139 +1,139 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.model.changeset_status
3 rhodecode.model.changeset_status
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5
5
6
6
7 :created_on: Apr 30, 2012
7 :created_on: Apr 30, 2012
8 :author: marcink
8 :author: marcink
9 :copyright: (C) 2011-2012 Marcin Kuzminski <marcin@python-works.com>
9 :copyright: (C) 2011-2012 Marcin Kuzminski <marcin@python-works.com>
10 :license: GPLv3, see COPYING for more details.
10 :license: GPLv3, see COPYING for more details.
11 """
11 """
12 # This program is free software: you can redistribute it and/or modify
12 # This program is free software: you can redistribute it and/or modify
13 # it under the terms of the GNU General Public License as published by
13 # it under the terms of the GNU General Public License as published by
14 # the Free Software Foundation, either version 3 of the License, or
14 # the Free Software Foundation, either version 3 of the License, or
15 # (at your option) any later version.
15 # (at your option) any later version.
16 #
16 #
17 # This program is distributed in the hope that it will be useful,
17 # This program is distributed in the hope that it will be useful,
18 # but WITHOUT ANY WARRANTY; without even the implied warranty of
18 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 # GNU General Public License for more details.
20 # GNU General Public License for more details.
21 #
21 #
22 # You should have received a copy of the GNU General Public License
22 # You should have received a copy of the GNU General Public License
23 # along with this program. If not, see <http://www.gnu.org/licenses/>.
23 # along with this program. If not, see <http://www.gnu.org/licenses/>.
24
24
25
25
26 import logging
26 import logging
27
27
28 from rhodecode.model import BaseModel
28 from rhodecode.model import BaseModel
29 from rhodecode.model.db import ChangesetStatus, PullRequest
29 from rhodecode.model.db import ChangesetStatus, PullRequest
30
30
31 log = logging.getLogger(__name__)
31 log = logging.getLogger(__name__)
32
32
33
33
34 class ChangesetStatusModel(BaseModel):
34 class ChangesetStatusModel(BaseModel):
35
35
36 def __get_changeset_status(self, changeset_status):
36 def __get_changeset_status(self, changeset_status):
37 return self._get_instance(ChangesetStatus, changeset_status)
37 return self._get_instance(ChangesetStatus, changeset_status)
38
38
39 def __get_pull_request(self, pull_request):
39 def __get_pull_request(self, pull_request):
40 return self._get_instance(PullRequest, pull_request)
40 return self._get_instance(PullRequest, pull_request)
41
41
42 def get_status(self, repo, revision=None, pull_request=None):
42 def get_status(self, repo, revision=None, pull_request=None):
43 """
43 """
44 Returns latest status of changeset for given revision or for given
44 Returns latest status of changeset for given revision or for given
45 pull request. Statuses are versioned inside a table itself and
45 pull request. Statuses are versioned inside a table itself and
46 version == 0 is always the current one
46 version == 0 is always the current one
47
47
48 :param repo:
48 :param repo:
49 :type repo:
49 :type repo:
50 :param revision: 40char hash or None
50 :param revision: 40char hash or None
51 :type revision: str
51 :type revision: str
52 :param pull_request: pull_request reference
52 :param pull_request: pull_request reference
53 :type:
53 :type:
54 """
54 """
55 repo = self._get_repo(repo)
55 repo = self._get_repo(repo)
56
56
57 q = ChangesetStatus.query()\
57 q = ChangesetStatus.query()\
58 .filter(ChangesetStatus.repo == repo)\
58 .filter(ChangesetStatus.repo == repo)\
59 .filter(ChangesetStatus.version == 0)
59 .filter(ChangesetStatus.version == 0)
60
60
61 if revision:
61 if revision:
62 q = q.filter(ChangesetStatus.revision == revision)
62 q = q.filter(ChangesetStatus.revision == revision)
63 elif pull_request:
63 elif pull_request:
64 pull_request = self.__get_pull_request(pull_request)
64 pull_request = self.__get_pull_request(pull_request)
65 q = q.filter(ChangesetStatus.pull_request == pull_request)
65 q = q.filter(ChangesetStatus.pull_request == pull_request)
66 else:
66 else:
67 raise Exception('Please specify revision or pull_request')
67 raise Exception('Please specify revision or pull_request')
68
68
69 # need to use first here since there can be multiple statuses
69 # need to use first here since there can be multiple statuses
70 # returned from pull_request
70 # returned from pull_request
71 status = q.first()
71 status = q.first()
72 status = status.status if status else status
72 status = status.status if status else status
73 st = status or ChangesetStatus.DEFAULT
73 st = status or ChangesetStatus.DEFAULT
74 return str(st)
74 return str(st)
75
75
76 def set_status(self, repo, status, user, comment, revision=None,
76 def set_status(self, repo, status, user, comment, revision=None,
77 pull_request=None):
77 pull_request=None):
78 """
78 """
79 Creates new status for changeset or updates the old ones bumping their
79 Creates new status for changeset or updates the old ones bumping their
80 version, leaving the current status at
80 version, leaving the current status at
81
81
82 :param repo:
82 :param repo:
83 :type repo:
83 :type repo:
84 :param revision:
84 :param revision:
85 :type revision:
85 :type revision:
86 :param status:
86 :param status:
87 :type status:
87 :type status:
88 :param user:
88 :param user:
89 :type user:
89 :type user:
90 :param comment:
90 :param comment:
91 :type comment:
91 :type comment:
92 """
92 """
93 repo = self._get_repo(repo)
93 repo = self._get_repo(repo)
94
94
95 q = ChangesetStatus.query()
95 q = ChangesetStatus.query()
96
96
97 if revision:
97 if revision:
98 q = q.filter(ChangesetStatus.repo == repo)
98 q = q.filter(ChangesetStatus.repo == repo)
99 q = q.filter(ChangesetStatus.revision == revision)
99 q = q.filter(ChangesetStatus.revision == revision)
100 elif pull_request:
100 elif pull_request:
101 pull_request = self.__get_pull_request(pull_request)
101 pull_request = self.__get_pull_request(pull_request)
102 q = q.filter(ChangesetStatus.repo == pull_request.org_repo)
102 q = q.filter(ChangesetStatus.repo == pull_request.org_repo)
103 q = q.filter(ChangesetStatus.pull_request == pull_request)
103 q = q.filter(ChangesetStatus.pull_request == pull_request)
104 cur_statuses = q.all()
104 cur_statuses = q.all()
105
105
106 if cur_statuses:
106 if cur_statuses:
107 for st in cur_statuses:
107 for st in cur_statuses:
108 st.version += 1
108 st.version += 1
109 self.sa.add(st)
109 self.sa.add(st)
110
110
111 def _create_status(user, repo, status, comment, revision, pull_request):
111 def _create_status(user, repo, status, comment, revision, pull_request):
112 new_status = ChangesetStatus()
112 new_status = ChangesetStatus()
113 new_status.author = self._get_user(user)
113 new_status.author = self._get_user(user)
114 new_status.repo = self._get_repo(repo)
114 new_status.repo = self._get_repo(repo)
115 new_status.status = status
115 new_status.status = status
116 new_status.comment = comment
116 new_status.comment = comment
117 new_status.revision = revision
117 new_status.revision = revision
118 new_status.pull_request = pull_request
118 new_status.pull_request = pull_request
119 return new_status
119 return new_status
120
120
121 if revision:
121 if revision:
122 new_status = _create_status(user=user, repo=repo, status=status,
122 new_status = _create_status(user=user, repo=repo, status=status,
123 comment=comment, revision=revision,
123 comment=comment, revision=revision,
124 pull_request=None)
124 pull_request=None)
125 self.sa.add(new_status)
125 self.sa.add(new_status)
126 return new_status
126 return new_status
127 elif pull_request:
127 elif pull_request:
128 #pull request can have more than one revision associated to it
128 #pull request can have more than one revision associated to it
129 #we need to create new version for each one
129 #we need to create new version for each one
130 new_statuses = []
130 new_statuses = []
131 repo = pull_request.org_repo
131 repo = pull_request.org_repo
132 for rev in pull_request.revisions:
132 for rev in pull_request.revisions:
133 new_status = _create_status(user=user, repo=repo,
133 new_status = _create_status(user=user, repo=repo,
134 status=status, comment=comment,
134 status=status, comment=comment,
135 revision=rev,
135 revision=rev,
136 pull_request=pull_request)
136 pull_request=pull_request)
137 new_statuses.append(new_status)
137 new_statuses.append(new_status)
138 self.sa.add(new_status)
138 self.sa.add(new_status)
139 return new_statuses
139 return new_statuses
@@ -1,211 +1,211 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.model.comment
3 rhodecode.model.comment
4 ~~~~~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~~~~~
5
5
6 comments model for RhodeCode
6 comments model for RhodeCode
7
7
8 :created_on: Nov 11, 2011
8 :created_on: Nov 11, 2011
9 :author: marcink
9 :author: marcink
10 :copyright: (C) 2011-2012 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2011-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 logging
26 import logging
27 import traceback
27 import traceback
28
28
29 from pylons.i18n.translation import _
29 from pylons.i18n.translation import _
30 from sqlalchemy.util.compat import defaultdict
30 from sqlalchemy.util.compat import defaultdict
31
31
32 from rhodecode.lib.utils2 import extract_mentioned_users, safe_unicode
32 from rhodecode.lib.utils2 import extract_mentioned_users, safe_unicode
33 from rhodecode.lib import helpers as h
33 from rhodecode.lib import helpers as h
34 from rhodecode.model import BaseModel
34 from rhodecode.model import BaseModel
35 from rhodecode.model.db import ChangesetComment, User, Repository, \
35 from rhodecode.model.db import ChangesetComment, User, Repository, \
36 Notification, PullRequest
36 Notification, PullRequest
37 from rhodecode.model.notification import NotificationModel
37 from rhodecode.model.notification import NotificationModel
38
38
39 log = logging.getLogger(__name__)
39 log = logging.getLogger(__name__)
40
40
41
41
42 class ChangesetCommentsModel(BaseModel):
42 class ChangesetCommentsModel(BaseModel):
43
43
44 def __get_changeset_comment(self, changeset_comment):
44 def __get_changeset_comment(self, changeset_comment):
45 return self._get_instance(ChangesetComment, changeset_comment)
45 return self._get_instance(ChangesetComment, changeset_comment)
46
46
47 def __get_pull_request(self, pull_request):
47 def __get_pull_request(self, pull_request):
48 return self._get_instance(PullRequest, pull_request)
48 return self._get_instance(PullRequest, pull_request)
49
49
50 def _extract_mentions(self, s):
50 def _extract_mentions(self, s):
51 user_objects = []
51 user_objects = []
52 for username in extract_mentioned_users(s):
52 for username in extract_mentioned_users(s):
53 user_obj = User.get_by_username(username, case_insensitive=True)
53 user_obj = User.get_by_username(username, case_insensitive=True)
54 if user_obj:
54 if user_obj:
55 user_objects.append(user_obj)
55 user_objects.append(user_obj)
56 return user_objects
56 return user_objects
57
57
58 def create(self, text, repo_id, user_id, revision=None, pull_request=None,
58 def create(self, text, repo_id, user_id, revision=None, pull_request=None,
59 f_path=None, line_no=None, status_change=None):
59 f_path=None, line_no=None, status_change=None):
60 """
60 """
61 Creates new comment for changeset or pull request.
61 Creates new comment for changeset or pull request.
62 IF status_change is not none this comment is associated with a
62 IF status_change is not none this comment is associated with a
63 status change of changeset or changesets associated with pull request
63 status change of changeset or changesets associated with pull request
64
64
65 :param text:
65 :param text:
66 :param repo_id:
66 :param repo_id:
67 :param user_id:
67 :param user_id:
68 :param revision:
68 :param revision:
69 :param pull_request:
69 :param pull_request:
70 :param f_path:
70 :param f_path:
71 :param line_no:
71 :param line_no:
72 :param status_change:
72 :param status_change:
73 """
73 """
74 if not text:
74 if not text:
75 return
75 return
76
76
77 repo = Repository.get(repo_id)
77 repo = Repository.get(repo_id)
78 comment = ChangesetComment()
78 comment = ChangesetComment()
79 comment.repo = repo
79 comment.repo = repo
80 comment.user_id = user_id
80 comment.user_id = user_id
81 comment.text = text
81 comment.text = text
82 comment.f_path = f_path
82 comment.f_path = f_path
83 comment.line_no = line_no
83 comment.line_no = line_no
84
84
85 if revision:
85 if revision:
86 cs = repo.scm_instance.get_changeset(revision)
86 cs = repo.scm_instance.get_changeset(revision)
87 desc = "%s - %s" % (cs.short_id, h.shorter(cs.message, 256))
87 desc = "%s - %s" % (cs.short_id, h.shorter(cs.message, 256))
88 author_email = cs.author_email
88 author_email = cs.author_email
89 comment.revision = revision
89 comment.revision = revision
90 elif pull_request:
90 elif pull_request:
91 pull_request = self.__get_pull_request(pull_request)
91 pull_request = self.__get_pull_request(pull_request)
92 comment.pull_request = pull_request
92 comment.pull_request = pull_request
93 desc = ''
93 desc = ''
94 else:
94 else:
95 raise Exception('Please specify revision or pull_request_id')
95 raise Exception('Please specify revision or pull_request_id')
96
96
97 self.sa.add(comment)
97 self.sa.add(comment)
98 self.sa.flush()
98 self.sa.flush()
99
99
100 # make notification
100 # make notification
101 line = ''
101 line = ''
102 body = text
102 body = text
103
103
104 #changeset
104 #changeset
105 if revision:
105 if revision:
106 if line_no:
106 if line_no:
107 line = _('on line %s') % line_no
107 line = _('on line %s') % line_no
108 subj = safe_unicode(
108 subj = safe_unicode(
109 h.link_to('Re commit: %(commit_desc)s %(line)s' % \
109 h.link_to('Re commit: %(commit_desc)s %(line)s' % \
110 {'commit_desc': desc, 'line': line},
110 {'commit_desc': desc, 'line': line},
111 h.url('changeset_home', repo_name=repo.repo_name,
111 h.url('changeset_home', repo_name=repo.repo_name,
112 revision=revision,
112 revision=revision,
113 anchor='comment-%s' % comment.comment_id,
113 anchor='comment-%s' % comment.comment_id,
114 qualified=True,
114 qualified=True,
115 )
115 )
116 )
116 )
117 )
117 )
118 notification_type = Notification.TYPE_CHANGESET_COMMENT
118 notification_type = Notification.TYPE_CHANGESET_COMMENT
119 # get the current participants of this changeset
119 # get the current participants of this changeset
120 recipients = ChangesetComment.get_users(revision=revision)
120 recipients = ChangesetComment.get_users(revision=revision)
121 # add changeset author if it's in rhodecode system
121 # add changeset author if it's in rhodecode system
122 recipients += [User.get_by_email(author_email)]
122 recipients += [User.get_by_email(author_email)]
123 #pull request
123 #pull request
124 elif pull_request:
124 elif pull_request:
125 #TODO: make this something usefull
125 #TODO: make this something usefull
126 subj = 'commented on pull request something...'
126 subj = 'commented on pull request something...'
127 notification_type = Notification.TYPE_PULL_REQUEST_COMMENT
127 notification_type = Notification.TYPE_PULL_REQUEST_COMMENT
128 # get the current participants of this pull request
128 # get the current participants of this pull request
129 recipients = ChangesetComment.get_users(pull_request_id=
129 recipients = ChangesetComment.get_users(pull_request_id=
130 pull_request.pull_request_id)
130 pull_request.pull_request_id)
131 # add pull request author
131 # add pull request author
132 recipients += [pull_request.author]
132 recipients += [pull_request.author]
133
133
134 # create notification objects, and emails
134 # create notification objects, and emails
135 NotificationModel().create(
135 NotificationModel().create(
136 created_by=user_id, subject=subj, body=body,
136 created_by=user_id, subject=subj, body=body,
137 recipients=recipients, type_=notification_type,
137 recipients=recipients, type_=notification_type,
138 email_kwargs={'status_change': status_change}
138 email_kwargs={'status_change': status_change}
139 )
139 )
140
140
141 mention_recipients = set(self._extract_mentions(body))\
141 mention_recipients = set(self._extract_mentions(body))\
142 .difference(recipients)
142 .difference(recipients)
143 if mention_recipients:
143 if mention_recipients:
144 subj = _('[Mention]') + ' ' + subj
144 subj = _('[Mention]') + ' ' + subj
145 NotificationModel().create(
145 NotificationModel().create(
146 created_by=user_id, subject=subj, body=body,
146 created_by=user_id, subject=subj, body=body,
147 recipients=mention_recipients,
147 recipients=mention_recipients,
148 type_=notification_type,
148 type_=notification_type,
149 email_kwargs={'status_change': status_change}
149 email_kwargs={'status_change': status_change}
150 )
150 )
151
151
152 return comment
152 return comment
153
153
154 def delete(self, comment):
154 def delete(self, comment):
155 """
155 """
156 Deletes given comment
156 Deletes given comment
157
157
158 :param comment_id:
158 :param comment_id:
159 """
159 """
160 comment = self.__get_changeset_comment(comment)
160 comment = self.__get_changeset_comment(comment)
161 self.sa.delete(comment)
161 self.sa.delete(comment)
162
162
163 return comment
163 return comment
164
164
165 def get_comments(self, repo_id, revision=None, pull_request=None):
165 def get_comments(self, repo_id, revision=None, pull_request=None):
166 """
166 """
167 Get's main comments based on revision or pull_request_id
167 Get's main comments based on revision or pull_request_id
168
168
169 :param repo_id:
169 :param repo_id:
170 :type repo_id:
170 :type repo_id:
171 :param revision:
171 :param revision:
172 :type revision:
172 :type revision:
173 :param pull_request:
173 :param pull_request:
174 :type pull_request:
174 :type pull_request:
175 """
175 """
176
176
177 q = ChangesetComment.query()\
177 q = ChangesetComment.query()\
178 .filter(ChangesetComment.repo_id == repo_id)\
178 .filter(ChangesetComment.repo_id == repo_id)\
179 .filter(ChangesetComment.line_no == None)\
179 .filter(ChangesetComment.line_no == None)\
180 .filter(ChangesetComment.f_path == None)
180 .filter(ChangesetComment.f_path == None)
181 if revision:
181 if revision:
182 q = q.filter(ChangesetComment.revision == revision)
182 q = q.filter(ChangesetComment.revision == revision)
183 elif pull_request:
183 elif pull_request:
184 pull_request = self.__get_pull_request(pull_request)
184 pull_request = self.__get_pull_request(pull_request)
185 q = q.filter(ChangesetComment.pull_request == pull_request)
185 q = q.filter(ChangesetComment.pull_request == pull_request)
186 else:
186 else:
187 raise Exception('Please specify revision or pull_request')
187 raise Exception('Please specify revision or pull_request')
188 return q.all()
188 return q.all()
189
189
190 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
190 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
191 q = self.sa.query(ChangesetComment)\
191 q = self.sa.query(ChangesetComment)\
192 .filter(ChangesetComment.repo_id == repo_id)\
192 .filter(ChangesetComment.repo_id == repo_id)\
193 .filter(ChangesetComment.line_no != None)\
193 .filter(ChangesetComment.line_no != None)\
194 .filter(ChangesetComment.f_path != None)\
194 .filter(ChangesetComment.f_path != None)\
195 .order_by(ChangesetComment.comment_id.asc())\
195 .order_by(ChangesetComment.comment_id.asc())\
196
196
197 if revision:
197 if revision:
198 q = q.filter(ChangesetComment.revision == revision)
198 q = q.filter(ChangesetComment.revision == revision)
199 elif pull_request:
199 elif pull_request:
200 pull_request = self.__get_pull_request(pull_request)
200 pull_request = self.__get_pull_request(pull_request)
201 q = q.filter(ChangesetComment.pull_request == pull_request)
201 q = q.filter(ChangesetComment.pull_request == pull_request)
202 else:
202 else:
203 raise Exception('Please specify revision or pull_request_id')
203 raise Exception('Please specify revision or pull_request_id')
204
204
205 comments = q.all()
205 comments = q.all()
206
206
207 paths = defaultdict(lambda: defaultdict(list))
207 paths = defaultdict(lambda: defaultdict(list))
208
208
209 for co in comments:
209 for co in comments:
210 paths[co.f_path][co.line_no].append(co)
210 paths[co.f_path][co.line_no].append(co)
211 return paths.items()
211 return paths.items()
@@ -1,613 +1,613 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.model.user
3 rhodecode.model.user
4 ~~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~~
5
5
6 users model for RhodeCode
6 users model for RhodeCode
7
7
8 :created_on: Apr 9, 2010
8 :created_on: Apr 9, 2010
9 :author: marcink
9 :author: marcink
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 :license: GPLv3, see COPYING for more details.
11 :license: GPLv3, see COPYING for more details.
12 """
12 """
13 # This program is free software: you can redistribute it and/or modify
13 # This program is free software: you can redistribute it and/or modify
14 # it under the terms of the GNU General Public License as published by
14 # it under the terms of the GNU General Public License as published by
15 # the Free Software Foundation, either version 3 of the License, or
15 # the Free Software Foundation, either version 3 of the License, or
16 # (at your option) any later version.
16 # (at your option) any later version.
17 #
17 #
18 # This program is distributed in the hope that it will be useful,
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # GNU General Public License for more details.
21 # GNU General Public License for more details.
22 #
22 #
23 # You should have received a copy of the GNU General Public License
23 # You should have received a copy of the GNU General Public License
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25
25
26 import logging
26 import logging
27 import traceback
27 import traceback
28
28
29 from pylons import url
29 from pylons import url
30 from pylons.i18n.translation import _
30 from pylons.i18n.translation import _
31
31
32 from rhodecode.lib.utils2 import safe_unicode, generate_api_key
32 from rhodecode.lib.utils2 import safe_unicode, generate_api_key
33 from rhodecode.lib.caching_query import FromCache
33 from rhodecode.lib.caching_query import FromCache
34
34
35 from rhodecode.model import BaseModel
35 from rhodecode.model import BaseModel
36 from rhodecode.model.db import User, UserRepoToPerm, Repository, Permission, \
36 from rhodecode.model.db import User, UserRepoToPerm, Repository, Permission, \
37 UserToPerm, UsersGroupRepoToPerm, UsersGroupToPerm, UsersGroupMember, \
37 UserToPerm, UsersGroupRepoToPerm, UsersGroupToPerm, UsersGroupMember, \
38 Notification, RepoGroup, UserRepoGroupToPerm, UsersGroupRepoGroupToPerm, \
38 Notification, RepoGroup, UserRepoGroupToPerm, UsersGroupRepoGroupToPerm, \
39 UserEmailMap
39 UserEmailMap
40 from rhodecode.lib.exceptions import DefaultUserException, \
40 from rhodecode.lib.exceptions import DefaultUserException, \
41 UserOwnsReposException
41 UserOwnsReposException
42
42
43 from sqlalchemy.exc import DatabaseError
43 from sqlalchemy.exc import DatabaseError
44
44
45 from sqlalchemy.orm import joinedload
45 from sqlalchemy.orm import joinedload
46
46
47 log = logging.getLogger(__name__)
47 log = logging.getLogger(__name__)
48
48
49
49
50 PERM_WEIGHTS = {
50 PERM_WEIGHTS = {
51 'repository.none': 0,
51 'repository.none': 0,
52 'repository.read': 1,
52 'repository.read': 1,
53 'repository.write': 3,
53 'repository.write': 3,
54 'repository.admin': 4,
54 'repository.admin': 4,
55 'group.none': 0,
55 'group.none': 0,
56 'group.read': 1,
56 'group.read': 1,
57 'group.write': 3,
57 'group.write': 3,
58 'group.admin': 4,
58 'group.admin': 4,
59 }
59 }
60
60
61
61
62 class UserModel(BaseModel):
62 class UserModel(BaseModel):
63
63
64 def get(self, user_id, cache=False):
64 def get(self, user_id, cache=False):
65 user = self.sa.query(User)
65 user = self.sa.query(User)
66 if cache:
66 if cache:
67 user = user.options(FromCache("sql_cache_short",
67 user = user.options(FromCache("sql_cache_short",
68 "get_user_%s" % user_id))
68 "get_user_%s" % user_id))
69 return user.get(user_id)
69 return user.get(user_id)
70
70
71 def get_user(self, user):
71 def get_user(self, user):
72 return self._get_user(user)
72 return self._get_user(user)
73
73
74 def get_by_username(self, username, cache=False, case_insensitive=False):
74 def get_by_username(self, username, cache=False, case_insensitive=False):
75
75
76 if case_insensitive:
76 if case_insensitive:
77 user = self.sa.query(User).filter(User.username.ilike(username))
77 user = self.sa.query(User).filter(User.username.ilike(username))
78 else:
78 else:
79 user = self.sa.query(User)\
79 user = self.sa.query(User)\
80 .filter(User.username == username)
80 .filter(User.username == username)
81 if cache:
81 if cache:
82 user = user.options(FromCache("sql_cache_short",
82 user = user.options(FromCache("sql_cache_short",
83 "get_user_%s" % username))
83 "get_user_%s" % username))
84 return user.scalar()
84 return user.scalar()
85
85
86 def get_by_api_key(self, api_key, cache=False):
86 def get_by_api_key(self, api_key, cache=False):
87 return User.get_by_api_key(api_key, cache)
87 return User.get_by_api_key(api_key, cache)
88
88
89 def create(self, form_data):
89 def create(self, form_data):
90 from rhodecode.lib.auth import get_crypt_password
90 from rhodecode.lib.auth import get_crypt_password
91 try:
91 try:
92 new_user = User()
92 new_user = User()
93 for k, v in form_data.items():
93 for k, v in form_data.items():
94 if k == 'password':
94 if k == 'password':
95 v = get_crypt_password(v)
95 v = get_crypt_password(v)
96 setattr(new_user, k, v)
96 setattr(new_user, k, v)
97
97
98 new_user.api_key = generate_api_key(form_data['username'])
98 new_user.api_key = generate_api_key(form_data['username'])
99 self.sa.add(new_user)
99 self.sa.add(new_user)
100 return new_user
100 return new_user
101 except:
101 except:
102 log.error(traceback.format_exc())
102 log.error(traceback.format_exc())
103 raise
103 raise
104
104
105 def create_or_update(self, username, password, email, name, lastname,
105 def create_or_update(self, username, password, email, name, lastname,
106 active=True, admin=False, ldap_dn=None):
106 active=True, admin=False, ldap_dn=None):
107 """
107 """
108 Creates a new instance if not found, or updates current one
108 Creates a new instance if not found, or updates current one
109
109
110 :param username:
110 :param username:
111 :param password:
111 :param password:
112 :param email:
112 :param email:
113 :param active:
113 :param active:
114 :param name:
114 :param name:
115 :param lastname:
115 :param lastname:
116 :param active:
116 :param active:
117 :param admin:
117 :param admin:
118 :param ldap_dn:
118 :param ldap_dn:
119 """
119 """
120
120
121 from rhodecode.lib.auth import get_crypt_password
121 from rhodecode.lib.auth import get_crypt_password
122
122
123 log.debug('Checking for %s account in RhodeCode database' % username)
123 log.debug('Checking for %s account in RhodeCode database' % username)
124 user = User.get_by_username(username, case_insensitive=True)
124 user = User.get_by_username(username, case_insensitive=True)
125 if user is None:
125 if user is None:
126 log.debug('creating new user %s' % username)
126 log.debug('creating new user %s' % username)
127 new_user = User()
127 new_user = User()
128 else:
128 else:
129 log.debug('updating user %s' % username)
129 log.debug('updating user %s' % username)
130 new_user = user
130 new_user = user
131
131
132 try:
132 try:
133 new_user.username = username
133 new_user.username = username
134 new_user.admin = admin
134 new_user.admin = admin
135 new_user.password = get_crypt_password(password)
135 new_user.password = get_crypt_password(password)
136 new_user.api_key = generate_api_key(username)
136 new_user.api_key = generate_api_key(username)
137 new_user.email = email
137 new_user.email = email
138 new_user.active = active
138 new_user.active = active
139 new_user.ldap_dn = safe_unicode(ldap_dn) if ldap_dn else None
139 new_user.ldap_dn = safe_unicode(ldap_dn) if ldap_dn else None
140 new_user.name = name
140 new_user.name = name
141 new_user.lastname = lastname
141 new_user.lastname = lastname
142 self.sa.add(new_user)
142 self.sa.add(new_user)
143 return new_user
143 return new_user
144 except (DatabaseError,):
144 except (DatabaseError,):
145 log.error(traceback.format_exc())
145 log.error(traceback.format_exc())
146 raise
146 raise
147
147
148 def create_for_container_auth(self, username, attrs):
148 def create_for_container_auth(self, username, attrs):
149 """
149 """
150 Creates the given user if it's not already in the database
150 Creates the given user if it's not already in the database
151
151
152 :param username:
152 :param username:
153 :param attrs:
153 :param attrs:
154 """
154 """
155 if self.get_by_username(username, case_insensitive=True) is None:
155 if self.get_by_username(username, case_insensitive=True) is None:
156
156
157 # autogenerate email for container account without one
157 # autogenerate email for container account without one
158 generate_email = lambda usr: '%s@container_auth.account' % usr
158 generate_email = lambda usr: '%s@container_auth.account' % usr
159
159
160 try:
160 try:
161 new_user = User()
161 new_user = User()
162 new_user.username = username
162 new_user.username = username
163 new_user.password = None
163 new_user.password = None
164 new_user.api_key = generate_api_key(username)
164 new_user.api_key = generate_api_key(username)
165 new_user.email = attrs['email']
165 new_user.email = attrs['email']
166 new_user.active = attrs.get('active', True)
166 new_user.active = attrs.get('active', True)
167 new_user.name = attrs['name'] or generate_email(username)
167 new_user.name = attrs['name'] or generate_email(username)
168 new_user.lastname = attrs['lastname']
168 new_user.lastname = attrs['lastname']
169
169
170 self.sa.add(new_user)
170 self.sa.add(new_user)
171 return new_user
171 return new_user
172 except (DatabaseError,):
172 except (DatabaseError,):
173 log.error(traceback.format_exc())
173 log.error(traceback.format_exc())
174 self.sa.rollback()
174 self.sa.rollback()
175 raise
175 raise
176 log.debug('User %s already exists. Skipping creation of account'
176 log.debug('User %s already exists. Skipping creation of account'
177 ' for container auth.', username)
177 ' for container auth.', username)
178 return None
178 return None
179
179
180 def create_ldap(self, username, password, user_dn, attrs):
180 def create_ldap(self, username, password, user_dn, attrs):
181 """
181 """
182 Checks if user is in database, if not creates this user marked
182 Checks if user is in database, if not creates this user marked
183 as ldap user
183 as ldap user
184
184
185 :param username:
185 :param username:
186 :param password:
186 :param password:
187 :param user_dn:
187 :param user_dn:
188 :param attrs:
188 :param attrs:
189 """
189 """
190 from rhodecode.lib.auth import get_crypt_password
190 from rhodecode.lib.auth import get_crypt_password
191 log.debug('Checking for such ldap account in RhodeCode database')
191 log.debug('Checking for such ldap account in RhodeCode database')
192 if self.get_by_username(username, case_insensitive=True) is None:
192 if self.get_by_username(username, case_insensitive=True) is None:
193
193
194 # autogenerate email for ldap account without one
194 # autogenerate email for ldap account without one
195 generate_email = lambda usr: '%s@ldap.account' % usr
195 generate_email = lambda usr: '%s@ldap.account' % usr
196
196
197 try:
197 try:
198 new_user = User()
198 new_user = User()
199 username = username.lower()
199 username = username.lower()
200 # add ldap account always lowercase
200 # add ldap account always lowercase
201 new_user.username = username
201 new_user.username = username
202 new_user.password = get_crypt_password(password)
202 new_user.password = get_crypt_password(password)
203 new_user.api_key = generate_api_key(username)
203 new_user.api_key = generate_api_key(username)
204 new_user.email = attrs['email'] or generate_email(username)
204 new_user.email = attrs['email'] or generate_email(username)
205 new_user.active = attrs.get('active', True)
205 new_user.active = attrs.get('active', True)
206 new_user.ldap_dn = safe_unicode(user_dn)
206 new_user.ldap_dn = safe_unicode(user_dn)
207 new_user.name = attrs['name']
207 new_user.name = attrs['name']
208 new_user.lastname = attrs['lastname']
208 new_user.lastname = attrs['lastname']
209
209
210 self.sa.add(new_user)
210 self.sa.add(new_user)
211 return new_user
211 return new_user
212 except (DatabaseError,):
212 except (DatabaseError,):
213 log.error(traceback.format_exc())
213 log.error(traceback.format_exc())
214 self.sa.rollback()
214 self.sa.rollback()
215 raise
215 raise
216 log.debug('this %s user exists skipping creation of ldap account',
216 log.debug('this %s user exists skipping creation of ldap account',
217 username)
217 username)
218 return None
218 return None
219
219
220 def create_registration(self, form_data):
220 def create_registration(self, form_data):
221 from rhodecode.model.notification import NotificationModel
221 from rhodecode.model.notification import NotificationModel
222
222
223 try:
223 try:
224 form_data['admin'] = False
224 form_data['admin'] = False
225 new_user = self.create(form_data)
225 new_user = self.create(form_data)
226
226
227 self.sa.add(new_user)
227 self.sa.add(new_user)
228 self.sa.flush()
228 self.sa.flush()
229
229
230 # notification to admins
230 # notification to admins
231 subject = _('new user registration')
231 subject = _('new user registration')
232 body = ('New user registration\n'
232 body = ('New user registration\n'
233 '---------------------\n'
233 '---------------------\n'
234 '- Username: %s\n'
234 '- Username: %s\n'
235 '- Full Name: %s\n'
235 '- Full Name: %s\n'
236 '- Email: %s\n')
236 '- Email: %s\n')
237 body = body % (new_user.username, new_user.full_name,
237 body = body % (new_user.username, new_user.full_name,
238 new_user.email)
238 new_user.email)
239 edit_url = url('edit_user', id=new_user.user_id, qualified=True)
239 edit_url = url('edit_user', id=new_user.user_id, qualified=True)
240 kw = {'registered_user_url': edit_url}
240 kw = {'registered_user_url': edit_url}
241 NotificationModel().create(created_by=new_user, subject=subject,
241 NotificationModel().create(created_by=new_user, subject=subject,
242 body=body, recipients=None,
242 body=body, recipients=None,
243 type_=Notification.TYPE_REGISTRATION,
243 type_=Notification.TYPE_REGISTRATION,
244 email_kwargs=kw)
244 email_kwargs=kw)
245
245
246 except:
246 except:
247 log.error(traceback.format_exc())
247 log.error(traceback.format_exc())
248 raise
248 raise
249
249
250 def update(self, user_id, form_data):
250 def update(self, user_id, form_data):
251 try:
251 try:
252 user = self.get(user_id, cache=False)
252 user = self.get(user_id, cache=False)
253 if user.username == 'default':
253 if user.username == 'default':
254 raise DefaultUserException(
254 raise DefaultUserException(
255 _("You can't Edit this user since it's"
255 _("You can't Edit this user since it's"
256 " crucial for entire application"))
256 " crucial for entire application"))
257
257
258 for k, v in form_data.items():
258 for k, v in form_data.items():
259 if k == 'new_password' and v != '':
259 if k == 'new_password' and v != '':
260 user.password = v
260 user.password = v
261 user.api_key = generate_api_key(user.username)
261 user.api_key = generate_api_key(user.username)
262 else:
262 else:
263 setattr(user, k, v)
263 setattr(user, k, v)
264
264
265 self.sa.add(user)
265 self.sa.add(user)
266 except:
266 except:
267 log.error(traceback.format_exc())
267 log.error(traceback.format_exc())
268 raise
268 raise
269
269
270 def update_my_account(self, user_id, form_data):
270 def update_my_account(self, user_id, form_data):
271 from rhodecode.lib.auth import get_crypt_password
271 from rhodecode.lib.auth import get_crypt_password
272 try:
272 try:
273 user = self.get(user_id, cache=False)
273 user = self.get(user_id, cache=False)
274 if user.username == 'default':
274 if user.username == 'default':
275 raise DefaultUserException(
275 raise DefaultUserException(
276 _("You can't Edit this user since it's"
276 _("You can't Edit this user since it's"
277 " crucial for entire application")
277 " crucial for entire application")
278 )
278 )
279 for k, v in form_data.items():
279 for k, v in form_data.items():
280 if k == 'new_password' and v != '':
280 if k == 'new_password' and v != '':
281 user.password = get_crypt_password(v)
281 user.password = get_crypt_password(v)
282 user.api_key = generate_api_key(user.username)
282 user.api_key = generate_api_key(user.username)
283 else:
283 else:
284 if k not in ['admin', 'active']:
284 if k not in ['admin', 'active']:
285 setattr(user, k, v)
285 setattr(user, k, v)
286
286
287 self.sa.add(user)
287 self.sa.add(user)
288 except:
288 except:
289 log.error(traceback.format_exc())
289 log.error(traceback.format_exc())
290 raise
290 raise
291
291
292 def delete(self, user):
292 def delete(self, user):
293 user = self._get_user(user)
293 user = self._get_user(user)
294
294
295 try:
295 try:
296 if user.username == 'default':
296 if user.username == 'default':
297 raise DefaultUserException(
297 raise DefaultUserException(
298 _(u"You can't remove this user since it's"
298 _(u"You can't remove this user since it's"
299 " crucial for entire application")
299 " crucial for entire application")
300 )
300 )
301 if user.repositories:
301 if user.repositories:
302 repos = [x.repo_name for x in user.repositories]
302 repos = [x.repo_name for x in user.repositories]
303 raise UserOwnsReposException(
303 raise UserOwnsReposException(
304 _(u'user "%s" still owns %s repositories and cannot be '
304 _(u'user "%s" still owns %s repositories and cannot be '
305 'removed. Switch owners or remove those repositories. %s')
305 'removed. Switch owners or remove those repositories. %s')
306 % (user.username, len(repos), ', '.join(repos))
306 % (user.username, len(repos), ', '.join(repos))
307 )
307 )
308 self.sa.delete(user)
308 self.sa.delete(user)
309 except:
309 except:
310 log.error(traceback.format_exc())
310 log.error(traceback.format_exc())
311 raise
311 raise
312
312
313 def reset_password_link(self, data):
313 def reset_password_link(self, data):
314 from rhodecode.lib.celerylib import tasks, run_task
314 from rhodecode.lib.celerylib import tasks, run_task
315 run_task(tasks.send_password_link, data['email'])
315 run_task(tasks.send_password_link, data['email'])
316
316
317 def reset_password(self, data):
317 def reset_password(self, data):
318 from rhodecode.lib.celerylib import tasks, run_task
318 from rhodecode.lib.celerylib import tasks, run_task
319 run_task(tasks.reset_user_password, data['email'])
319 run_task(tasks.reset_user_password, data['email'])
320
320
321 def fill_data(self, auth_user, user_id=None, api_key=None):
321 def fill_data(self, auth_user, user_id=None, api_key=None):
322 """
322 """
323 Fetches auth_user by user_id,or api_key if present.
323 Fetches auth_user by user_id,or api_key if present.
324 Fills auth_user attributes with those taken from database.
324 Fills auth_user attributes with those taken from database.
325 Additionally set's is_authenitated if lookup fails
325 Additionally set's is_authenitated if lookup fails
326 present in database
326 present in database
327
327
328 :param auth_user: instance of user to set attributes
328 :param auth_user: instance of user to set attributes
329 :param user_id: user id to fetch by
329 :param user_id: user id to fetch by
330 :param api_key: api key to fetch by
330 :param api_key: api key to fetch by
331 """
331 """
332 if user_id is None and api_key is None:
332 if user_id is None and api_key is None:
333 raise Exception('You need to pass user_id or api_key')
333 raise Exception('You need to pass user_id or api_key')
334
334
335 try:
335 try:
336 if api_key:
336 if api_key:
337 dbuser = self.get_by_api_key(api_key)
337 dbuser = self.get_by_api_key(api_key)
338 else:
338 else:
339 dbuser = self.get(user_id)
339 dbuser = self.get(user_id)
340
340
341 if dbuser is not None and dbuser.active:
341 if dbuser is not None and dbuser.active:
342 log.debug('filling %s data' % dbuser)
342 log.debug('filling %s data' % dbuser)
343 for k, v in dbuser.get_dict().items():
343 for k, v in dbuser.get_dict().items():
344 setattr(auth_user, k, v)
344 setattr(auth_user, k, v)
345 else:
345 else:
346 return False
346 return False
347
347
348 except:
348 except:
349 log.error(traceback.format_exc())
349 log.error(traceback.format_exc())
350 auth_user.is_authenticated = False
350 auth_user.is_authenticated = False
351 return False
351 return False
352
352
353 return True
353 return True
354
354
355 def fill_perms(self, user):
355 def fill_perms(self, user):
356 """
356 """
357 Fills user permission attribute with permissions taken from database
357 Fills user permission attribute with permissions taken from database
358 works for permissions given for repositories, and for permissions that
358 works for permissions given for repositories, and for permissions that
359 are granted to groups
359 are granted to groups
360
360
361 :param user: user instance to fill his perms
361 :param user: user instance to fill his perms
362 """
362 """
363 RK = 'repositories'
363 RK = 'repositories'
364 GK = 'repositories_groups'
364 GK = 'repositories_groups'
365 GLOBAL = 'global'
365 GLOBAL = 'global'
366 user.permissions[RK] = {}
366 user.permissions[RK] = {}
367 user.permissions[GK] = {}
367 user.permissions[GK] = {}
368 user.permissions[GLOBAL] = set()
368 user.permissions[GLOBAL] = set()
369
369
370 #======================================================================
370 #======================================================================
371 # fetch default permissions
371 # fetch default permissions
372 #======================================================================
372 #======================================================================
373 default_user = User.get_by_username('default', cache=True)
373 default_user = User.get_by_username('default', cache=True)
374 default_user_id = default_user.user_id
374 default_user_id = default_user.user_id
375
375
376 default_repo_perms = Permission.get_default_perms(default_user_id)
376 default_repo_perms = Permission.get_default_perms(default_user_id)
377 default_repo_groups_perms = Permission.get_default_group_perms(default_user_id)
377 default_repo_groups_perms = Permission.get_default_group_perms(default_user_id)
378
378
379 if user.is_admin:
379 if user.is_admin:
380 #==================================================================
380 #==================================================================
381 # admin user have all default rights for repositories
381 # admin user have all default rights for repositories
382 # and groups set to admin
382 # and groups set to admin
383 #==================================================================
383 #==================================================================
384 user.permissions[GLOBAL].add('hg.admin')
384 user.permissions[GLOBAL].add('hg.admin')
385
385
386 # repositories
386 # repositories
387 for perm in default_repo_perms:
387 for perm in default_repo_perms:
388 r_k = perm.UserRepoToPerm.repository.repo_name
388 r_k = perm.UserRepoToPerm.repository.repo_name
389 p = 'repository.admin'
389 p = 'repository.admin'
390 user.permissions[RK][r_k] = p
390 user.permissions[RK][r_k] = p
391
391
392 # repositories groups
392 # repositories groups
393 for perm in default_repo_groups_perms:
393 for perm in default_repo_groups_perms:
394 rg_k = perm.UserRepoGroupToPerm.group.group_name
394 rg_k = perm.UserRepoGroupToPerm.group.group_name
395 p = 'group.admin'
395 p = 'group.admin'
396 user.permissions[GK][rg_k] = p
396 user.permissions[GK][rg_k] = p
397 return user
397 return user
398
398
399 #==================================================================
399 #==================================================================
400 # set default permissions first for repositories and groups
400 # set default permissions first for repositories and groups
401 #==================================================================
401 #==================================================================
402 uid = user.user_id
402 uid = user.user_id
403
403
404 # default global permissions
404 # default global permissions
405 default_global_perms = self.sa.query(UserToPerm)\
405 default_global_perms = self.sa.query(UserToPerm)\
406 .filter(UserToPerm.user_id == default_user_id)
406 .filter(UserToPerm.user_id == default_user_id)
407
407
408 for perm in default_global_perms:
408 for perm in default_global_perms:
409 user.permissions[GLOBAL].add(perm.permission.permission_name)
409 user.permissions[GLOBAL].add(perm.permission.permission_name)
410
410
411 # defaults for repositories, taken from default user
411 # defaults for repositories, taken from default user
412 for perm in default_repo_perms:
412 for perm in default_repo_perms:
413 r_k = perm.UserRepoToPerm.repository.repo_name
413 r_k = perm.UserRepoToPerm.repository.repo_name
414 if perm.Repository.private and not (perm.Repository.user_id == uid):
414 if perm.Repository.private and not (perm.Repository.user_id == uid):
415 # disable defaults for private repos,
415 # disable defaults for private repos,
416 p = 'repository.none'
416 p = 'repository.none'
417 elif perm.Repository.user_id == uid:
417 elif perm.Repository.user_id == uid:
418 # set admin if owner
418 # set admin if owner
419 p = 'repository.admin'
419 p = 'repository.admin'
420 else:
420 else:
421 p = perm.Permission.permission_name
421 p = perm.Permission.permission_name
422
422
423 user.permissions[RK][r_k] = p
423 user.permissions[RK][r_k] = p
424
424
425 # defaults for repositories groups taken from default user permission
425 # defaults for repositories groups taken from default user permission
426 # on given group
426 # on given group
427 for perm in default_repo_groups_perms:
427 for perm in default_repo_groups_perms:
428 rg_k = perm.UserRepoGroupToPerm.group.group_name
428 rg_k = perm.UserRepoGroupToPerm.group.group_name
429 p = perm.Permission.permission_name
429 p = perm.Permission.permission_name
430 user.permissions[GK][rg_k] = p
430 user.permissions[GK][rg_k] = p
431
431
432 #==================================================================
432 #==================================================================
433 # overwrite defaults with user permissions if any found
433 # overwrite defaults with user permissions if any found
434 #==================================================================
434 #==================================================================
435
435
436 # user global permissions
436 # user global permissions
437 user_perms = self.sa.query(UserToPerm)\
437 user_perms = self.sa.query(UserToPerm)\
438 .options(joinedload(UserToPerm.permission))\
438 .options(joinedload(UserToPerm.permission))\
439 .filter(UserToPerm.user_id == uid).all()
439 .filter(UserToPerm.user_id == uid).all()
440
440
441 for perm in user_perms:
441 for perm in user_perms:
442 user.permissions[GLOBAL].add(perm.permission.permission_name)
442 user.permissions[GLOBAL].add(perm.permission.permission_name)
443
443
444 # user explicit permissions for repositories
444 # user explicit permissions for repositories
445 user_repo_perms = \
445 user_repo_perms = \
446 self.sa.query(UserRepoToPerm, Permission, Repository)\
446 self.sa.query(UserRepoToPerm, Permission, Repository)\
447 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
447 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
448 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
448 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
449 .filter(UserRepoToPerm.user_id == uid)\
449 .filter(UserRepoToPerm.user_id == uid)\
450 .all()
450 .all()
451
451
452 for perm in user_repo_perms:
452 for perm in user_repo_perms:
453 # set admin if owner
453 # set admin if owner
454 r_k = perm.UserRepoToPerm.repository.repo_name
454 r_k = perm.UserRepoToPerm.repository.repo_name
455 if perm.Repository.user_id == uid:
455 if perm.Repository.user_id == uid:
456 p = 'repository.admin'
456 p = 'repository.admin'
457 else:
457 else:
458 p = perm.Permission.permission_name
458 p = perm.Permission.permission_name
459 user.permissions[RK][r_k] = p
459 user.permissions[RK][r_k] = p
460
460
461 # USER GROUP
461 # USER GROUP
462 #==================================================================
462 #==================================================================
463 # check if user is part of user groups for this repository and
463 # check if user is part of user groups for this repository and
464 # fill in (or replace with higher) permissions
464 # fill in (or replace with higher) permissions
465 #==================================================================
465 #==================================================================
466
466
467 # users group global
467 # users group global
468 user_perms_from_users_groups = self.sa.query(UsersGroupToPerm)\
468 user_perms_from_users_groups = self.sa.query(UsersGroupToPerm)\
469 .options(joinedload(UsersGroupToPerm.permission))\
469 .options(joinedload(UsersGroupToPerm.permission))\
470 .join((UsersGroupMember, UsersGroupToPerm.users_group_id ==
470 .join((UsersGroupMember, UsersGroupToPerm.users_group_id ==
471 UsersGroupMember.users_group_id))\
471 UsersGroupMember.users_group_id))\
472 .filter(UsersGroupMember.user_id == uid).all()
472 .filter(UsersGroupMember.user_id == uid).all()
473
473
474 for perm in user_perms_from_users_groups:
474 for perm in user_perms_from_users_groups:
475 user.permissions[GLOBAL].add(perm.permission.permission_name)
475 user.permissions[GLOBAL].add(perm.permission.permission_name)
476
476
477 # users group for repositories permissions
477 # users group for repositories permissions
478 user_repo_perms_from_users_groups = \
478 user_repo_perms_from_users_groups = \
479 self.sa.query(UsersGroupRepoToPerm, Permission, Repository,)\
479 self.sa.query(UsersGroupRepoToPerm, Permission, Repository,)\
480 .join((Repository, UsersGroupRepoToPerm.repository_id == Repository.repo_id))\
480 .join((Repository, UsersGroupRepoToPerm.repository_id == Repository.repo_id))\
481 .join((Permission, UsersGroupRepoToPerm.permission_id == Permission.permission_id))\
481 .join((Permission, UsersGroupRepoToPerm.permission_id == Permission.permission_id))\
482 .join((UsersGroupMember, UsersGroupRepoToPerm.users_group_id == UsersGroupMember.users_group_id))\
482 .join((UsersGroupMember, UsersGroupRepoToPerm.users_group_id == UsersGroupMember.users_group_id))\
483 .filter(UsersGroupMember.user_id == uid)\
483 .filter(UsersGroupMember.user_id == uid)\
484 .all()
484 .all()
485
485
486 for perm in user_repo_perms_from_users_groups:
486 for perm in user_repo_perms_from_users_groups:
487 r_k = perm.UsersGroupRepoToPerm.repository.repo_name
487 r_k = perm.UsersGroupRepoToPerm.repository.repo_name
488 p = perm.Permission.permission_name
488 p = perm.Permission.permission_name
489 cur_perm = user.permissions[RK][r_k]
489 cur_perm = user.permissions[RK][r_k]
490 # overwrite permission only if it's greater than permission
490 # overwrite permission only if it's greater than permission
491 # given from other sources
491 # given from other sources
492 if PERM_WEIGHTS[p] > PERM_WEIGHTS[cur_perm]:
492 if PERM_WEIGHTS[p] > PERM_WEIGHTS[cur_perm]:
493 user.permissions[RK][r_k] = p
493 user.permissions[RK][r_k] = p
494
494
495 # REPO GROUP
495 # REPO GROUP
496 #==================================================================
496 #==================================================================
497 # get access for this user for repos group and override defaults
497 # get access for this user for repos group and override defaults
498 #==================================================================
498 #==================================================================
499
499
500 # user explicit permissions for repository
500 # user explicit permissions for repository
501 user_repo_groups_perms = \
501 user_repo_groups_perms = \
502 self.sa.query(UserRepoGroupToPerm, Permission, RepoGroup)\
502 self.sa.query(UserRepoGroupToPerm, Permission, RepoGroup)\
503 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
503 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
504 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
504 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
505 .filter(UserRepoGroupToPerm.user_id == uid)\
505 .filter(UserRepoGroupToPerm.user_id == uid)\
506 .all()
506 .all()
507
507
508 for perm in user_repo_groups_perms:
508 for perm in user_repo_groups_perms:
509 rg_k = perm.UserRepoGroupToPerm.group.group_name
509 rg_k = perm.UserRepoGroupToPerm.group.group_name
510 p = perm.Permission.permission_name
510 p = perm.Permission.permission_name
511 cur_perm = user.permissions[GK][rg_k]
511 cur_perm = user.permissions[GK][rg_k]
512 if PERM_WEIGHTS[p] > PERM_WEIGHTS[cur_perm]:
512 if PERM_WEIGHTS[p] > PERM_WEIGHTS[cur_perm]:
513 user.permissions[GK][rg_k] = p
513 user.permissions[GK][rg_k] = p
514
514
515 # REPO GROUP + USER GROUP
515 # REPO GROUP + USER GROUP
516 #==================================================================
516 #==================================================================
517 # check if user is part of user groups for this repo group and
517 # check if user is part of user groups for this repo group and
518 # fill in (or replace with higher) permissions
518 # fill in (or replace with higher) permissions
519 #==================================================================
519 #==================================================================
520
520
521 # users group for repositories permissions
521 # users group for repositories permissions
522 user_repo_group_perms_from_users_groups = \
522 user_repo_group_perms_from_users_groups = \
523 self.sa.query(UsersGroupRepoGroupToPerm, Permission, RepoGroup)\
523 self.sa.query(UsersGroupRepoGroupToPerm, Permission, RepoGroup)\
524 .join((RepoGroup, UsersGroupRepoGroupToPerm.group_id == RepoGroup.group_id))\
524 .join((RepoGroup, UsersGroupRepoGroupToPerm.group_id == RepoGroup.group_id))\
525 .join((Permission, UsersGroupRepoGroupToPerm.permission_id == Permission.permission_id))\
525 .join((Permission, UsersGroupRepoGroupToPerm.permission_id == Permission.permission_id))\
526 .join((UsersGroupMember, UsersGroupRepoGroupToPerm.users_group_id == UsersGroupMember.users_group_id))\
526 .join((UsersGroupMember, UsersGroupRepoGroupToPerm.users_group_id == UsersGroupMember.users_group_id))\
527 .filter(UsersGroupMember.user_id == uid)\
527 .filter(UsersGroupMember.user_id == uid)\
528 .all()
528 .all()
529
529
530 for perm in user_repo_group_perms_from_users_groups:
530 for perm in user_repo_group_perms_from_users_groups:
531 g_k = perm.UsersGroupRepoGroupToPerm.group.group_name
531 g_k = perm.UsersGroupRepoGroupToPerm.group.group_name
532 p = perm.Permission.permission_name
532 p = perm.Permission.permission_name
533 cur_perm = user.permissions[GK][g_k]
533 cur_perm = user.permissions[GK][g_k]
534 # overwrite permission only if it's greater than permission
534 # overwrite permission only if it's greater than permission
535 # given from other sources
535 # given from other sources
536 if PERM_WEIGHTS[p] > PERM_WEIGHTS[cur_perm]:
536 if PERM_WEIGHTS[p] > PERM_WEIGHTS[cur_perm]:
537 user.permissions[GK][g_k] = p
537 user.permissions[GK][g_k] = p
538
538
539 return user
539 return user
540
540
541 def has_perm(self, user, perm):
541 def has_perm(self, user, perm):
542 if not isinstance(perm, Permission):
542 if not isinstance(perm, Permission):
543 raise Exception('perm needs to be an instance of Permission class '
543 raise Exception('perm needs to be an instance of Permission class '
544 'got %s instead' % type(perm))
544 'got %s instead' % type(perm))
545
545
546 user = self._get_user(user)
546 user = self._get_user(user)
547
547
548 return UserToPerm.query().filter(UserToPerm.user == user)\
548 return UserToPerm.query().filter(UserToPerm.user == user)\
549 .filter(UserToPerm.permission == perm).scalar() is not None
549 .filter(UserToPerm.permission == perm).scalar() is not None
550
550
551 def grant_perm(self, user, perm):
551 def grant_perm(self, user, perm):
552 """
552 """
553 Grant user global permissions
553 Grant user global permissions
554
554
555 :param user:
555 :param user:
556 :param perm:
556 :param perm:
557 """
557 """
558 user = self._get_user(user)
558 user = self._get_user(user)
559 perm = self._get_perm(perm)
559 perm = self._get_perm(perm)
560 # if this permission is already granted skip it
560 # if this permission is already granted skip it
561 _perm = UserToPerm.query()\
561 _perm = UserToPerm.query()\
562 .filter(UserToPerm.user == user)\
562 .filter(UserToPerm.user == user)\
563 .filter(UserToPerm.permission == perm)\
563 .filter(UserToPerm.permission == perm)\
564 .scalar()
564 .scalar()
565 if _perm:
565 if _perm:
566 return
566 return
567 new = UserToPerm()
567 new = UserToPerm()
568 new.user = user
568 new.user = user
569 new.permission = perm
569 new.permission = perm
570 self.sa.add(new)
570 self.sa.add(new)
571
571
572 def revoke_perm(self, user, perm):
572 def revoke_perm(self, user, perm):
573 """
573 """
574 Revoke users global permissions
574 Revoke users global permissions
575
575
576 :param user:
576 :param user:
577 :param perm:
577 :param perm:
578 """
578 """
579 user = self._get_user(user)
579 user = self._get_user(user)
580 perm = self._get_perm(perm)
580 perm = self._get_perm(perm)
581
581
582 obj = UserToPerm.query()\
582 obj = UserToPerm.query()\
583 .filter(UserToPerm.user == user)\
583 .filter(UserToPerm.user == user)\
584 .filter(UserToPerm.permission == perm)\
584 .filter(UserToPerm.permission == perm)\
585 .scalar()
585 .scalar()
586 if obj:
586 if obj:
587 self.sa.delete(obj)
587 self.sa.delete(obj)
588
588
589 def add_extra_email(self, user, email):
589 def add_extra_email(self, user, email):
590 """
590 """
591 Adds email address to UserEmailMap
591 Adds email address to UserEmailMap
592
592
593 :param user:
593 :param user:
594 :param email:
594 :param email:
595 """
595 """
596 user = self._get_user(user)
596 user = self._get_user(user)
597 obj = UserEmailMap()
597 obj = UserEmailMap()
598 obj.user = user
598 obj.user = user
599 obj.email = email
599 obj.email = email
600 self.sa.add(obj)
600 self.sa.add(obj)
601 return obj
601 return obj
602
602
603 def delete_extra_email(self, user, email_id):
603 def delete_extra_email(self, user, email_id):
604 """
604 """
605 Removes email address from UserEmailMap
605 Removes email address from UserEmailMap
606
606
607 :param user:
607 :param user:
608 :param email_id:
608 :param email_id:
609 """
609 """
610 user = self._get_user(user)
610 user = self._get_user(user)
611 obj = UserEmailMap.query().get(email_id)
611 obj = UserEmailMap.query().get(email_id)
612 if obj:
612 if obj:
613 self.sa.delete(obj) No newline at end of file
613 self.sa.delete(obj)
@@ -1,251 +1,251 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <%inherit file="/base/base.html"/>
2 <%inherit file="/base/base.html"/>
3
3
4 <%def name="title()">
4 <%def name="title()">
5 ${_('Edit user')} ${c.user.username} - ${c.rhodecode_name}
5 ${_('Edit user')} ${c.user.username} - ${c.rhodecode_name}
6 </%def>
6 </%def>
7
7
8 <%def name="breadcrumbs_links()">
8 <%def name="breadcrumbs_links()">
9 ${h.link_to(_('Admin'),h.url('admin_home'))}
9 ${h.link_to(_('Admin'),h.url('admin_home'))}
10 &raquo;
10 &raquo;
11 ${h.link_to(_('Users'),h.url('users'))}
11 ${h.link_to(_('Users'),h.url('users'))}
12 &raquo;
12 &raquo;
13 ${_('edit')} "${c.user.username}"
13 ${_('edit')} "${c.user.username}"
14 </%def>
14 </%def>
15
15
16 <%def name="page_nav()">
16 <%def name="page_nav()">
17 ${self.menu('admin')}
17 ${self.menu('admin')}
18 </%def>
18 </%def>
19
19
20 <%def name="main()">
20 <%def name="main()">
21 <div class="box box-left">
21 <div class="box box-left">
22 <!-- box / title -->
22 <!-- box / title -->
23 <div class="title">
23 <div class="title">
24 ${self.breadcrumbs()}
24 ${self.breadcrumbs()}
25 </div>
25 </div>
26 <!-- end box / title -->
26 <!-- end box / title -->
27 ${h.form(url('update_user', id=c.user.user_id),method='put')}
27 ${h.form(url('update_user', id=c.user.user_id),method='put')}
28 <div class="form">
28 <div class="form">
29 <div class="field">
29 <div class="field">
30 <div class="gravatar_box">
30 <div class="gravatar_box">
31 <div class="gravatar"><img alt="gravatar" src="${h.gravatar_url(c.user.email)}"/></div>
31 <div class="gravatar"><img alt="gravatar" src="${h.gravatar_url(c.user.email)}"/></div>
32 <p>
32 <p>
33 %if c.use_gravatar:
33 %if c.use_gravatar:
34 <strong>${_('Change your avatar at')} <a href="http://gravatar.com">gravatar.com</a></strong>
34 <strong>${_('Change your avatar at')} <a href="http://gravatar.com">gravatar.com</a></strong>
35 <br/>${_('Using')} ${c.user.email}
35 <br/>${_('Using')} ${c.user.email}
36 %else:
36 %else:
37 <br/>${c.user.email}
37 <br/>${c.user.email}
38 %endif
38 %endif
39 </div>
39 </div>
40 </div>
40 </div>
41 <div class="field">
41 <div class="field">
42 <div class="label">
42 <div class="label">
43 <label>${_('API key')}</label> ${c.user.api_key}
43 <label>${_('API key')}</label> ${c.user.api_key}
44 </div>
44 </div>
45 </div>
45 </div>
46
46
47 <div class="fields">
47 <div class="fields">
48 <div class="field">
48 <div class="field">
49 <div class="label">
49 <div class="label">
50 <label for="username">${_('Username')}:</label>
50 <label for="username">${_('Username')}:</label>
51 </div>
51 </div>
52 <div class="input">
52 <div class="input">
53 ${h.text('username',class_='medium')}
53 ${h.text('username',class_='medium')}
54 </div>
54 </div>
55 </div>
55 </div>
56
56
57 <div class="field">
57 <div class="field">
58 <div class="label">
58 <div class="label">
59 <label for="ldap_dn">${_('LDAP DN')}:</label>
59 <label for="ldap_dn">${_('LDAP DN')}:</label>
60 </div>
60 </div>
61 <div class="input">
61 <div class="input">
62 ${h.text('ldap_dn',class_='medium disabled',readonly="readonly")}
62 ${h.text('ldap_dn',class_='medium disabled',readonly="readonly")}
63 </div>
63 </div>
64 </div>
64 </div>
65
65
66 <div class="field">
66 <div class="field">
67 <div class="label">
67 <div class="label">
68 <label for="new_password">${_('New password')}:</label>
68 <label for="new_password">${_('New password')}:</label>
69 </div>
69 </div>
70 <div class="input">
70 <div class="input">
71 ${h.password('new_password',class_='medium',autocomplete="off")}
71 ${h.password('new_password',class_='medium',autocomplete="off")}
72 </div>
72 </div>
73 </div>
73 </div>
74
74
75 <div class="field">
75 <div class="field">
76 <div class="label">
76 <div class="label">
77 <label for="password_confirmation">${_('New password confirmation')}:</label>
77 <label for="password_confirmation">${_('New password confirmation')}:</label>
78 </div>
78 </div>
79 <div class="input">
79 <div class="input">
80 ${h.password('password_confirmation',class_="medium",autocomplete="off")}
80 ${h.password('password_confirmation',class_="medium",autocomplete="off")}
81 </div>
81 </div>
82 </div>
82 </div>
83
83
84 <div class="field">
84 <div class="field">
85 <div class="label">
85 <div class="label">
86 <label for="name">${_('First Name')}:</label>
86 <label for="name">${_('First Name')}:</label>
87 </div>
87 </div>
88 <div class="input">
88 <div class="input">
89 ${h.text('name',class_='medium')}
89 ${h.text('name',class_='medium')}
90 </div>
90 </div>
91 </div>
91 </div>
92
92
93 <div class="field">
93 <div class="field">
94 <div class="label">
94 <div class="label">
95 <label for="lastname">${_('Last Name')}:</label>
95 <label for="lastname">${_('Last Name')}:</label>
96 </div>
96 </div>
97 <div class="input">
97 <div class="input">
98 ${h.text('lastname',class_='medium')}
98 ${h.text('lastname',class_='medium')}
99 </div>
99 </div>
100 </div>
100 </div>
101
101
102 <div class="field">
102 <div class="field">
103 <div class="label">
103 <div class="label">
104 <label for="email">${_('Email')}:</label>
104 <label for="email">${_('Email')}:</label>
105 </div>
105 </div>
106 <div class="input">
106 <div class="input">
107 ${h.text('email',class_='medium')}
107 ${h.text('email',class_='medium')}
108 </div>
108 </div>
109 </div>
109 </div>
110
110
111 <div class="field">
111 <div class="field">
112 <div class="label label-checkbox">
112 <div class="label label-checkbox">
113 <label for="active">${_('Active')}:</label>
113 <label for="active">${_('Active')}:</label>
114 </div>
114 </div>
115 <div class="checkboxes">
115 <div class="checkboxes">
116 ${h.checkbox('active',value=True)}
116 ${h.checkbox('active',value=True)}
117 </div>
117 </div>
118 </div>
118 </div>
119
119
120 <div class="field">
120 <div class="field">
121 <div class="label label-checkbox">
121 <div class="label label-checkbox">
122 <label for="admin">${_('Admin')}:</label>
122 <label for="admin">${_('Admin')}:</label>
123 </div>
123 </div>
124 <div class="checkboxes">
124 <div class="checkboxes">
125 ${h.checkbox('admin',value=True)}
125 ${h.checkbox('admin',value=True)}
126 </div>
126 </div>
127 </div>
127 </div>
128 <div class="buttons">
128 <div class="buttons">
129 ${h.submit('save',_('Save'),class_="ui-button")}
129 ${h.submit('save',_('Save'),class_="ui-button")}
130 ${h.reset('reset',_('Reset'),class_="ui-button")}
130 ${h.reset('reset',_('Reset'),class_="ui-button")}
131 </div>
131 </div>
132 </div>
132 </div>
133 </div>
133 </div>
134 ${h.end_form()}
134 ${h.end_form()}
135 </div>
135 </div>
136 <div class="box box-right">
136 <div class="box box-right">
137 <!-- box / title -->
137 <!-- box / title -->
138 <div class="title">
138 <div class="title">
139 <h5>${_('Permissions')}</h5>
139 <h5>${_('Permissions')}</h5>
140 </div>
140 </div>
141 ${h.form(url('user_perm', id=c.user.user_id),method='put')}
141 ${h.form(url('user_perm', id=c.user.user_id),method='put')}
142 <div class="form">
142 <div class="form">
143 <!-- fields -->
143 <!-- fields -->
144 <div class="fields">
144 <div class="fields">
145 <div class="field">
145 <div class="field">
146 <div class="label label-checkbox">
146 <div class="label label-checkbox">
147 <label for="create_repo_perm">${_('Create repositories')}:</label>
147 <label for="create_repo_perm">${_('Create repositories')}:</label>
148 </div>
148 </div>
149 <div class="checkboxes">
149 <div class="checkboxes">
150 ${h.checkbox('create_repo_perm',value=True)}
150 ${h.checkbox('create_repo_perm',value=True)}
151 </div>
151 </div>
152 </div>
152 </div>
153 <div class="buttons">
153 <div class="buttons">
154 ${h.submit('save',_('Save'),class_="ui-button")}
154 ${h.submit('save',_('Save'),class_="ui-button")}
155 ${h.reset('reset',_('Reset'),class_="ui-button")}
155 ${h.reset('reset',_('Reset'),class_="ui-button")}
156 </div>
156 </div>
157 </div>
157 </div>
158 </div>
158 </div>
159 ${h.end_form()}
159 ${h.end_form()}
160
160
161 ## permissions overview
161 ## permissions overview
162 <div id="perms" class="table">
162 <div id="perms" class="table">
163 %for section in sorted(c.perm_user.permissions.keys()):
163 %for section in sorted(c.perm_user.permissions.keys()):
164 <div class="perms_section_head">${section.replace("_"," ").capitalize()}</div>
164 <div class="perms_section_head">${section.replace("_"," ").capitalize()}</div>
165
165
166 <div id='tbl_list_wrap_${section}' class="yui-skin-sam">
166 <div id='tbl_list_wrap_${section}' class="yui-skin-sam">
167 <table id="tbl_list_${section}">
167 <table id="tbl_list_${section}">
168 <thead>
168 <thead>
169 <tr>
169 <tr>
170 <th class="left">${_('Name')}</th>
170 <th class="left">${_('Name')}</th>
171 <th class="left">${_('Permission')}</th>
171 <th class="left">${_('Permission')}</th>
172 </thead>
172 </thead>
173 <tbody>
173 <tbody>
174 %for k in c.perm_user.permissions[section]:
174 %for k in c.perm_user.permissions[section]:
175 <%
175 <%
176 if section != 'global':
176 if section != 'global':
177 section_perm = c.perm_user.permissions[section].get(k)
177 section_perm = c.perm_user.permissions[section].get(k)
178 _perm = section_perm.split('.')[-1]
178 _perm = section_perm.split('.')[-1]
179 else:
179 else:
180 _perm = section_perm = None
180 _perm = section_perm = None
181 %>
181 %>
182 <tr>
182 <tr>
183 <td>
183 <td>
184 %if section == 'repositories':
184 %if section == 'repositories':
185 <a href="${h.url('summary_home',repo_name=k)}">${k}</a>
185 <a href="${h.url('summary_home',repo_name=k)}">${k}</a>
186 %elif section == 'repositories_groups':
186 %elif section == 'repositories_groups':
187 <a href="${h.url('repos_group_home',group_name=k)}">${k}</a>
187 <a href="${h.url('repos_group_home',group_name=k)}">${k}</a>
188 %else:
188 %else:
189 ${k}
189 ${k}
190 %endif
190 %endif
191 </td>
191 </td>
192 <td>
192 <td>
193 %if section == 'global':
193 %if section == 'global':
194 ${h.bool2icon(True)}
194 ${h.bool2icon(True)}
195 %else:
195 %else:
196 <span class="perm_tag ${_perm}">${section_perm}</span>
196 <span class="perm_tag ${_perm}">${section_perm}</span>
197 %endif
197 %endif
198 </td>
198 </td>
199 </tr>
199 </tr>
200 %endfor
200 %endfor
201 </tbody>
201 </tbody>
202 </table>
202 </table>
203 </div>
203 </div>
204 %endfor
204 %endfor
205 </div>
205 </div>
206 </div>
206 </div>
207 <div class="box box-right">
207 <div class="box box-right">
208 <!-- box / title -->
208 <!-- box / title -->
209 <div class="title">
209 <div class="title">
210 <h5>${_('Email addresses')}</h5>
210 <h5>${_('Email addresses')}</h5>
211 </div>
211 </div>
212
212
213 <div class="emails_wrap">
213 <div class="emails_wrap">
214 <table class="noborder">
214 <table class="noborder">
215 %for em in c.user_email_map:
215 %for em in c.user_email_map:
216 <tr>
216 <tr>
217 <td><div class="gravatar"><img alt="gravatar" src="${h.gravatar_url(em.user.email,16)}"/> </div></td>
217 <td><div class="gravatar"><img alt="gravatar" src="${h.gravatar_url(em.user.email,16)}"/> </div></td>
218 <td><div class="email">${em.email}</div></td>
218 <td><div class="email">${em.email}</div></td>
219 <td>
219 <td>
220 ${h.form(url('user_emails_delete', id=c.user.user_id),method='delete')}
220 ${h.form(url('user_emails_delete', id=c.user.user_id),method='delete')}
221 ${h.hidden('del_email',em.email_id)}
221 ${h.hidden('del_email',em.email_id)}
222 ${h.submit('remove_',_('delete'),id="remove_email_%s" % em.email_id,
222 ${h.submit('remove_',_('delete'),id="remove_email_%s" % em.email_id,
223 class_="delete_icon action_button", onclick="return confirm('"+_('Confirm to delete this email: %s') % em.email+"');")}
223 class_="delete_icon action_button", onclick="return confirm('"+_('Confirm to delete this email: %s') % em.email+"');")}
224 ${h.end_form()}
224 ${h.end_form()}
225 </td>
225 </td>
226 </tr>
226 </tr>
227 %endfor
227 %endfor
228 </table>
228 </table>
229 </div>
229 </div>
230
230
231 ${h.form(url('user_emails', id=c.user.user_id),method='put')}
231 ${h.form(url('user_emails', id=c.user.user_id),method='put')}
232 <div class="form">
232 <div class="form">
233 <!-- fields -->
233 <!-- fields -->
234 <div class="fields">
234 <div class="fields">
235 <div class="field">
235 <div class="field">
236 <div class="label">
236 <div class="label">
237 <label for="email">${_('New email address')}:</label>
237 <label for="email">${_('New email address')}:</label>
238 </div>
238 </div>
239 <div class="input">
239 <div class="input">
240 ${h.text('new_email', class_='medium')}
240 ${h.text('new_email', class_='medium')}
241 </div>
241 </div>
242 </div>
242 </div>
243 <div class="buttons">
243 <div class="buttons">
244 ${h.submit('save',_('Add'),class_="ui-button")}
244 ${h.submit('save',_('Add'),class_="ui-button")}
245 ${h.reset('reset',_('Reset'),class_="ui-button")}
245 ${h.reset('reset',_('Reset'),class_="ui-button")}
246 </div>
246 </div>
247 </div>
247 </div>
248 </div>
248 </div>
249 ${h.end_form()}
249 ${h.end_form()}
250 </div>
250 </div>
251 </%def>
251 </%def>
@@ -1,94 +1,94 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <%inherit file="/base/base.html"/>
2 <%inherit file="/base/base.html"/>
3
3
4 <%def name="title()">
4 <%def name="title()">
5 ${_('%s Branches') % c.repo_name} - ${c.rhodecode_name}
5 ${_('%s Branches') % c.repo_name} - ${c.rhodecode_name}
6 </%def>
6 </%def>
7
7
8 <%def name="breadcrumbs_links()">
8 <%def name="breadcrumbs_links()">
9 <input class="q_filter_box" id="q_filter_branches" size="15" type="text" name="filter" value="${_('quick filter...')}"/>
9 <input class="q_filter_box" id="q_filter_branches" size="15" type="text" name="filter" value="${_('quick filter...')}"/>
10 ${h.link_to(u'Home',h.url('/'))}
10 ${h.link_to(u'Home',h.url('/'))}
11 &raquo;
11 &raquo;
12 ${h.link_to(c.repo_name,h.url('summary_home',repo_name=c.repo_name))}
12 ${h.link_to(c.repo_name,h.url('summary_home',repo_name=c.repo_name))}
13 &raquo;
13 &raquo;
14 ${_('branches')}
14 ${_('branches')}
15 </%def>
15 </%def>
16
16
17 <%def name="page_nav()">
17 <%def name="page_nav()">
18 ${self.menu('branches')}
18 ${self.menu('branches')}
19 </%def>
19 </%def>
20
20
21 <%def name="main()">
21 <%def name="main()">
22 <div class="box">
22 <div class="box">
23 <!-- box / title -->
23 <!-- box / title -->
24 <div class="title">
24 <div class="title">
25 ${self.breadcrumbs()}
25 ${self.breadcrumbs()}
26 </div>
26 </div>
27 <!-- end box / title -->
27 <!-- end box / title -->
28 %if c.repo_branches:
28 %if c.repo_branches:
29 <div class="info_box" id="compare_branches" style="clear: both;padding: 10px 19px;vertical-align: right;text-align: right;"><a href="#" class="ui-btn small">${_('Compare branches')}</a></div>
29 <div class="info_box" id="compare_branches" style="clear: both;padding: 10px 19px;vertical-align: right;text-align: right;"><a href="#" class="ui-btn small">${_('Compare branches')}</a></div>
30 %endif
30 %endif
31 <div class="table">
31 <div class="table">
32 <%include file='branches_data.html'/>
32 <%include file='branches_data.html'/>
33 </div>
33 </div>
34 </div>
34 </div>
35 <script type="text/javascript">
35 <script type="text/javascript">
36 YUE.on('compare_branches','click',function(e){
36 YUE.on('compare_branches','click',function(e){
37 YUE.preventDefault(e);
37 YUE.preventDefault(e);
38 var org = YUQ('input[name=compare_org]:checked')[0];
38 var org = YUQ('input[name=compare_org]:checked')[0];
39 var other = YUQ('input[name=compare_other]:checked')[0];
39 var other = YUQ('input[name=compare_other]:checked')[0];
40
40
41 if(org && other){
41 if(org && other){
42 var compare_url = "${h.url('compare_url',repo_name=c.repo_name,org_ref_type='branch',org_ref='__ORG__',other_ref_type='branch',other_ref='__OTHER__')}";
42 var compare_url = "${h.url('compare_url',repo_name=c.repo_name,org_ref_type='branch',org_ref='__ORG__',other_ref_type='branch',other_ref='__OTHER__')}";
43 var u = compare_url.replace('__ORG__',org.value)
43 var u = compare_url.replace('__ORG__',org.value)
44 .replace('__OTHER__',other.value);
44 .replace('__OTHER__',other.value);
45 window.location=u;
45 window.location=u;
46 }
46 }
47
47
48 })
48 })
49 // main table sorting
49 // main table sorting
50 var myColumnDefs = [
50 var myColumnDefs = [
51 {key:"name",label:"${_('Name')}",sortable:true},
51 {key:"name",label:"${_('Name')}",sortable:true},
52 {key:"date",label:"${_('Date')}",sortable:true,
52 {key:"date",label:"${_('Date')}",sortable:true,
53 sortOptions: { sortFunction: dateSort }},
53 sortOptions: { sortFunction: dateSort }},
54 {key:"author",label:"${_('Author')}",sortable:true},
54 {key:"author",label:"${_('Author')}",sortable:true},
55 {key:"revision",label:"${_('Revision')}",sortable:true,
55 {key:"revision",label:"${_('Revision')}",sortable:true,
56 sortOptions: { sortFunction: revisionSort }},
56 sortOptions: { sortFunction: revisionSort }},
57 {key:"compare",label:"${_('Compare')}",sortable:false,},
57 {key:"compare",label:"${_('Compare')}",sortable:false,},
58 ];
58 ];
59
59
60 var myDataSource = new YAHOO.util.DataSource(YUD.get("branches_data"));
60 var myDataSource = new YAHOO.util.DataSource(YUD.get("branches_data"));
61
61
62 myDataSource.responseType = YAHOO.util.DataSource.TYPE_HTMLTABLE;
62 myDataSource.responseType = YAHOO.util.DataSource.TYPE_HTMLTABLE;
63
63
64 myDataSource.responseSchema = {
64 myDataSource.responseSchema = {
65 fields: [
65 fields: [
66 {key:"name"},
66 {key:"name"},
67 {key:"date"},
67 {key:"date"},
68 {key:"author"},
68 {key:"author"},
69 {key:"revision"},
69 {key:"revision"},
70 {key:"compare"},
70 {key:"compare"},
71 ]
71 ]
72 };
72 };
73
73
74 var myDataTable = new YAHOO.widget.DataTable("table_wrap", myColumnDefs, myDataSource,
74 var myDataTable = new YAHOO.widget.DataTable("table_wrap", myColumnDefs, myDataSource,
75 {
75 {
76 sortedBy:{key:"name",dir:"asc"},
76 sortedBy:{key:"name",dir:"asc"},
77 MSG_SORTASC:"${_('Click to sort ascending')}",
77 MSG_SORTASC:"${_('Click to sort ascending')}",
78 MSG_SORTDESC:"${_('Click to sort descending')}",
78 MSG_SORTDESC:"${_('Click to sort descending')}",
79 MSG_EMPTY:"${_('No records found.')}",
79 MSG_EMPTY:"${_('No records found.')}",
80 MSG_ERROR:"${_('Data error.')}",
80 MSG_ERROR:"${_('Data error.')}",
81 MSG_LOADING:"${_('Loading...')}",
81 MSG_LOADING:"${_('Loading...')}",
82 }
82 }
83 );
83 );
84 myDataTable.subscribe('postRenderEvent',function(oArgs) {
84 myDataTable.subscribe('postRenderEvent',function(oArgs) {
85 tooltip_activate();
85 tooltip_activate();
86 var func = function(node){
86 var func = function(node){
87 return node.parentNode.parentNode.parentNode.parentNode.parentNode;
87 return node.parentNode.parentNode.parentNode.parentNode.parentNode;
88 }
88 }
89 q_filter('q_filter_branches',YUQ('div.table tr td .logtags .branchtag a'),func);
89 q_filter('q_filter_branches',YUQ('div.table tr td .logtags .branchtag a'),func);
90 });
90 });
91
91
92 </script>
92 </script>
93
93
94 </%def>
94 </%def>
@@ -1,155 +1,155 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 ## usage:
2 ## usage:
3 ## <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
3 ## <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
4 ## ${comment.comment_block(co)}
4 ## ${comment.comment_block(co)}
5 ##
5 ##
6 <%def name="comment_block(co)">
6 <%def name="comment_block(co)">
7 <div class="comment" id="comment-${co.comment_id}" line="${co.line_no}">
7 <div class="comment" id="comment-${co.comment_id}" line="${co.line_no}">
8 <div class="comment-wrapp">
8 <div class="comment-wrapp">
9 <div class="meta">
9 <div class="meta">
10 <div style="float:left"> <img src="${h.gravatar_url(co.author.email, 20)}" /> </div>
10 <div style="float:left"> <img src="${h.gravatar_url(co.author.email, 20)}" /> </div>
11 <div class="user">
11 <div class="user">
12 ${co.author.username}
12 ${co.author.username}
13 </div>
13 </div>
14 <div class="date">
14 <div class="date">
15 ${h.age(co.modified_at)}
15 ${h.age(co.modified_at)}
16 </div>
16 </div>
17 %if co.status_change:
17 %if co.status_change:
18 <div style="float:left" class="changeset-status-container">
18 <div style="float:left" class="changeset-status-container">
19 <div style="float:left;padding:0px 2px 0px 2px"><span style="font-size: 18px;">&rsaquo;</span></div>
19 <div style="float:left;padding:0px 2px 0px 2px"><span style="font-size: 18px;">&rsaquo;</span></div>
20 <div title="${_('Changeset status')}" class="changeset-status-lbl"> ${co.status_change.status_lbl}</div>
20 <div title="${_('Changeset status')}" class="changeset-status-lbl"> ${co.status_change.status_lbl}</div>
21 <div class="changeset-status-ico"><img src="${h.url(str('/images/icons/flag_status_%s.png' % co.status_change.status))}" /></div>
21 <div class="changeset-status-ico"><img src="${h.url(str('/images/icons/flag_status_%s.png' % co.status_change.status))}" /></div>
22 </div>
22 </div>
23 %endif
23 %endif
24 %if h.HasPermissionAny('hg.admin', 'repository.admin')() or co.author.user_id == c.rhodecode_user.user_id:
24 %if h.HasPermissionAny('hg.admin', 'repository.admin')() or co.author.user_id == c.rhodecode_user.user_id:
25 <div class="buttons">
25 <div class="buttons">
26 <span onClick="deleteComment(${co.comment_id})" class="delete-comment ui-btn">${_('Delete')}</span>
26 <span onClick="deleteComment(${co.comment_id})" class="delete-comment ui-btn">${_('Delete')}</span>
27 </div>
27 </div>
28 %endif
28 %endif
29 </div>
29 </div>
30 <div class="text">
30 <div class="text">
31 ${h.rst_w_mentions(co.text)|n}
31 ${h.rst_w_mentions(co.text)|n}
32 </div>
32 </div>
33 </div>
33 </div>
34 </div>
34 </div>
35 </%def>
35 </%def>
36
36
37
37
38 <%def name="comment_inline_form(changeset)">
38 <%def name="comment_inline_form(changeset)">
39 <div id='comment-inline-form-template' style="display:none">
39 <div id='comment-inline-form-template' style="display:none">
40 <div class="comment-inline-form ac">
40 <div class="comment-inline-form ac">
41 %if c.rhodecode_user.username != 'default':
41 %if c.rhodecode_user.username != 'default':
42 <div class="overlay"><div class="overlay-text">${_('Submitting...')}</div></div>
42 <div class="overlay"><div class="overlay-text">${_('Submitting...')}</div></div>
43 ${h.form(h.url('changeset_comment', repo_name=c.repo_name, revision=changeset.raw_id),class_='inline-form')}
43 ${h.form(h.url('changeset_comment', repo_name=c.repo_name, revision=changeset.raw_id),class_='inline-form')}
44 <div class="clearfix">
44 <div class="clearfix">
45 <div class="comment-help">${_('Commenting on line {1}.')}
45 <div class="comment-help">${_('Commenting on line {1}.')}
46 ${(_('Comments parsed using %s syntax with %s support.') % (
46 ${(_('Comments parsed using %s syntax with %s support.') % (
47 ('<a href="%s">RST</a>' % h.url('rst_help')),
47 ('<a href="%s">RST</a>' % h.url('rst_help')),
48 ('<span style="color:#003367" class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user'))
48 ('<span style="color:#003367" class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user'))
49 )
49 )
50 )|n
50 )|n
51 }
51 }
52 </div>
52 </div>
53 <div class="mentions-container" id="mentions_container_{1}"></div>
53 <div class="mentions-container" id="mentions_container_{1}"></div>
54 <textarea id="text_{1}" name="text" class="yui-ac-input"></textarea>
54 <textarea id="text_{1}" name="text" class="yui-ac-input"></textarea>
55 </div>
55 </div>
56 <div class="comment-button">
56 <div class="comment-button">
57 <input type="hidden" name="f_path" value="{0}">
57 <input type="hidden" name="f_path" value="{0}">
58 <input type="hidden" name="line" value="{1}">
58 <input type="hidden" name="line" value="{1}">
59 ${h.submit('save', _('Comment'), class_='ui-btn save-inline-form')}
59 ${h.submit('save', _('Comment'), class_='ui-btn save-inline-form')}
60 ${h.reset('hide-inline-form', _('Hide'), class_='ui-btn hide-inline-form')}
60 ${h.reset('hide-inline-form', _('Hide'), class_='ui-btn hide-inline-form')}
61 </div>
61 </div>
62 ${h.end_form()}
62 ${h.end_form()}
63 %else:
63 %else:
64 ${h.form('')}
64 ${h.form('')}
65 <div class="clearfix">
65 <div class="clearfix">
66 <div class="comment-help">
66 <div class="comment-help">
67 ${_('You need to be logged in to comment.')} <a href="${h.url('login_home',came_from=h.url.current())}">${_('Login now')}</a>
67 ${_('You need to be logged in to comment.')} <a href="${h.url('login_home',came_from=h.url.current())}">${_('Login now')}</a>
68 </div>
68 </div>
69 </div>
69 </div>
70 <div class="comment-button">
70 <div class="comment-button">
71 ${h.reset('hide-inline-form', _('Hide'), class_='ui-btn hide-inline-form')}
71 ${h.reset('hide-inline-form', _('Hide'), class_='ui-btn hide-inline-form')}
72 </div>
72 </div>
73 ${h.end_form()}
73 ${h.end_form()}
74 %endif
74 %endif
75 </div>
75 </div>
76 </div>
76 </div>
77 </%def>
77 </%def>
78
78
79
79
80 ## generates inlines taken from c.comments var
80 ## generates inlines taken from c.comments var
81 <%def name="inlines()">
81 <%def name="inlines()">
82 <div class="comments-number">${ungettext("%d comment", "%d comments", len(c.comments)) % len(c.comments)} ${ungettext("(%d inline)", "(%d inline)", c.inline_cnt) % c.inline_cnt}</div>
82 <div class="comments-number">${ungettext("%d comment", "%d comments", len(c.comments)) % len(c.comments)} ${ungettext("(%d inline)", "(%d inline)", c.inline_cnt) % c.inline_cnt}</div>
83 %for path, lines in c.inline_comments:
83 %for path, lines in c.inline_comments:
84 % for line,comments in lines.iteritems():
84 % for line,comments in lines.iteritems():
85 <div style="display:none" class="inline-comment-placeholder" path="${path}" target_id="${h.safeid(h.safe_unicode(path))}">
85 <div style="display:none" class="inline-comment-placeholder" path="${path}" target_id="${h.safeid(h.safe_unicode(path))}">
86 %for co in comments:
86 %for co in comments:
87 ${comment_block(co)}
87 ${comment_block(co)}
88 %endfor
88 %endfor
89 </div>
89 </div>
90 %endfor
90 %endfor
91 %endfor
91 %endfor
92
92
93 </%def>
93 </%def>
94
94
95 ## MAIN COMMENT FORM
95 ## MAIN COMMENT FORM
96 <%def name="comments(post_url, cur_status)">
96 <%def name="comments(post_url, cur_status)">
97
97
98 <div class="comments">
98 <div class="comments">
99 <div id="inline-comments-container">
99 <div id="inline-comments-container">
100 ## generate inlines for this changeset
100 ## generate inlines for this changeset
101 ${inlines()}
101 ${inlines()}
102 </div>
102 </div>
103
103
104 %for co in c.comments:
104 %for co in c.comments:
105 <div id="comment-tr-${co.comment_id}">
105 <div id="comment-tr-${co.comment_id}">
106 ${comment_block(co)}
106 ${comment_block(co)}
107 </div>
107 </div>
108 %endfor
108 %endfor
109 %if c.rhodecode_user.username != 'default':
109 %if c.rhodecode_user.username != 'default':
110 <div class="comment-form ac">
110 <div class="comment-form ac">
111 ${h.form(post_url)}
111 ${h.form(post_url)}
112 <strong>${_('Leave a comment')}</strong>
112 <strong>${_('Leave a comment')}</strong>
113 <div class="clearfix">
113 <div class="clearfix">
114 <div class="comment-help">
114 <div class="comment-help">
115 ${(_('Comments parsed using %s syntax with %s support.') % (('<a href="%s">RST</a>' % h.url('rst_help')),
115 ${(_('Comments parsed using %s syntax with %s support.') % (('<a href="%s">RST</a>' % h.url('rst_help')),
116 '<span style="color:#003367" class="tooltip" title="%s">@mention</span>' %
116 '<span style="color:#003367" class="tooltip" title="%s">@mention</span>' %
117 _('Use @username inside this text to send notification to this RhodeCode user')))|n}
117 _('Use @username inside this text to send notification to this RhodeCode user')))|n}
118 | <span class="tooltip" title="${_('Check this to change current status of code-review for this changeset')}"> ${_('change status')}
118 | <span class="tooltip" title="${_('Check this to change current status of code-review for this changeset')}"> ${_('change status')}
119 <input style="vertical-align: bottom;margin-bottom:-2px" id="show_changeset_status_box" type="checkbox" name="change_changeset_status" />
119 <input style="vertical-align: bottom;margin-bottom:-2px" id="show_changeset_status_box" type="checkbox" name="change_changeset_status" />
120 </span>
120 </span>
121 </div>
121 </div>
122 <div id="status_block_container" class="status-block" style="display:none">
122 <div id="status_block_container" class="status-block" style="display:none">
123 %for status,lbl in c.changeset_statuses:
123 %for status,lbl in c.changeset_statuses:
124 <div class="">
124 <div class="">
125 <img src="${h.url('/images/icons/flag_status_%s.png' % status)}" /> <input ${'checked="checked"' if status == cur_status else ''}" type="radio" name="changeset_status" value="${status}"> <label>${lbl}</label>
125 <img src="${h.url('/images/icons/flag_status_%s.png' % status)}" /> <input ${'checked="checked"' if status == cur_status else ''}" type="radio" name="changeset_status" value="${status}"> <label>${lbl}</label>
126 </div>
126 </div>
127 %endfor
127 %endfor
128 </div>
128 </div>
129 <div class="mentions-container" id="mentions_container"></div>
129 <div class="mentions-container" id="mentions_container"></div>
130 ${h.textarea('text')}
130 ${h.textarea('text')}
131 </div>
131 </div>
132 <div class="comment-button">
132 <div class="comment-button">
133 ${h.submit('save', _('Comment'), class_='ui-button')}
133 ${h.submit('save', _('Comment'), class_='ui-button')}
134 </div>
134 </div>
135 ${h.end_form()}
135 ${h.end_form()}
136 </div>
136 </div>
137 %endif
137 %endif
138 </div>
138 </div>
139 <script>
139 <script>
140 YUE.onDOMReady(function () {
140 YUE.onDOMReady(function () {
141 MentionsAutoComplete('text', 'mentions_container', _USERS_AC_DATA, _GROUPS_AC_DATA);
141 MentionsAutoComplete('text', 'mentions_container', _USERS_AC_DATA, _GROUPS_AC_DATA);
142
142
143 // changeset status box listener
143 // changeset status box listener
144 YUE.on(YUD.get('show_changeset_status_box'),'change',function(e){
144 YUE.on(YUD.get('show_changeset_status_box'),'change',function(e){
145 if(e.currentTarget.checked){
145 if(e.currentTarget.checked){
146 YUD.setStyle('status_block_container','display','');
146 YUD.setStyle('status_block_container','display','');
147 }
147 }
148 else{
148 else{
149 YUD.setStyle('status_block_container','display','none');
149 YUD.setStyle('status_block_container','display','none');
150 }
150 }
151 })
151 })
152
152
153 });
153 });
154 </script>
154 </script>
155 </%def>
155 </%def>
@@ -1,61 +1,61 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 ##usage:
2 ##usage:
3 ## <%namespace name="diff_block" file="/changeset/diff_block.html"/>
3 ## <%namespace name="diff_block" file="/changeset/diff_block.html"/>
4 ## ${diff_block.diff_block(change)}
4 ## ${diff_block.diff_block(change)}
5 ##
5 ##
6 <%def name="diff_block(change)">
6 <%def name="diff_block(change)">
7
7
8 %for op,filenode,diff,cs1,cs2,stat in change:
8 %for op,filenode,diff,cs1,cs2,stat in change:
9 %if op !='removed':
9 %if op !='removed':
10 <div id="${h.FID(filenode.changeset.raw_id,filenode.path)}_target" style="clear:both;margin-top:25px"></div>
10 <div id="${h.FID(filenode.changeset.raw_id,filenode.path)}_target" style="clear:both;margin-top:25px"></div>
11 <div id="${h.FID(filenode.changeset.raw_id,filenode.path)}" class="diffblock margined comm">
11 <div id="${h.FID(filenode.changeset.raw_id,filenode.path)}" class="diffblock margined comm">
12 <div class="code-header">
12 <div class="code-header">
13 <div class="changeset_header">
13 <div class="changeset_header">
14 <div class="changeset_file">
14 <div class="changeset_file">
15 ${h.link_to_if(change!='removed',h.safe_unicode(filenode.path),h.url('files_home',repo_name=c.repo_name,
15 ${h.link_to_if(change!='removed',h.safe_unicode(filenode.path),h.url('files_home',repo_name=c.repo_name,
16 revision=filenode.changeset.raw_id,f_path=h.safe_unicode(filenode.path)))}
16 revision=filenode.changeset.raw_id,f_path=h.safe_unicode(filenode.path)))}
17 </div>
17 </div>
18 <div class="diff-actions">
18 <div class="diff-actions">
19 <a href="${h.url('files_diff_home',repo_name=c.repo_name,f_path=h.safe_unicode(filenode.path),diff2=cs2,diff1=cs1,diff='diff',fulldiff=1)}" class="tooltip" title="${h.tooltip(_('diff'))}"><img class="icon" src="${h.url('/images/icons/page_white_go.png')}"/></a>
19 <a href="${h.url('files_diff_home',repo_name=c.repo_name,f_path=h.safe_unicode(filenode.path),diff2=cs2,diff1=cs1,diff='diff',fulldiff=1)}" class="tooltip" title="${h.tooltip(_('diff'))}"><img class="icon" src="${h.url('/images/icons/page_white_go.png')}"/></a>
20 <a href="${h.url('files_diff_home',repo_name=c.repo_name,f_path=h.safe_unicode(filenode.path),diff2=cs2,diff1=cs1,diff='raw')}" class="tooltip" title="${h.tooltip(_('raw diff'))}"><img class="icon" src="${h.url('/images/icons/page_white.png')}"/></a>
20 <a href="${h.url('files_diff_home',repo_name=c.repo_name,f_path=h.safe_unicode(filenode.path),diff2=cs2,diff1=cs1,diff='raw')}" class="tooltip" title="${h.tooltip(_('raw diff'))}"><img class="icon" src="${h.url('/images/icons/page_white.png')}"/></a>
21 <a href="${h.url('files_diff_home',repo_name=c.repo_name,f_path=h.safe_unicode(filenode.path),diff2=cs2,diff1=cs1,diff='download')}" class="tooltip" title="${h.tooltip(_('download diff'))}"><img class="icon" src="${h.url('/images/icons/page_white_get.png')}"/></a>
21 <a href="${h.url('files_diff_home',repo_name=c.repo_name,f_path=h.safe_unicode(filenode.path),diff2=cs2,diff1=cs1,diff='download')}" class="tooltip" title="${h.tooltip(_('download diff'))}"><img class="icon" src="${h.url('/images/icons/page_white_get.png')}"/></a>
22 ${c.ignorews_url(request.GET, h.FID(filenode.changeset.raw_id,filenode.path))}
22 ${c.ignorews_url(request.GET, h.FID(filenode.changeset.raw_id,filenode.path))}
23 ${c.context_url(request.GET, h.FID(filenode.changeset.raw_id,filenode.path))}
23 ${c.context_url(request.GET, h.FID(filenode.changeset.raw_id,filenode.path))}
24 </div>
24 </div>
25 <span style="float:right;margin-top:-3px">
25 <span style="float:right;margin-top:-3px">
26 <label>
26 <label>
27 ${_('show inline comments')}
27 ${_('show inline comments')}
28 ${h.checkbox('',checked="checked",class_="show-inline-comments",id_for=h.FID(filenode.changeset.raw_id,filenode.path))}
28 ${h.checkbox('',checked="checked",class_="show-inline-comments",id_for=h.FID(filenode.changeset.raw_id,filenode.path))}
29 </label>
29 </label>
30 </span>
30 </span>
31 </div>
31 </div>
32 </div>
32 </div>
33 <div class="code-body">
33 <div class="code-body">
34 <div class="full_f_path" path="${h.safe_unicode(filenode.path)}"></div>
34 <div class="full_f_path" path="${h.safe_unicode(filenode.path)}"></div>
35 ${diff|n}
35 ${diff|n}
36 </div>
36 </div>
37 </div>
37 </div>
38 %endif
38 %endif
39 %endfor
39 %endfor
40
40
41 </%def>
41 </%def>
42
42
43 <%def name="diff_block_simple(change)">
43 <%def name="diff_block_simple(change)">
44
44
45 %for op,filenode_path,diff in change:
45 %for op,filenode_path,diff in change:
46 <div id="${h.FID('',filenode_path)}_target" style="clear:both;margin-top:25px"></div>
46 <div id="${h.FID('',filenode_path)}_target" style="clear:both;margin-top:25px"></div>
47 <div id="${h.FID('',filenode_path)}" class="diffblock margined comm">
47 <div id="${h.FID('',filenode_path)}" class="diffblock margined comm">
48 <div class="code-header">
48 <div class="code-header">
49 <div class="changeset_header">
49 <div class="changeset_header">
50 <div class="changeset_file">
50 <div class="changeset_file">
51 <a href="#">${h.safe_unicode(filenode_path)}</a>
51 <a href="#">${h.safe_unicode(filenode_path)}</a>
52 </div>
52 </div>
53 </div>
53 </div>
54 </div>
54 </div>
55 <div class="code-body">
55 <div class="code-body">
56 <div class="full_f_path" path="${h.safe_unicode(filenode_path)}"></div>
56 <div class="full_f_path" path="${h.safe_unicode(filenode_path)}"></div>
57 ${diff|n}
57 ${diff|n}
58 </div>
58 </div>
59 </div>
59 </div>
60 %endfor
60 %endfor
61 </%def> No newline at end of file
61 </%def>
@@ -1,12 +1,12 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <%inherit file="main.html"/>
2 <%inherit file="main.html"/>
3
3
4 <h4>${subject}</h4>
4 <h4>${subject}</h4>
5
5
6 ${body}
6 ${body}
7
7
8 % if status_change is not None:
8 % if status_change is not None:
9 <div>
9 <div>
10 New status -> ${status_change}
10 New status -> ${status_change}
11 </div>
11 </div>
12 % endif No newline at end of file
12 % endif
@@ -1,83 +1,83 b''
1 <%inherit file="/base/base.html"/>
1 <%inherit file="/base/base.html"/>
2
2
3 <%def name="title()">
3 <%def name="title()">
4 ${c.repo_name} ${_('Pull request #%s') % c.pull_request.pull_request_id}
4 ${c.repo_name} ${_('Pull request #%s') % c.pull_request.pull_request_id}
5 </%def>
5 </%def>
6
6
7 <%def name="breadcrumbs_links()">
7 <%def name="breadcrumbs_links()">
8 ${h.link_to(u'Home',h.url('/'))}
8 ${h.link_to(u'Home',h.url('/'))}
9 &raquo;
9 &raquo;
10 ${h.link_to(c.repo_name,h.url('changelog_home',repo_name=c.repo_name))}
10 ${h.link_to(c.repo_name,h.url('changelog_home',repo_name=c.repo_name))}
11 &raquo;
11 &raquo;
12 ${_('Pull request #%s') % c.pull_request.pull_request_id}
12 ${_('Pull request #%s') % c.pull_request.pull_request_id}
13 </%def>
13 </%def>
14
14
15 <%def name="main()">
15 <%def name="main()">
16
16
17 <div class="box">
17 <div class="box">
18 <!-- box / title -->
18 <!-- box / title -->
19 <div class="title">
19 <div class="title">
20 ${self.breadcrumbs()}
20 ${self.breadcrumbs()}
21 </div>
21 </div>
22
22
23 <h3>${_('Title')}: ${c.pull_request.title}</h3>
23 <h3>${_('Title')}: ${c.pull_request.title}</h3>
24 <div class="changeset-status-container" style="float:left;padding:0px 20px 20px 20px">
24 <div class="changeset-status-container" style="float:left;padding:0px 20px 20px 20px">
25 %if c.current_changeset_status:
25 %if c.current_changeset_status:
26 <div title="${_('Changeset status')}" class="changeset-status-lbl">[${h.changeset_status_lbl(c.current_changeset_status)}]</div>
26 <div title="${_('Changeset status')}" class="changeset-status-lbl">[${h.changeset_status_lbl(c.current_changeset_status)}]</div>
27 <div class="changeset-status-ico"><img src="${h.url('/images/icons/flag_status_%s.png' % c.current_changeset_status)}" /></div>
27 <div class="changeset-status-ico"><img src="${h.url('/images/icons/flag_status_%s.png' % c.current_changeset_status)}" /></div>
28 %endif
28 %endif
29 </div>
29 </div>
30 <div style="padding:4px">
30 <div style="padding:4px">
31 <div>${h.fmt_date(c.pull_request.created_on)}</div>
31 <div>${h.fmt_date(c.pull_request.created_on)}</div>
32 </div>
32 </div>
33
33
34 ##DIFF
34 ##DIFF
35
35
36 <div class="table">
36 <div class="table">
37 <div id="body" class="diffblock">
37 <div id="body" class="diffblock">
38 <div style="white-space:pre-wrap;padding:5px">${h.literal(c.pull_request.description)}</div>
38 <div style="white-space:pre-wrap;padding:5px">${h.literal(c.pull_request.description)}</div>
39 </div>
39 </div>
40 <div id="changeset_compare_view_content">
40 <div id="changeset_compare_view_content">
41 ##CS
41 ##CS
42 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">${_('Incoming changesets')}</div>
42 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">${_('Incoming changesets')}</div>
43 <%include file="/compare/compare_cs.html" />
43 <%include file="/compare/compare_cs.html" />
44
44
45 ## FILES
45 ## FILES
46 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">${_('Files affected')}</div>
46 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">${_('Files affected')}</div>
47 <div class="cs_files">
47 <div class="cs_files">
48 %for fid, change, f, stat in c.files:
48 %for fid, change, f, stat in c.files:
49 <div class="cs_${change}">
49 <div class="cs_${change}">
50 <div class="node">${h.link_to(h.safe_unicode(f),h.url.current(anchor=fid))}</div>
50 <div class="node">${h.link_to(h.safe_unicode(f),h.url.current(anchor=fid))}</div>
51 <div class="changes">${h.fancy_file_stats(stat)}</div>
51 <div class="changes">${h.fancy_file_stats(stat)}</div>
52 </div>
52 </div>
53 %endfor
53 %endfor
54 </div>
54 </div>
55 </div>
55 </div>
56 </div>
56 </div>
57 <script>
57 <script>
58 var _USERS_AC_DATA = ${c.users_array|n};
58 var _USERS_AC_DATA = ${c.users_array|n};
59 var _GROUPS_AC_DATA = ${c.users_groups_array|n};
59 var _GROUPS_AC_DATA = ${c.users_groups_array|n};
60 </script>
60 </script>
61
61
62 ## diff block
62 ## diff block
63 <%namespace name="diff_block" file="/changeset/diff_block.html"/>
63 <%namespace name="diff_block" file="/changeset/diff_block.html"/>
64 %for fid, change, f, stat in c.files:
64 %for fid, change, f, stat in c.files:
65 ${diff_block.diff_block_simple([c.changes[fid]])}
65 ${diff_block.diff_block_simple([c.changes[fid]])}
66 %endfor
66 %endfor
67
67
68 ## template for inline comment form
68 ## template for inline comment form
69 <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
69 <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
70 ##${comment.comment_inline_form(c.changeset)}
70 ##${comment.comment_inline_form(c.changeset)}
71
71
72 ## render comments main comments form and it status
72 ## render comments main comments form and it status
73 ${comment.comments(h.url('pullrequest_comment', repo_name=c.repo_name, pull_request_id=c.pull_request.pull_request_id),
73 ${comment.comments(h.url('pullrequest_comment', repo_name=c.repo_name, pull_request_id=c.pull_request.pull_request_id),
74 c.current_changeset_status)}
74 c.current_changeset_status)}
75
75
76 </div>
76 </div>
77
77
78 <script type="text/javascript">
78 <script type="text/javascript">
79
79
80
80
81 </script>
81 </script>
82
82
83 </%def>
83 </%def>
@@ -1,31 +1,31 b''
1 <%inherit file="/base/base.html"/>
1 <%inherit file="/base/base.html"/>
2
2
3 <%def name="title()">
3 <%def name="title()">
4 ${c.repo_name} ${_('All pull requests')}
4 ${c.repo_name} ${_('All pull requests')}
5 </%def>
5 </%def>
6
6
7 <%def name="breadcrumbs_links()">
7 <%def name="breadcrumbs_links()">
8 ${h.link_to(u'Home',h.url('/'))}
8 ${h.link_to(u'Home',h.url('/'))}
9 &raquo;
9 &raquo;
10 ${h.link_to(c.repo_name,h.url('changelog_home',repo_name=c.repo_name))}
10 ${h.link_to(c.repo_name,h.url('changelog_home',repo_name=c.repo_name))}
11 &raquo;
11 &raquo;
12 ${_('All pull requests')}
12 ${_('All pull requests')}
13 </%def>
13 </%def>
14
14
15 <%def name="main()">
15 <%def name="main()">
16
16
17 <div class="box">
17 <div class="box">
18 <!-- box / title -->
18 <!-- box / title -->
19 <div class="title">
19 <div class="title">
20 ${self.breadcrumbs()}
20 ${self.breadcrumbs()}
21 </div>
21 </div>
22
22
23 %for pr in c.pull_requests:
23 %for pr in c.pull_requests:
24 <a href="${h.url('pullrequest_show',repo_name=c.repo_name,pull_request_id=pr.pull_request_id)}">#${pr.pull_request_id}</a>
24 <a href="${h.url('pullrequest_show',repo_name=c.repo_name,pull_request_id=pr.pull_request_id)}">#${pr.pull_request_id}</a>
25 %endfor
25 %endfor
26
26
27 </div>
27 </div>
28
28
29 <script type="text/javascript"></script>
29 <script type="text/javascript"></script>
30
30
31 </%def>
31 </%def>
@@ -1,160 +1,160 b''
1 """Pylons application test package
1 """Pylons application test package
2
2
3 This package assumes the Pylons environment is already loaded, such as
3 This package assumes the Pylons environment is already loaded, such as
4 when this script is imported from the `nosetests --with-pylons=test.ini`
4 when this script is imported from the `nosetests --with-pylons=test.ini`
5 command.
5 command.
6
6
7 This module initializes the application via ``websetup`` (`paster
7 This module initializes the application via ``websetup`` (`paster
8 setup-app`) and provides the base testing objects.
8 setup-app`) and provides the base testing objects.
9 """
9 """
10 import os
10 import os
11 import time
11 import time
12 import logging
12 import logging
13 import datetime
13 import datetime
14 import hashlib
14 import hashlib
15 import tempfile
15 import tempfile
16 from os.path import join as jn
16 from os.path import join as jn
17
17
18 from unittest import TestCase
18 from unittest import TestCase
19 from tempfile import _RandomNameSequence
19 from tempfile import _RandomNameSequence
20
20
21 from paste.deploy import loadapp
21 from paste.deploy import loadapp
22 from paste.script.appinstall import SetupCommand
22 from paste.script.appinstall import SetupCommand
23 from pylons import config, url
23 from pylons import config, url
24 from routes.util import URLGenerator
24 from routes.util import URLGenerator
25 from webtest import TestApp
25 from webtest import TestApp
26
26
27 from rhodecode import is_windows
27 from rhodecode import is_windows
28 from rhodecode.model.meta import Session
28 from rhodecode.model.meta import Session
29 from rhodecode.model.db import User
29 from rhodecode.model.db import User
30 from rhodecode.tests.nose_parametrized import parameterized
30 from rhodecode.tests.nose_parametrized import parameterized
31
31
32 import pylons.test
32 import pylons.test
33
33
34
34
35 os.environ['TZ'] = 'UTC'
35 os.environ['TZ'] = 'UTC'
36 if not is_windows:
36 if not is_windows:
37 time.tzset()
37 time.tzset()
38
38
39 log = logging.getLogger(__name__)
39 log = logging.getLogger(__name__)
40
40
41 __all__ = [
41 __all__ = [
42 'parameterized', 'environ', 'url', 'get_new_dir', 'TestController',
42 'parameterized', 'environ', 'url', 'get_new_dir', 'TestController',
43 'TESTS_TMP_PATH', 'HG_REPO', 'GIT_REPO', 'NEW_HG_REPO', 'NEW_GIT_REPO',
43 'TESTS_TMP_PATH', 'HG_REPO', 'GIT_REPO', 'NEW_HG_REPO', 'NEW_GIT_REPO',
44 'HG_FORK', 'GIT_FORK', 'TEST_USER_ADMIN_LOGIN', 'TEST_USER_REGULAR_LOGIN',
44 'HG_FORK', 'GIT_FORK', 'TEST_USER_ADMIN_LOGIN', 'TEST_USER_REGULAR_LOGIN',
45 'TEST_USER_REGULAR_PASS', 'TEST_USER_REGULAR_EMAIL',
45 'TEST_USER_REGULAR_PASS', 'TEST_USER_REGULAR_EMAIL',
46 'TEST_USER_REGULAR2_LOGIN', 'TEST_USER_REGULAR2_PASS',
46 'TEST_USER_REGULAR2_LOGIN', 'TEST_USER_REGULAR2_PASS',
47 'TEST_USER_REGULAR2_EMAIL', 'TEST_HG_REPO', 'TEST_HG_REPO_CLONE',
47 'TEST_USER_REGULAR2_EMAIL', 'TEST_HG_REPO', 'TEST_HG_REPO_CLONE',
48 'TEST_HG_REPO_PULL', 'TEST_GIT_REPO', 'TEST_GIT_REPO_CLONE',
48 'TEST_HG_REPO_PULL', 'TEST_GIT_REPO', 'TEST_GIT_REPO_CLONE',
49 'TEST_GIT_REPO_PULL', 'HG_REMOTE_REPO', 'GIT_REMOTE_REPO', 'SCM_TESTS',
49 'TEST_GIT_REPO_PULL', 'HG_REMOTE_REPO', 'GIT_REMOTE_REPO', 'SCM_TESTS',
50 ]
50 ]
51
51
52 # Invoke websetup with the current config file
52 # Invoke websetup with the current config file
53 # SetupCommand('setup-app').run([config_file])
53 # SetupCommand('setup-app').run([config_file])
54
54
55 ##RUNNING DESIRED TESTS
55 ##RUNNING DESIRED TESTS
56 # nosetests -x rhodecode.tests.functional.test_admin_settings:TestSettingsController.test_my_account
56 # nosetests -x rhodecode.tests.functional.test_admin_settings:TestSettingsController.test_my_account
57 # nosetests --pdb --pdb-failures
57 # nosetests --pdb --pdb-failures
58 # nosetests --with-coverage --cover-package=rhodecode.model.validators rhodecode.tests.test_validators
58 # nosetests --with-coverage --cover-package=rhodecode.model.validators rhodecode.tests.test_validators
59 environ = {}
59 environ = {}
60
60
61 #SOME GLOBALS FOR TESTS
61 #SOME GLOBALS FOR TESTS
62
62
63 TESTS_TMP_PATH = jn('/', 'tmp', 'rc_test_%s' % _RandomNameSequence().next())
63 TESTS_TMP_PATH = jn('/', 'tmp', 'rc_test_%s' % _RandomNameSequence().next())
64 TEST_USER_ADMIN_LOGIN = 'test_admin'
64 TEST_USER_ADMIN_LOGIN = 'test_admin'
65 TEST_USER_ADMIN_PASS = 'test12'
65 TEST_USER_ADMIN_PASS = 'test12'
66 TEST_USER_ADMIN_EMAIL = 'test_admin@mail.com'
66 TEST_USER_ADMIN_EMAIL = 'test_admin@mail.com'
67
67
68 TEST_USER_REGULAR_LOGIN = 'test_regular'
68 TEST_USER_REGULAR_LOGIN = 'test_regular'
69 TEST_USER_REGULAR_PASS = 'test12'
69 TEST_USER_REGULAR_PASS = 'test12'
70 TEST_USER_REGULAR_EMAIL = 'test_regular@mail.com'
70 TEST_USER_REGULAR_EMAIL = 'test_regular@mail.com'
71
71
72 TEST_USER_REGULAR2_LOGIN = 'test_regular2'
72 TEST_USER_REGULAR2_LOGIN = 'test_regular2'
73 TEST_USER_REGULAR2_PASS = 'test12'
73 TEST_USER_REGULAR2_PASS = 'test12'
74 TEST_USER_REGULAR2_EMAIL = 'test_regular2@mail.com'
74 TEST_USER_REGULAR2_EMAIL = 'test_regular2@mail.com'
75
75
76 HG_REPO = 'vcs_test_hg'
76 HG_REPO = 'vcs_test_hg'
77 GIT_REPO = 'vcs_test_git'
77 GIT_REPO = 'vcs_test_git'
78
78
79 NEW_HG_REPO = 'vcs_test_hg_new'
79 NEW_HG_REPO = 'vcs_test_hg_new'
80 NEW_GIT_REPO = 'vcs_test_git_new'
80 NEW_GIT_REPO = 'vcs_test_git_new'
81
81
82 HG_FORK = 'vcs_test_hg_fork'
82 HG_FORK = 'vcs_test_hg_fork'
83 GIT_FORK = 'vcs_test_git_fork'
83 GIT_FORK = 'vcs_test_git_fork'
84
84
85 ## VCS
85 ## VCS
86 SCM_TESTS = ['hg', 'git']
86 SCM_TESTS = ['hg', 'git']
87 uniq_suffix = str(int(time.mktime(datetime.datetime.now().timetuple())))
87 uniq_suffix = str(int(time.mktime(datetime.datetime.now().timetuple())))
88
88
89 GIT_REMOTE_REPO = 'git://github.com/codeinn/vcs.git'
89 GIT_REMOTE_REPO = 'git://github.com/codeinn/vcs.git'
90
90
91 TEST_GIT_REPO = jn(TESTS_TMP_PATH, GIT_REPO)
91 TEST_GIT_REPO = jn(TESTS_TMP_PATH, GIT_REPO)
92 TEST_GIT_REPO_CLONE = jn(TESTS_TMP_PATH, 'vcsgitclone%s' % uniq_suffix)
92 TEST_GIT_REPO_CLONE = jn(TESTS_TMP_PATH, 'vcsgitclone%s' % uniq_suffix)
93 TEST_GIT_REPO_PULL = jn(TESTS_TMP_PATH, 'vcsgitpull%s' % uniq_suffix)
93 TEST_GIT_REPO_PULL = jn(TESTS_TMP_PATH, 'vcsgitpull%s' % uniq_suffix)
94
94
95
95
96 HG_REMOTE_REPO = 'http://bitbucket.org/marcinkuzminski/vcs'
96 HG_REMOTE_REPO = 'http://bitbucket.org/marcinkuzminski/vcs'
97
97
98 TEST_HG_REPO = jn(TESTS_TMP_PATH, HG_REPO)
98 TEST_HG_REPO = jn(TESTS_TMP_PATH, HG_REPO)
99 TEST_HG_REPO_CLONE = jn(TESTS_TMP_PATH, 'vcshgclone%s' % uniq_suffix)
99 TEST_HG_REPO_CLONE = jn(TESTS_TMP_PATH, 'vcshgclone%s' % uniq_suffix)
100 TEST_HG_REPO_PULL = jn(TESTS_TMP_PATH, 'vcshgpull%s' % uniq_suffix)
100 TEST_HG_REPO_PULL = jn(TESTS_TMP_PATH, 'vcshgpull%s' % uniq_suffix)
101
101
102 TEST_DIR = tempfile.gettempdir()
102 TEST_DIR = tempfile.gettempdir()
103 TEST_REPO_PREFIX = 'vcs-test'
103 TEST_REPO_PREFIX = 'vcs-test'
104
104
105 # cached repos if any !
105 # cached repos if any !
106 # comment out to get some other repos from bb or github
106 # comment out to get some other repos from bb or github
107 GIT_REMOTE_REPO = jn(TESTS_TMP_PATH, GIT_REPO)
107 GIT_REMOTE_REPO = jn(TESTS_TMP_PATH, GIT_REPO)
108 HG_REMOTE_REPO = jn(TESTS_TMP_PATH, HG_REPO)
108 HG_REMOTE_REPO = jn(TESTS_TMP_PATH, HG_REPO)
109
109
110
110
111 def get_new_dir(title):
111 def get_new_dir(title):
112 """
112 """
113 Returns always new directory path.
113 Returns always new directory path.
114 """
114 """
115 from rhodecode.tests.vcs.utils import get_normalized_path
115 from rhodecode.tests.vcs.utils import get_normalized_path
116 name = TEST_REPO_PREFIX
116 name = TEST_REPO_PREFIX
117 if title:
117 if title:
118 name = '-'.join((name, title))
118 name = '-'.join((name, title))
119 hex = hashlib.sha1(str(time.time())).hexdigest()
119 hex = hashlib.sha1(str(time.time())).hexdigest()
120 name = '-'.join((name, hex))
120 name = '-'.join((name, hex))
121 path = os.path.join(TEST_DIR, name)
121 path = os.path.join(TEST_DIR, name)
122 return get_normalized_path(path)
122 return get_normalized_path(path)
123
123
124
124
125 class TestController(TestCase):
125 class TestController(TestCase):
126
126
127 def __init__(self, *args, **kwargs):
127 def __init__(self, *args, **kwargs):
128 wsgiapp = pylons.test.pylonsapp
128 wsgiapp = pylons.test.pylonsapp
129 config = wsgiapp.config
129 config = wsgiapp.config
130
130
131 self.app = TestApp(wsgiapp)
131 self.app = TestApp(wsgiapp)
132 url._push_object(URLGenerator(config['routes.map'], environ))
132 url._push_object(URLGenerator(config['routes.map'], environ))
133 self.Session = Session
133 self.Session = Session
134 self.index_location = config['app_conf']['index_dir']
134 self.index_location = config['app_conf']['index_dir']
135 TestCase.__init__(self, *args, **kwargs)
135 TestCase.__init__(self, *args, **kwargs)
136
136
137 def log_user(self, username=TEST_USER_ADMIN_LOGIN,
137 def log_user(self, username=TEST_USER_ADMIN_LOGIN,
138 password=TEST_USER_ADMIN_PASS):
138 password=TEST_USER_ADMIN_PASS):
139 self._logged_username = username
139 self._logged_username = username
140 response = self.app.post(url(controller='login', action='index'),
140 response = self.app.post(url(controller='login', action='index'),
141 {'username': username,
141 {'username': username,
142 'password': password})
142 'password': password})
143
143
144 if 'invalid user name' in response.body:
144 if 'invalid user name' in response.body:
145 self.fail('could not login using %s %s' % (username, password))
145 self.fail('could not login using %s %s' % (username, password))
146
146
147 self.assertEqual(response.status, '302 Found')
147 self.assertEqual(response.status, '302 Found')
148 ses = response.session['rhodecode_user']
148 ses = response.session['rhodecode_user']
149 self.assertEqual(ses.get('username'), username)
149 self.assertEqual(ses.get('username'), username)
150 response = response.follow()
150 response = response.follow()
151 self.assertEqual(ses.get('is_authenticated'), True)
151 self.assertEqual(ses.get('is_authenticated'), True)
152
152
153 return response.session['rhodecode_user']
153 return response.session['rhodecode_user']
154
154
155 def _get_logged_user(self):
155 def _get_logged_user(self):
156 return User.get_by_username(self._logged_username)
156 return User.get_by_username(self._logged_username)
157
157
158 def checkSessionFlash(self, response, msg):
158 def checkSessionFlash(self, response, msg):
159 self.assertTrue('flash' in response.session)
159 self.assertTrue('flash' in response.session)
160 self.assertTrue(msg in response.session['flash'][0][1])
160 self.assertTrue(msg in response.session['flash'][0][1])
@@ -1,52 +1,52 b''
1 from rhodecode.tests import *
1 from rhodecode.tests import *
2
2
3
3
4 class TestCompareController(TestController):
4 class TestCompareController(TestController):
5
5
6 def test_index_tag(self):
6 def test_index_tag(self):
7 self.log_user()
7 self.log_user()
8 tag1='0.1.3'
8 tag1='0.1.3'
9 tag2='0.1.2'
9 tag2='0.1.2'
10 response = self.app.get(url(controller='compare', action='index',
10 response = self.app.get(url(controller='compare', action='index',
11 repo_name=HG_REPO,
11 repo_name=HG_REPO,
12 org_ref_type="tag",
12 org_ref_type="tag",
13 org_ref=tag1,
13 org_ref=tag1,
14 other_ref_type="tag",
14 other_ref_type="tag",
15 other_ref=tag2,
15 other_ref=tag2,
16 ))
16 ))
17 response.mustcontain('%s@%s -> %s@%s' % (HG_REPO, tag1, HG_REPO, tag2))
17 response.mustcontain('%s@%s -> %s@%s' % (HG_REPO, tag1, HG_REPO, tag2))
18 ## outgoing changesets between tags
18 ## outgoing changesets between tags
19 response.mustcontain('''<a href="/%s/changeset/17544fbfcd33ffb439e2b728b5d526b1ef30bfcf">r120:17544fbfcd33</a>''' % HG_REPO)
19 response.mustcontain('''<a href="/%s/changeset/17544fbfcd33ffb439e2b728b5d526b1ef30bfcf">r120:17544fbfcd33</a>''' % HG_REPO)
20 response.mustcontain('''<a href="/%s/changeset/36e0fc9d2808c5022a24f49d6658330383ed8666">r119:36e0fc9d2808</a>''' % HG_REPO)
20 response.mustcontain('''<a href="/%s/changeset/36e0fc9d2808c5022a24f49d6658330383ed8666">r119:36e0fc9d2808</a>''' % HG_REPO)
21 response.mustcontain('''<a href="/%s/changeset/bb1a3ab98cc45cb934a77dcabf87a5a598b59e97">r118:bb1a3ab98cc4</a>''' % HG_REPO)
21 response.mustcontain('''<a href="/%s/changeset/bb1a3ab98cc45cb934a77dcabf87a5a598b59e97">r118:bb1a3ab98cc4</a>''' % HG_REPO)
22 response.mustcontain('''<a href="/%s/changeset/41fda979f02fda216374bf8edac4e83f69e7581c">r117:41fda979f02f</a>''' % HG_REPO)
22 response.mustcontain('''<a href="/%s/changeset/41fda979f02fda216374bf8edac4e83f69e7581c">r117:41fda979f02f</a>''' % HG_REPO)
23 response.mustcontain('''<a href="/%s/changeset/9749bfbfc0d2eba208d7947de266303b67c87cda">r116:9749bfbfc0d2</a>''' % HG_REPO)
23 response.mustcontain('''<a href="/%s/changeset/9749bfbfc0d2eba208d7947de266303b67c87cda">r116:9749bfbfc0d2</a>''' % HG_REPO)
24 response.mustcontain('''<a href="/%s/changeset/70d4cef8a37657ee4cf5aabb3bd9f68879769816">r115:70d4cef8a376</a>''' % HG_REPO)
24 response.mustcontain('''<a href="/%s/changeset/70d4cef8a37657ee4cf5aabb3bd9f68879769816">r115:70d4cef8a376</a>''' % HG_REPO)
25 response.mustcontain('''<a href="/%s/changeset/c5ddebc06eaaba3010c2d66ea6ec9d074eb0f678">r112:c5ddebc06eaa</a>''' % HG_REPO)
25 response.mustcontain('''<a href="/%s/changeset/c5ddebc06eaaba3010c2d66ea6ec9d074eb0f678">r112:c5ddebc06eaa</a>''' % HG_REPO)
26
26
27 ## files diff
27 ## files diff
28 response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--1c5cf9e91c12">docs/api/utils/index.rst</a></div>''' % (HG_REPO, tag1, tag2))
28 response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--1c5cf9e91c12">docs/api/utils/index.rst</a></div>''' % (HG_REPO, tag1, tag2))
29 response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--e3305437df55">test_and_report.sh</a></div>''' % (HG_REPO, tag1, tag2))
29 response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--e3305437df55">test_and_report.sh</a></div>''' % (HG_REPO, tag1, tag2))
30 response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--c8e92ef85cd1">.hgignore</a></div>''' % (HG_REPO, tag1, tag2))
30 response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--c8e92ef85cd1">.hgignore</a></div>''' % (HG_REPO, tag1, tag2))
31 response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--6e08b694d687">.hgtags</a></div>''' % (HG_REPO, tag1, tag2))
31 response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--6e08b694d687">.hgtags</a></div>''' % (HG_REPO, tag1, tag2))
32 response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--2c14b00f3393">docs/api/index.rst</a></div>''' % (HG_REPO, tag1, tag2))
32 response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--2c14b00f3393">docs/api/index.rst</a></div>''' % (HG_REPO, tag1, tag2))
33 response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--430ccbc82bdf">vcs/__init__.py</a></div>''' % (HG_REPO, tag1, tag2))
33 response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--430ccbc82bdf">vcs/__init__.py</a></div>''' % (HG_REPO, tag1, tag2))
34 response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--9c390eb52cd6">vcs/backends/hg.py</a></div>''' % (HG_REPO, tag1, tag2))
34 response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--9c390eb52cd6">vcs/backends/hg.py</a></div>''' % (HG_REPO, tag1, tag2))
35 response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--ebb592c595c0">vcs/utils/__init__.py</a></div>''' % (HG_REPO, tag1, tag2))
35 response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--ebb592c595c0">vcs/utils/__init__.py</a></div>''' % (HG_REPO, tag1, tag2))
36 response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--7abc741b5052">vcs/utils/annotate.py</a></div>''' % (HG_REPO, tag1, tag2))
36 response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--7abc741b5052">vcs/utils/annotate.py</a></div>''' % (HG_REPO, tag1, tag2))
37 response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--2ef0ef106c56">vcs/utils/diffs.py</a></div>''' % (HG_REPO, tag1, tag2))
37 response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--2ef0ef106c56">vcs/utils/diffs.py</a></div>''' % (HG_REPO, tag1, tag2))
38 response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--3150cb87d4b7">vcs/utils/lazy.py</a></div>''' % (HG_REPO, tag1, tag2))
38 response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--3150cb87d4b7">vcs/utils/lazy.py</a></div>''' % (HG_REPO, tag1, tag2))
39
39
40 def test_index_branch(self):
40 def test_index_branch(self):
41 self.log_user()
41 self.log_user()
42 response = self.app.get(url(controller='compare', action='index',
42 response = self.app.get(url(controller='compare', action='index',
43 repo_name=HG_REPO,
43 repo_name=HG_REPO,
44 org_ref_type="branch",
44 org_ref_type="branch",
45 org_ref='default',
45 org_ref='default',
46 other_ref_type="branch",
46 other_ref_type="branch",
47 other_ref='default',
47 other_ref='default',
48 ))
48 ))
49
49
50 response.mustcontain('%s@default -> %s@default' % (HG_REPO, HG_REPO))
50 response.mustcontain('%s@default -> %s@default' % (HG_REPO, HG_REPO))
51 # branch are equal
51 # branch are equal
52 response.mustcontain('<tr><td>No changesets</td></tr>')
52 response.mustcontain('<tr><td>No changesets</td></tr>')
General Comments 0
You need to be logged in to leave comments. Login now