##// END OF EJS Templates
Multiple changes for compare system...
marcink -
r3015:16af2498 beta
parent child Browse files
Show More
@@ -1,161 +1,167
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.compare
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 compare controller for pylons showoing differences between two
7 7 repos, branches, bookmarks or tips
8 8
9 9 :created_on: May 6, 2012
10 10 :author: marcink
11 11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 12 :license: GPLv3, see COPYING for more details.
13 13 """
14 14 # This program is free software: you can redistribute it and/or modify
15 15 # it under the terms of the GNU General Public License as published by
16 16 # the Free Software Foundation, either version 3 of the License, or
17 17 # (at your option) any later version.
18 18 #
19 19 # This program is distributed in the hope that it will be useful,
20 20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 22 # GNU General Public License for more details.
23 23 #
24 24 # You should have received a copy of the GNU General Public License
25 25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 26 import logging
27 27 import traceback
28 28
29 29 from webob.exc import HTTPNotFound
30 30 from pylons import request, response, session, tmpl_context as c, url
31 31 from pylons.controllers.util import abort, redirect
32 32 from pylons.i18n.translation import _
33 33
34 34 from rhodecode.lib.vcs.exceptions import EmptyRepositoryError, RepositoryError
35 35 from rhodecode.lib import helpers as h
36 36 from rhodecode.lib.base import BaseRepoController, render
37 37 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
38 38 from rhodecode.lib import diffs
39 39
40 40 from rhodecode.model.db import Repository
41 41 from rhodecode.model.pull_request import PullRequestModel
42 42 from webob.exc import HTTPBadRequest
43 43 from rhodecode.lib.utils2 import str2bool
44 44 from rhodecode.lib.diffs import LimitedDiffContainer
45 45
46 46 log = logging.getLogger(__name__)
47 47
48 48
49 49 class CompareController(BaseRepoController):
50 50
51 51 @LoginRequired()
52 52 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
53 53 'repository.admin')
54 54 def __before__(self):
55 55 super(CompareController, self).__before__()
56 56
57 57 def __get_cs_or_redirect(self, rev, repo, redirect_after=True,
58 58 partial=False):
59 59 """
60 60 Safe way to get changeset if error occur it redirects to changeset with
61 61 proper message. If partial is set then don't do redirect raise Exception
62 62 instead
63 63
64 64 :param rev: revision to fetch
65 65 :param repo: repo instance
66 66 """
67 67
68 68 try:
69 69 type_, rev = rev
70 70 return repo.scm_instance.get_changeset(rev)
71 71 except EmptyRepositoryError, e:
72 72 if not redirect_after:
73 73 return None
74 74 h.flash(h.literal(_('There are no changesets yet')),
75 75 category='warning')
76 76 redirect(url('summary_home', repo_name=repo.repo_name))
77 77
78 78 except RepositoryError, e:
79 79 log.error(traceback.format_exc())
80 80 h.flash(str(e), category='warning')
81 81 if not partial:
82 82 redirect(h.url('summary_home', repo_name=repo.repo_name))
83 83 raise HTTPBadRequest()
84 84
85 85 def index(self, org_ref_type, org_ref, other_ref_type, other_ref):
86 86
87 87 org_repo = c.rhodecode_db_repo.repo_name
88 88 org_ref = (org_ref_type, org_ref)
89 89 other_ref = (other_ref_type, other_ref)
90 90 other_repo = request.GET.get('repo', org_repo)
91 bundle_compare = str2bool(request.GET.get('bundle', True))
91 remote_compare = str2bool(request.GET.get('bundle', True))
92 92 c.fulldiff = fulldiff = request.GET.get('fulldiff')
93 93
94 94 c.swap_url = h.url('compare_url', repo_name=other_repo,
95 95 org_ref_type=other_ref[0], org_ref=other_ref[1],
96 96 other_ref_type=org_ref[0], other_ref=org_ref[1],
97 97 repo=org_repo, as_form=request.GET.get('as_form'),
98 bundle=bundle_compare)
98 bundle=remote_compare)
99 99
100 100 c.org_repo = org_repo = Repository.get_by_repo_name(org_repo)
101 101 c.other_repo = other_repo = Repository.get_by_repo_name(other_repo)
102 102
103 103 if c.org_repo is None or c.other_repo is None:
104 104 log.error('Could not found repo %s or %s' % (org_repo, other_repo))
105 105 raise HTTPNotFound
106 106
107 107 if c.org_repo != c.other_repo and h.is_git(c.rhodecode_repo):
108 108 log.error('compare of two remote repos not available for GIT REPOS')
109 109 raise HTTPNotFound
110 110
111 111 if c.org_repo.scm_instance.alias != c.other_repo.scm_instance.alias:
112 112 log.error('compare of two different kind of remote repos not available')
113 113 raise HTTPNotFound
114 114
115 115 partial = request.environ.get('HTTP_X_PARTIAL_XHR')
116 116 self.__get_cs_or_redirect(rev=org_ref, repo=org_repo, partial=partial)
117 117 self.__get_cs_or_redirect(rev=other_ref, repo=other_repo, partial=partial)
118 118
119 119 c.cs_ranges, discovery_data = PullRequestModel().get_compare_data(
120 120 org_repo, org_ref, other_repo, other_ref
121 121 )
122 122
123 123 c.statuses = c.rhodecode_db_repo.statuses([x.raw_id for x in
124 124 c.cs_ranges])
125 125 c.target_repo = c.repo_name
126 126 # defines that we need hidden inputs with changesets
127 127 c.as_form = request.GET.get('as_form', False)
128 128 if partial:
129 129 return render('compare/compare_cs.html')
130 130
131 if not bundle_compare and c.cs_ranges:
131 c.org_ref = org_ref[1]
132 c.other_ref = other_ref[1]
133
134 if not remote_compare and c.cs_ranges:
132 135 # case we want a simple diff without incoming changesets, just
133 136 # for review purposes. Make the diff on the forked repo, with
134 137 # revision that is common ancestor
135 138 other_ref = ('rev', c.cs_ranges[-1].parents[0].raw_id)
136 139 other_repo = org_repo
137 140
138 c.org_ref = org_ref[1]
139 c.other_ref = other_ref[1]
141 diff_limit = self.cut_off_limit if not fulldiff else None
142 _diff = diffs.differ(org_repo, org_ref, other_repo, other_ref,
143 discovery_data, remote_compare=remote_compare)
140 144
141 _diff = diffs.differ(other_repo, other_ref, org_repo, org_ref,
142 discovery_data, bundle_compare=bundle_compare)
143 diff_limit = self.cut_off_limit if not fulldiff else None
144 diff_processor = diffs.DiffProcessor(_diff, format='gitdiff',
145 diff_processor = diffs.DiffProcessor(_diff or '', format='gitdiff',
145 146 diff_limit=diff_limit)
146 147 _parsed = diff_processor.prepare()
147 148
148 149 c.limited_diff = False
149 150 if isinstance(_parsed, LimitedDiffContainer):
150 151 c.limited_diff = True
151 152
152 153 c.files = []
153 154 c.changes = {}
154
155 c.lines_added = 0
156 c.lines_deleted = 0
155 157 for f in _parsed:
158 st = f['stats']
159 if st[0] != 'b':
160 c.lines_added += st[0]
161 c.lines_deleted += st[1]
156 162 fid = h.FID('', f['filename'])
157 163 c.files.append([fid, f['operation'], f['filename'], f['stats']])
158 164 diff = diff_processor.as_html(enable_comments=False, parsed_lines=[f])
159 165 c.changes[fid] = [f['operation'], f['filename'], diff]
160 166
161 167 return render('compare/compare_diff.html')
@@ -1,446 +1,449
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.pullrequests
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 pull requests controller for rhodecode for initializing pull requests
7 7
8 8 :created_on: May 7, 2012
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25 import logging
26 26 import traceback
27 27 import formencode
28 28
29 29 from webob.exc import HTTPNotFound, HTTPForbidden
30 30 from collections import defaultdict
31 31 from itertools import groupby
32 32
33 33 from pylons import request, response, session, tmpl_context as c, url
34 34 from pylons.controllers.util import abort, redirect
35 35 from pylons.i18n.translation import _
36 36 from pylons.decorators import jsonify
37 37
38 38 from rhodecode.lib.compat import json
39 39 from rhodecode.lib.base import BaseRepoController, render
40 40 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator,\
41 41 NotAnonymous
42 42 from rhodecode.lib import helpers as h
43 43 from rhodecode.lib import diffs
44 44 from rhodecode.lib.utils import action_logger
45 45 from rhodecode.model.db import User, PullRequest, ChangesetStatus,\
46 46 ChangesetComment
47 47 from rhodecode.model.pull_request import PullRequestModel
48 48 from rhodecode.model.meta import Session
49 49 from rhodecode.model.repo import RepoModel
50 50 from rhodecode.model.comment import ChangesetCommentsModel
51 51 from rhodecode.model.changeset_status import ChangesetStatusModel
52 52 from rhodecode.model.forms import PullRequestForm
53 53 from rhodecode.lib.vcs.exceptions import EmptyRepositoryError
54 from rhodecode.lib.vcs.backends.base import EmptyChangeset
54 55
55 56 log = logging.getLogger(__name__)
56 57
57 58
58 59 class PullrequestsController(BaseRepoController):
59 60
60 61 @LoginRequired()
61 62 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
62 63 'repository.admin')
63 64 def __before__(self):
64 65 super(PullrequestsController, self).__before__()
65 66 repo_model = RepoModel()
66 67 c.users_array = repo_model.get_users_js()
67 68 c.users_groups_array = repo_model.get_users_groups_js()
68 69
69 70 def _get_repo_refs(self, repo):
70 71 hist_l = []
71 72
72 73 branches_group = ([('branch:%s:%s' % (k, v), k) for
73 74 k, v in repo.branches.iteritems()], _("Branches"))
74 75 bookmarks_group = ([('book:%s:%s' % (k, v), k) for
75 76 k, v in repo.bookmarks.iteritems()], _("Bookmarks"))
76 77 tags_group = ([('tag:%s:%s' % (k, v), k) for
77 78 k, v in repo.tags.iteritems()], _("Tags"))
78 79
79 80 hist_l.append(bookmarks_group)
80 81 hist_l.append(branches_group)
81 82 hist_l.append(tags_group)
82 83
83 84 return hist_l
84 85
85 86 def _get_default_rev(self, repo):
86 87 """
87 88 Get's default revision to do compare on pull request
88 89
89 90 :param repo:
90 91 """
91 92 repo = repo.scm_instance
92 93 if 'default' in repo.branches:
93 94 return 'default'
94 95 else:
95 96 #if repo doesn't have default branch return first found
96 97 return repo.branches.keys()[0]
97 98
98 99 def show_all(self, repo_name):
99 100 c.pull_requests = PullRequestModel().get_all(repo_name)
100 101 c.repo_name = repo_name
101 102 return render('/pullrequests/pullrequest_show_all.html')
102 103
103 104 @NotAnonymous()
104 105 def index(self):
105 106 org_repo = c.rhodecode_db_repo
106 107
107 108 if org_repo.scm_instance.alias != 'hg':
108 109 log.error('Review not available for GIT REPOS')
109 110 raise HTTPNotFound
110 111
111 112 try:
112 113 org_repo.scm_instance.get_changeset()
113 114 except EmptyRepositoryError, e:
114 115 h.flash(h.literal(_('There are no changesets yet')),
115 116 category='warning')
116 117 redirect(url('summary_home', repo_name=org_repo.repo_name))
117 118
118 119 other_repos_info = {}
119 120
120 121 c.org_refs = self._get_repo_refs(c.rhodecode_repo)
121 122 c.org_repos = []
122 123 c.other_repos = []
123 124 c.org_repos.append((org_repo.repo_name, '%s/%s' % (
124 125 org_repo.user.username, c.repo_name))
125 126 )
126 127
127 128 # add org repo to other so we can open pull request agains itself
128 129 c.other_repos.extend(c.org_repos)
129 130
130 131 c.default_pull_request = org_repo.repo_name # repo name pre-selected
131 132 c.default_pull_request_rev = self._get_default_rev(org_repo) # revision pre-selected
132 133 c.default_revs = self._get_repo_refs(org_repo.scm_instance)
133 134 #add orginal repo
134 135 other_repos_info[org_repo.repo_name] = {
135 136 'gravatar': h.gravatar_url(org_repo.user.email, 24),
136 137 'description': org_repo.description,
137 138 'revs': h.select('other_ref', '', c.default_revs, class_='refs')
138 139 }
139 140
140 141 #gather forks and add to this list
141 142 for fork in org_repo.forks:
142 143 c.other_repos.append((fork.repo_name, '%s/%s' % (
143 144 fork.user.username, fork.repo_name))
144 145 )
145 146 other_repos_info[fork.repo_name] = {
146 147 'gravatar': h.gravatar_url(fork.user.email, 24),
147 148 'description': fork.description,
148 149 'revs': h.select('other_ref', '',
149 150 self._get_repo_refs(fork.scm_instance),
150 151 class_='refs')
151 152 }
152 153 #add parents of this fork also, but only if it's not empty
153 154 if org_repo.parent and org_repo.parent.scm_instance.revisions:
154 155 c.default_pull_request = org_repo.parent.repo_name
155 156 c.default_pull_request_rev = self._get_default_rev(org_repo.parent)
156 157 c.default_revs = self._get_repo_refs(org_repo.parent.scm_instance)
157 158 c.other_repos.append((org_repo.parent.repo_name, '%s/%s' % (
158 159 org_repo.parent.user.username,
159 160 org_repo.parent.repo_name))
160 161 )
161 162 other_repos_info[org_repo.parent.repo_name] = {
162 163 'gravatar': h.gravatar_url(org_repo.parent.user.email, 24),
163 164 'description': org_repo.parent.description,
164 165 'revs': h.select('other_ref', '',
165 166 self._get_repo_refs(org_repo.parent.scm_instance),
166 167 class_='refs')
167 168 }
168 169
169 170 c.other_repos_info = json.dumps(other_repos_info)
170 171 c.review_members = [org_repo.user]
171 172 return render('/pullrequests/pullrequest.html')
172 173
173 174 @NotAnonymous()
174 175 def create(self, repo_name):
175 176 repo = RepoModel()._get_repo(repo_name)
176 177 try:
177 178 _form = PullRequestForm(repo.repo_id)().to_python(request.POST)
178 179 except formencode.Invalid, errors:
179 180 log.error(traceback.format_exc())
180 181 if errors.error_dict.get('revisions'):
181 182 msg = 'Revisions: %s' % errors.error_dict['revisions']
182 183 elif errors.error_dict.get('pullrequest_title'):
183 184 msg = _('Pull request requires a title with min. 3 chars')
184 185 else:
185 186 msg = _('error during creation of pull request')
186 187
187 188 h.flash(msg, 'error')
188 189 return redirect(url('pullrequest_home', repo_name=repo_name))
189 190
190 191 org_repo = _form['org_repo']
191 192 org_ref = _form['org_ref']
192 193 other_repo = _form['other_repo']
193 194 other_ref = _form['other_ref']
194 195 revisions = _form['revisions']
195 196 reviewers = _form['review_members']
196 197
197 198 title = _form['pullrequest_title']
198 199 description = _form['pullrequest_desc']
199 200
200 201 try:
201 202 pull_request = PullRequestModel().create(
202 203 self.rhodecode_user.user_id, org_repo, org_ref, other_repo,
203 204 other_ref, revisions, reviewers, title, description
204 205 )
205 206 Session().commit()
206 207 h.flash(_('Successfully opened new pull request'),
207 208 category='success')
208 209 except Exception:
209 210 h.flash(_('Error occurred during sending pull request'),
210 211 category='error')
211 212 log.error(traceback.format_exc())
212 213 return redirect(url('pullrequest_home', repo_name=repo_name))
213 214
214 215 return redirect(url('pullrequest_show', repo_name=other_repo,
215 216 pull_request_id=pull_request.pull_request_id))
216 217
217 218 @NotAnonymous()
218 219 @jsonify
219 220 def update(self, repo_name, pull_request_id):
220 221 pull_request = PullRequest.get_or_404(pull_request_id)
221 222 if pull_request.is_closed():
222 223 raise HTTPForbidden()
223 224 #only owner or admin can update it
224 225 owner = pull_request.author.user_id == c.rhodecode_user.user_id
225 226 if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
226 227 reviewers_ids = map(int, filter(lambda v: v not in [None, ''],
227 228 request.POST.get('reviewers_ids', '').split(',')))
228 229
229 230 PullRequestModel().update_reviewers(pull_request_id, reviewers_ids)
230 231 Session.commit()
231 232 return True
232 233 raise HTTPForbidden()
233 234
234 235 @NotAnonymous()
235 236 @jsonify
236 237 def delete(self, repo_name, pull_request_id):
237 238 pull_request = PullRequest.get_or_404(pull_request_id)
238 239 #only owner can delete it !
239 240 if pull_request.author.user_id == c.rhodecode_user.user_id:
240 241 PullRequestModel().delete(pull_request)
241 242 Session().commit()
242 243 h.flash(_('Successfully deleted pull request'),
243 244 category='success')
244 245 return redirect(url('admin_settings_my_account'))
245 246 raise HTTPForbidden()
246 247
247 248 def _load_compare_data(self, pull_request, enable_comments=True):
248 249 """
249 250 Load context data needed for generating compare diff
250 251
251 252 :param pull_request:
252 253 :type pull_request:
253 254 """
254 255
255 256 org_repo = pull_request.org_repo
256 257 (org_ref_type,
257 258 org_ref_name,
258 259 org_ref_rev) = pull_request.org_ref.split(':')
259 260
260 261 other_repo = pull_request.other_repo
261 262 (other_ref_type,
262 263 other_ref_name,
263 264 other_ref_rev) = pull_request.other_ref.split(':')
264 265
265 266 # despite opening revisions for bookmarks/branches/tags, we always
266 267 # convert this to rev to prevent changes after book or branch change
267 268 org_ref = ('rev', org_ref_rev)
268 269 other_ref = ('rev', other_ref_rev)
269 270
270 271 c.org_repo = org_repo
271 272 c.other_repo = other_repo
272 273
273 274 c.cs_ranges, discovery_data = PullRequestModel().get_compare_data(
274 275 org_repo, org_ref, other_repo, other_ref
275 276 )
276 277 if c.cs_ranges:
277 278 # case we want a simple diff without incoming changesets, just
278 279 # for review purposes. Make the diff on the forked repo, with
279 280 # revision that is common ancestor
280 other_ref = ('rev', c.cs_ranges[-1].parents[0].raw_id)
281 other_ref = ('rev', getattr(c.cs_ranges[-1].parents[0]
282 if c.cs_ranges[-1].parents
283 else EmptyChangeset(), 'raw_id'))
281 284 other_repo = org_repo
282 285
283 286 c.statuses = org_repo.statuses([x.raw_id for x in c.cs_ranges])
284 287 # defines that we need hidden inputs with changesets
285 288 c.as_form = request.GET.get('as_form', False)
286 289
287 290 c.org_ref = org_ref[1]
288 291 c.other_ref = other_ref[1]
289 # diff needs to have swapped org with other to generate proper diff
290 _diff = diffs.differ(other_repo, other_ref, org_repo, org_ref,
292 _diff = diffs.differ(org_repo, org_ref, other_repo, other_ref,
291 293 discovery_data)
294
292 295 diff_processor = diffs.DiffProcessor(_diff, format='gitdiff')
293 296 _parsed = diff_processor.prepare()
294 297
295 298 c.files = []
296 299 c.changes = {}
297 300
298 301 for f in _parsed:
299 302 fid = h.FID('', f['filename'])
300 303 c.files.append([fid, f['operation'], f['filename'], f['stats']])
301 304 diff = diff_processor.as_html(enable_comments=enable_comments,
302 305 parsed_lines=[f])
303 306 c.changes[fid] = [f['operation'], f['filename'], diff]
304 307
305 308 def show(self, repo_name, pull_request_id):
306 309 repo_model = RepoModel()
307 310 c.users_array = repo_model.get_users_js()
308 311 c.users_groups_array = repo_model.get_users_groups_js()
309 312 c.pull_request = PullRequest.get_or_404(pull_request_id)
310 313 c.target_repo = c.pull_request.org_repo.repo_name
311 314
312 315 cc_model = ChangesetCommentsModel()
313 316 cs_model = ChangesetStatusModel()
314 317 _cs_statuses = cs_model.get_statuses(c.pull_request.org_repo,
315 318 pull_request=c.pull_request,
316 319 with_revisions=True)
317 320
318 321 cs_statuses = defaultdict(list)
319 322 for st in _cs_statuses:
320 323 cs_statuses[st.author.username] += [st]
321 324
322 325 c.pull_request_reviewers = []
323 326 c.pull_request_pending_reviewers = []
324 327 for o in c.pull_request.reviewers:
325 328 st = cs_statuses.get(o.user.username, None)
326 329 if st:
327 330 sorter = lambda k: k.version
328 331 st = [(x, list(y)[0])
329 332 for x, y in (groupby(sorted(st, key=sorter), sorter))]
330 333 else:
331 334 c.pull_request_pending_reviewers.append(o.user)
332 335 c.pull_request_reviewers.append([o.user, st])
333 336
334 337 # pull_requests repo_name we opened it against
335 338 # ie. other_repo must match
336 339 if repo_name != c.pull_request.other_repo.repo_name:
337 340 raise HTTPNotFound
338 341
339 342 # load compare data into template context
340 343 enable_comments = not c.pull_request.is_closed()
341 344 self._load_compare_data(c.pull_request, enable_comments=enable_comments)
342 345
343 346 # inline comments
344 347 c.inline_cnt = 0
345 348 c.inline_comments = cc_model.get_inline_comments(
346 349 c.rhodecode_db_repo.repo_id,
347 350 pull_request=pull_request_id)
348 351 # count inline comments
349 352 for __, lines in c.inline_comments:
350 353 for comments in lines.values():
351 354 c.inline_cnt += len(comments)
352 355 # comments
353 356 c.comments = cc_model.get_comments(c.rhodecode_db_repo.repo_id,
354 357 pull_request=pull_request_id)
355 358
356 359 try:
357 360 cur_status = c.statuses[c.pull_request.revisions[0]][0]
358 361 except:
359 362 log.error(traceback.format_exc())
360 363 cur_status = 'undefined'
361 364 if c.pull_request.is_closed() and 0:
362 365 c.current_changeset_status = cur_status
363 366 else:
364 367 # changeset(pull-request) status calulation based on reviewers
365 368 c.current_changeset_status = cs_model.calculate_status(
366 369 c.pull_request_reviewers,
367 370 )
368 371 c.changeset_statuses = ChangesetStatus.STATUSES
369 372
370 373 return render('/pullrequests/pullrequest_show.html')
371 374
372 375 @NotAnonymous()
373 376 @jsonify
374 377 def comment(self, repo_name, pull_request_id):
375 378 pull_request = PullRequest.get_or_404(pull_request_id)
376 379 if pull_request.is_closed():
377 380 raise HTTPForbidden()
378 381
379 382 status = request.POST.get('changeset_status')
380 383 change_status = request.POST.get('change_changeset_status')
381 384 text = request.POST.get('text')
382 385 if status and change_status:
383 386 text = text or (_('Status change -> %s')
384 387 % ChangesetStatus.get_status_lbl(status))
385 388 comm = ChangesetCommentsModel().create(
386 389 text=text,
387 390 repo=c.rhodecode_db_repo.repo_id,
388 391 user=c.rhodecode_user.user_id,
389 392 pull_request=pull_request_id,
390 393 f_path=request.POST.get('f_path'),
391 394 line_no=request.POST.get('line'),
392 395 status_change=(ChangesetStatus.get_status_lbl(status)
393 396 if status and change_status else None)
394 397 )
395 398
396 399 # get status if set !
397 400 if status and change_status:
398 401 ChangesetStatusModel().set_status(
399 402 c.rhodecode_db_repo.repo_id,
400 403 status,
401 404 c.rhodecode_user.user_id,
402 405 comm,
403 406 pull_request=pull_request_id
404 407 )
405 408 action_logger(self.rhodecode_user,
406 409 'user_commented_pull_request:%s' % pull_request_id,
407 410 c.rhodecode_db_repo, self.ip_addr, self.sa)
408 411
409 412 if request.POST.get('save_close'):
410 413 PullRequestModel().close_pull_request(pull_request_id)
411 414 action_logger(self.rhodecode_user,
412 415 'user_closed_pull_request:%s' % pull_request_id,
413 416 c.rhodecode_db_repo, self.ip_addr, self.sa)
414 417
415 418 Session().commit()
416 419
417 420 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
418 421 return redirect(h.url('pullrequest_show', repo_name=repo_name,
419 422 pull_request_id=pull_request_id))
420 423
421 424 data = {
422 425 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
423 426 }
424 427 if comm:
425 428 c.co = comm
426 429 data.update(comm.get_dict())
427 430 data.update({'rendered_text':
428 431 render('changeset/changeset_comment_block.html')})
429 432
430 433 return data
431 434
432 435 @NotAnonymous()
433 436 @jsonify
434 437 def delete_comment(self, repo_name, comment_id):
435 438 co = ChangesetComment.get(comment_id)
436 439 if co.pull_request.is_closed():
437 440 #don't allow deleting comments on closed pull request
438 441 raise HTTPForbidden()
439 442
440 443 owner = lambda: co.author.user_id == c.rhodecode_user.user_id
441 444 if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
442 445 ChangesetCommentsModel().delete(comment=co)
443 446 Session().commit()
444 447 return True
445 448 else:
446 449 raise HTTPForbidden()
@@ -1,771 +1,768
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.diffs
4 4 ~~~~~~~~~~~~~~~~~~~
5 5
6 6 Set of diffing helpers, previously part of vcs
7 7
8 8
9 9 :created_on: Dec 4, 2011
10 10 :author: marcink
11 11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 12 :original copyright: 2007-2008 by Armin Ronacher
13 13 :license: GPLv3, see COPYING for more details.
14 14 """
15 15 # This program is free software: you can redistribute it and/or modify
16 16 # it under the terms of the GNU General Public License as published by
17 17 # the Free Software Foundation, either version 3 of the License, or
18 18 # (at your option) any later version.
19 19 #
20 20 # This program is distributed in the hope that it will be useful,
21 21 # but WITHOUT ANY WARRANTY; without even the implied warranty of
22 22 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23 23 # GNU General Public License for more details.
24 24 #
25 25 # You should have received a copy of the GNU General Public License
26 26 # along with this program. If not, see <http://www.gnu.org/licenses/>.
27 27
28 28 import re
29 29 import difflib
30 30 import logging
31 31 import traceback
32 32
33 33 from itertools import tee, imap
34 34
35 35 from mercurial import patch
36 36 from mercurial.mdiff import diffopts
37 37 from mercurial.bundlerepo import bundlerepository
38 38
39 39 from pylons.i18n.translation import _
40 40
41 41 from rhodecode.lib.compat import BytesIO
42 42 from rhodecode.lib.vcs.utils.hgcompat import localrepo
43 43 from rhodecode.lib.vcs.exceptions import VCSError
44 44 from rhodecode.lib.vcs.nodes import FileNode, SubModuleNode
45 45 from rhodecode.lib.vcs.backends.base import EmptyChangeset
46 46 from rhodecode.lib.helpers import escape
47 47 from rhodecode.lib.utils import make_ui
48 48 from rhodecode.lib.utils2 import safe_unicode
49 49
50 50 log = logging.getLogger(__name__)
51 51
52 52
53 53 def wrap_to_table(str_):
54 54 return '''<table class="code-difftable">
55 55 <tr class="line no-comment">
56 56 <td class="lineno new"></td>
57 57 <td class="code no-comment"><pre>%s</pre></td>
58 58 </tr>
59 59 </table>''' % str_
60 60
61 61
62 62 def wrapped_diff(filenode_old, filenode_new, cut_off_limit=None,
63 63 ignore_whitespace=True, line_context=3,
64 64 enable_comments=False):
65 65 """
66 66 returns a wrapped diff into a table, checks for cut_off_limit and presents
67 67 proper message
68 68 """
69 69
70 70 if filenode_old is None:
71 71 filenode_old = FileNode(filenode_new.path, '', EmptyChangeset())
72 72
73 73 if filenode_old.is_binary or filenode_new.is_binary:
74 74 diff = wrap_to_table(_('binary file'))
75 75 stats = (0, 0)
76 76 size = 0
77 77
78 78 elif cut_off_limit != -1 and (cut_off_limit is None or
79 79 (filenode_old.size < cut_off_limit and filenode_new.size < cut_off_limit)):
80 80
81 81 f_gitdiff = get_gitdiff(filenode_old, filenode_new,
82 82 ignore_whitespace=ignore_whitespace,
83 83 context=line_context)
84 84 diff_processor = DiffProcessor(f_gitdiff, format='gitdiff')
85 85
86 86 diff = diff_processor.as_html(enable_comments=enable_comments)
87 87 stats = diff_processor.stat()
88 88 size = len(diff or '')
89 89 else:
90 90 diff = wrap_to_table(_('Changeset was too big and was cut off, use '
91 91 'diff menu to display this diff'))
92 92 stats = (0, 0)
93 93 size = 0
94 94 if not diff:
95 95 submodules = filter(lambda o: isinstance(o, SubModuleNode),
96 96 [filenode_new, filenode_old])
97 97 if submodules:
98 98 diff = wrap_to_table(escape('Submodule %r' % submodules[0]))
99 99 else:
100 100 diff = wrap_to_table(_('No changes detected'))
101 101
102 102 cs1 = filenode_old.changeset.raw_id
103 103 cs2 = filenode_new.changeset.raw_id
104 104
105 105 return size, cs1, cs2, diff, stats
106 106
107 107
108 108 def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True, context=3):
109 109 """
110 110 Returns git style diff between given ``filenode_old`` and ``filenode_new``.
111 111
112 112 :param ignore_whitespace: ignore whitespaces in diff
113 113 """
114 114 # make sure we pass in default context
115 115 context = context or 3
116 116 submodules = filter(lambda o: isinstance(o, SubModuleNode),
117 117 [filenode_new, filenode_old])
118 118 if submodules:
119 119 return ''
120 120
121 121 for filenode in (filenode_old, filenode_new):
122 122 if not isinstance(filenode, FileNode):
123 123 raise VCSError("Given object should be FileNode object, not %s"
124 124 % filenode.__class__)
125 125
126 126 repo = filenode_new.changeset.repository
127 127 old_raw_id = getattr(filenode_old.changeset, 'raw_id', repo.EMPTY_CHANGESET)
128 128 new_raw_id = getattr(filenode_new.changeset, 'raw_id', repo.EMPTY_CHANGESET)
129 129
130 130 vcs_gitdiff = repo.get_diff(old_raw_id, new_raw_id, filenode_new.path,
131 131 ignore_whitespace, context)
132 132 return vcs_gitdiff
133 133
134 134 NEW_FILENODE = 1
135 135 DEL_FILENODE = 2
136 136 MOD_FILENODE = 3
137 137 RENAMED_FILENODE = 4
138 138 CHMOD_FILENODE = 5
139 139
140 140
141 141 class DiffLimitExceeded(Exception):
142 142 pass
143 143
144 144
145 145 class LimitedDiffContainer(object):
146 146
147 147 def __init__(self, diff_limit, cur_diff_size, diff):
148 148 self.diff = diff
149 149 self.diff_limit = diff_limit
150 150 self.cur_diff_size = cur_diff_size
151 151
152 152 def __iter__(self):
153 153 for l in self.diff:
154 154 yield l
155 155
156 156
157 157 class DiffProcessor(object):
158 158 """
159 159 Give it a unified or git diff and it returns a list of the files that were
160 160 mentioned in the diff together with a dict of meta information that
161 161 can be used to render it in a HTML template.
162 162 """
163 163 _chunk_re = re.compile(r'@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
164 164 _newline_marker = '\\ No newline at end of file\n'
165 165 _git_header_re = re.compile(r"""
166 166 #^diff[ ]--git
167 167 [ ]a/(?P<a_path>.+?)[ ]b/(?P<b_path>.+?)\n
168 168 (?:^similarity[ ]index[ ](?P<similarity_index>\d+)%\n
169 169 ^rename[ ]from[ ](?P<rename_from>\S+)\n
170 170 ^rename[ ]to[ ](?P<rename_to>\S+)(?:\n|$))?
171 171 (?:^old[ ]mode[ ](?P<old_mode>\d+)\n
172 172 ^new[ ]mode[ ](?P<new_mode>\d+)(?:\n|$))?
173 173 (?:^new[ ]file[ ]mode[ ](?P<new_file_mode>.+)(?:\n|$))?
174 174 (?:^deleted[ ]file[ ]mode[ ](?P<deleted_file_mode>.+)(?:\n|$))?
175 175 (?:^index[ ](?P<a_blob_id>[0-9A-Fa-f]+)
176 176 \.\.(?P<b_blob_id>[0-9A-Fa-f]+)[ ]?(?P<b_mode>.+)?(?:\n|$))?
177 177 (?:^---[ ](a/(?P<a_file>.+)|/dev/null)(?:\n|$))?
178 178 (?:^\+\+\+[ ](b/(?P<b_file>.+)|/dev/null)(?:\n|$))?
179 179 """, re.VERBOSE | re.MULTILINE)
180 180 _hg_header_re = re.compile(r"""
181 181 #^diff[ ]--git
182 182 [ ]a/(?P<a_path>.+?)[ ]b/(?P<b_path>.+?)\n
183 183 (?:^similarity[ ]index[ ](?P<similarity_index>\d+)%(?:\n|$))?
184 184 (?:^rename[ ]from[ ](?P<rename_from>\S+)\n
185 185 ^rename[ ]to[ ](?P<rename_to>\S+)(?:\n|$))?
186 186 (?:^old[ ]mode[ ](?P<old_mode>\d+)\n
187 187 ^new[ ]mode[ ](?P<new_mode>\d+)(?:\n|$))?
188 188 (?:^new[ ]file[ ]mode[ ](?P<new_file_mode>.+)(?:\n|$))?
189 189 (?:^deleted[ ]file[ ]mode[ ](?P<deleted_file_mode>.+)(?:\n|$))?
190 190 (?:^index[ ](?P<a_blob_id>[0-9A-Fa-f]+)
191 191 \.\.(?P<b_blob_id>[0-9A-Fa-f]+)[ ]?(?P<b_mode>.+)?(?:\n|$))?
192 192 (?:^---[ ](a/(?P<a_file>.+)|/dev/null)(?:\n|$))?
193 193 (?:^\+\+\+[ ](b/(?P<b_file>.+)|/dev/null)(?:\n|$))?
194 194 """, re.VERBOSE | re.MULTILINE)
195 195
196 196 def __init__(self, diff, vcs='hg', format='gitdiff', diff_limit=None):
197 197 """
198 198 :param diff: a text in diff format
199 199 :param vcs: type of version controll hg or git
200 200 :param format: format of diff passed, `udiff` or `gitdiff`
201 201 :param diff_limit: define the size of diff that is considered "big"
202 202 based on that parameter cut off will be triggered, set to None
203 203 to show full diff
204 204 """
205 205 if not isinstance(diff, basestring):
206 206 raise Exception('Diff must be a basestring got %s instead' % type(diff))
207 207
208 208 self._diff = diff
209 209 self._format = format
210 210 self.adds = 0
211 211 self.removes = 0
212 212 # calculate diff size
213 213 self.diff_size = len(diff)
214 214 self.diff_limit = diff_limit
215 215 self.cur_diff_size = 0
216 216 self.parsed = False
217 217 self.parsed_diff = []
218 218 self.vcs = vcs
219 219
220 220 if format == 'gitdiff':
221 221 self.differ = self._highlight_line_difflib
222 222 self._parser = self._parse_gitdiff
223 223 else:
224 224 self.differ = self._highlight_line_udiff
225 225 self._parser = self._parse_udiff
226 226
227 227 def _copy_iterator(self):
228 228 """
229 229 make a fresh copy of generator, we should not iterate thru
230 230 an original as it's needed for repeating operations on
231 231 this instance of DiffProcessor
232 232 """
233 233 self.__udiff, iterator_copy = tee(self.__udiff)
234 234 return iterator_copy
235 235
236 236 def _escaper(self, string):
237 237 """
238 238 Escaper for diff escapes special chars and checks the diff limit
239 239
240 240 :param string:
241 241 :type string:
242 242 """
243 243
244 244 self.cur_diff_size += len(string)
245 245
246 246 # escaper get's iterated on each .next() call and it checks if each
247 247 # parsed line doesn't exceed the diff limit
248 248 if self.diff_limit is not None and self.cur_diff_size > self.diff_limit:
249 249 raise DiffLimitExceeded('Diff Limit Exceeded')
250 250
251 251 return safe_unicode(string).replace('&', '&amp;')\
252 252 .replace('<', '&lt;')\
253 253 .replace('>', '&gt;')
254 254
255 255 def _line_counter(self, l):
256 256 """
257 257 Checks each line and bumps total adds/removes for this diff
258 258
259 259 :param l:
260 260 """
261 261 if l.startswith('+') and not l.startswith('+++'):
262 262 self.adds += 1
263 263 elif l.startswith('-') and not l.startswith('---'):
264 264 self.removes += 1
265 265 return safe_unicode(l)
266 266
267 267 def _highlight_line_difflib(self, line, next_):
268 268 """
269 269 Highlight inline changes in both lines.
270 270 """
271 271
272 272 if line['action'] == 'del':
273 273 old, new = line, next_
274 274 else:
275 275 old, new = next_, line
276 276
277 277 oldwords = re.split(r'(\W)', old['line'])
278 278 newwords = re.split(r'(\W)', new['line'])
279 279
280 280 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
281 281
282 282 oldfragments, newfragments = [], []
283 283 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
284 284 oldfrag = ''.join(oldwords[i1:i2])
285 285 newfrag = ''.join(newwords[j1:j2])
286 286 if tag != 'equal':
287 287 if oldfrag:
288 288 oldfrag = '<del>%s</del>' % oldfrag
289 289 if newfrag:
290 290 newfrag = '<ins>%s</ins>' % newfrag
291 291 oldfragments.append(oldfrag)
292 292 newfragments.append(newfrag)
293 293
294 294 old['line'] = "".join(oldfragments)
295 295 new['line'] = "".join(newfragments)
296 296
297 297 def _highlight_line_udiff(self, line, next_):
298 298 """
299 299 Highlight inline changes in both lines.
300 300 """
301 301 start = 0
302 302 limit = min(len(line['line']), len(next_['line']))
303 303 while start < limit and line['line'][start] == next_['line'][start]:
304 304 start += 1
305 305 end = -1
306 306 limit -= start
307 307 while -end <= limit and line['line'][end] == next_['line'][end]:
308 308 end -= 1
309 309 end += 1
310 310 if start or end:
311 311 def do(l):
312 312 last = end + len(l['line'])
313 313 if l['action'] == 'add':
314 314 tag = 'ins'
315 315 else:
316 316 tag = 'del'
317 317 l['line'] = '%s<%s>%s</%s>%s' % (
318 318 l['line'][:start],
319 319 tag,
320 320 l['line'][start:last],
321 321 tag,
322 322 l['line'][last:]
323 323 )
324 324 do(line)
325 325 do(next_)
326 326
327 327 def _get_header(self, diff_chunk):
328 328 """
329 329 parses the diff header, and returns parts, and leftover diff
330 330 parts consists of 14 elements::
331 331
332 332 a_path, b_path, similarity_index, rename_from, rename_to,
333 333 old_mode, new_mode, new_file_mode, deleted_file_mode,
334 334 a_blob_id, b_blob_id, b_mode, a_file, b_file
335 335
336 336 :param diff_chunk:
337 337 :type diff_chunk:
338 338 """
339 339
340 340 if self.vcs == 'git':
341 341 match = self._git_header_re.match(diff_chunk)
342 342 diff = diff_chunk[match.end():]
343 343 return match.groupdict(), imap(self._escaper, diff.splitlines(1))
344 344 elif self.vcs == 'hg':
345 345 match = self._hg_header_re.match(diff_chunk)
346 346 diff = diff_chunk[match.end():]
347 347 return match.groupdict(), imap(self._escaper, diff.splitlines(1))
348 348 else:
349 349 raise Exception('VCS type %s is not supported' % self.vcs)
350 350
351 351 def _parse_gitdiff(self, inline_diff=True):
352 352 _files = []
353 353 diff_container = lambda arg: arg
354 354
355 355 ##split the diff in chunks of separate --git a/file b/file chunks
356 356 for raw_diff in ('\n' + self._diff).split('\ndiff --git')[1:]:
357 357 binary = False
358 358 binary_msg = 'unknown binary'
359 359 head, diff = self._get_header(raw_diff)
360 360
361 361 if not head['a_file'] and head['b_file']:
362 362 op = 'A'
363 363 elif head['a_file'] and head['b_file']:
364 364 op = 'M'
365 365 elif head['a_file'] and not head['b_file']:
366 366 op = 'D'
367 367 else:
368 368 #probably we're dealing with a binary file 1
369 369 binary = True
370 370 if head['deleted_file_mode']:
371 371 op = 'D'
372 372 stats = ['b', DEL_FILENODE]
373 373 binary_msg = 'deleted binary file'
374 374 elif head['new_file_mode']:
375 375 op = 'A'
376 376 stats = ['b', NEW_FILENODE]
377 377 binary_msg = 'new binary file %s' % head['new_file_mode']
378 378 else:
379 379 if head['new_mode'] and head['old_mode']:
380 380 stats = ['b', CHMOD_FILENODE]
381 381 op = 'M'
382 382 binary_msg = ('modified binary file chmod %s => %s'
383 383 % (head['old_mode'], head['new_mode']))
384 384 elif (head['rename_from'] and head['rename_to']
385 385 and head['rename_from'] != head['rename_to']):
386 386 stats = ['b', RENAMED_FILENODE]
387 387 op = 'M'
388 388 binary_msg = ('file renamed from %s to %s'
389 389 % (head['rename_from'], head['rename_to']))
390 390 else:
391 391 stats = ['b', MOD_FILENODE]
392 392 op = 'M'
393 393 binary_msg = 'modified binary file'
394 394
395 395 if not binary:
396 396 try:
397 397 chunks, stats = self._parse_lines(diff)
398 398 except DiffLimitExceeded:
399 399 diff_container = lambda _diff: LimitedDiffContainer(
400 400 self.diff_limit,
401 401 self.cur_diff_size,
402 402 _diff)
403 403 break
404 404 else:
405 405 chunks = []
406 406 chunks.append([{
407 407 'old_lineno': '',
408 408 'new_lineno': '',
409 409 'action': 'binary',
410 410 'line': binary_msg,
411 411 }])
412 412
413 413 _files.append({
414 414 'filename': head['b_path'],
415 415 'old_revision': head['a_blob_id'],
416 416 'new_revision': head['b_blob_id'],
417 417 'chunks': chunks,
418 418 'operation': op,
419 419 'stats': stats,
420 420 })
421 421
422 422 sorter = lambda info: {'A': 0, 'M': 1, 'D': 2}.get(info['operation'])
423 423
424 424 if inline_diff is False:
425 425 return diff_container(sorted(_files, key=sorter))
426 426
427 427 # highlight inline changes
428 428 for diff_data in _files:
429 429 for chunk in diff_data['chunks']:
430 430 lineiter = iter(chunk)
431 431 try:
432 432 while 1:
433 433 line = lineiter.next()
434 434 if line['action'] not in ['unmod', 'context']:
435 435 nextline = lineiter.next()
436 436 if nextline['action'] in ['unmod', 'context'] or \
437 437 nextline['action'] == line['action']:
438 438 continue
439 439 self.differ(line, nextline)
440 440 except StopIteration:
441 441 pass
442 442
443 443 return diff_container(sorted(_files, key=sorter))
444 444
445 445 def _parse_udiff(self, inline_diff=True):
446 446 raise NotImplementedError()
447 447
448 448 def _parse_lines(self, diff):
449 449 """
450 450 Parse the diff an return data for the template.
451 451 """
452 452
453 453 lineiter = iter(diff)
454 454 stats = [0, 0]
455 455
456 456 try:
457 457 chunks = []
458 458 line = lineiter.next()
459 459
460 460 while line:
461 461 lines = []
462 462 chunks.append(lines)
463 463
464 464 match = self._chunk_re.match(line)
465 465
466 466 if not match:
467 467 break
468 468
469 469 gr = match.groups()
470 470 (old_line, old_end,
471 471 new_line, new_end) = [int(x or 1) for x in gr[:-1]]
472 472 old_line -= 1
473 473 new_line -= 1
474 474
475 475 context = len(gr) == 5
476 476 old_end += old_line
477 477 new_end += new_line
478 478
479 479 if context:
480 480 # skip context only if it's first line
481 481 if int(gr[0]) > 1:
482 482 lines.append({
483 483 'old_lineno': '...',
484 484 'new_lineno': '...',
485 485 'action': 'context',
486 486 'line': line,
487 487 })
488 488
489 489 line = lineiter.next()
490 490
491 491 while old_line < old_end or new_line < new_end:
492 492 if line:
493 493 command = line[0]
494 494 if command in ['+', '-', ' ']:
495 495 #only modify the line if it's actually a diff
496 496 # thing
497 497 line = line[1:]
498 498 else:
499 499 command = ' '
500 500
501 501 affects_old = affects_new = False
502 502
503 503 # ignore those if we don't expect them
504 504 if command in '#@':
505 505 continue
506 506 elif command == '+':
507 507 affects_new = True
508 508 action = 'add'
509 509 stats[0] += 1
510 510 elif command == '-':
511 511 affects_old = True
512 512 action = 'del'
513 513 stats[1] += 1
514 514 else:
515 515 affects_old = affects_new = True
516 516 action = 'unmod'
517 517
518 518 if line != self._newline_marker:
519 519 old_line += affects_old
520 520 new_line += affects_new
521 521 lines.append({
522 522 'old_lineno': affects_old and old_line or '',
523 523 'new_lineno': affects_new and new_line or '',
524 524 'action': action,
525 525 'line': line
526 526 })
527 527
528 528 line = lineiter.next()
529 529
530 530 if line == self._newline_marker:
531 531 # we need to append to lines, since this is not
532 532 # counted in the line specs of diff
533 533 lines.append({
534 534 'old_lineno': '...',
535 535 'new_lineno': '...',
536 536 'action': 'context',
537 537 'line': line
538 538 })
539 539
540 540 except StopIteration:
541 541 pass
542 542 return chunks, stats
543 543
544 544 def _safe_id(self, idstring):
545 545 """Make a string safe for including in an id attribute.
546 546
547 547 The HTML spec says that id attributes 'must begin with
548 548 a letter ([A-Za-z]) and may be followed by any number
549 549 of letters, digits ([0-9]), hyphens ("-"), underscores
550 550 ("_"), colons (":"), and periods (".")'. These regexps
551 551 are slightly over-zealous, in that they remove colons
552 552 and periods unnecessarily.
553 553
554 554 Whitespace is transformed into underscores, and then
555 555 anything which is not a hyphen or a character that
556 556 matches \w (alphanumerics and underscore) is removed.
557 557
558 558 """
559 559 # Transform all whitespace to underscore
560 560 idstring = re.sub(r'\s', "_", '%s' % idstring)
561 561 # Remove everything that is not a hyphen or a member of \w
562 562 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
563 563 return idstring
564 564
565 565 def prepare(self, inline_diff=True):
566 566 """
567 567 Prepare the passed udiff for HTML rendering. It'l return a list
568 568 of dicts with diff information
569 569 """
570 570 parsed = self._parser(inline_diff=inline_diff)
571 571 self.parsed = True
572 572 self.parsed_diff = parsed
573 573 return parsed
574 574
575 575 def as_raw(self, diff_lines=None):
576 576 """
577 577 Returns raw string diff
578 578 """
579 579 return self._diff
580 580 #return u''.join(imap(self._line_counter, self._diff.splitlines(1)))
581 581
582 582 def as_html(self, table_class='code-difftable', line_class='line',
583 583 new_lineno_class='lineno old', old_lineno_class='lineno new',
584 584 code_class='code', enable_comments=False, parsed_lines=None):
585 585 """
586 586 Return given diff as html table with customized css classes
587 587 """
588 588 def _link_to_if(condition, label, url):
589 589 """
590 590 Generates a link if condition is meet or just the label if not.
591 591 """
592 592
593 593 if condition:
594 594 return '''<a href="%(url)s">%(label)s</a>''' % {
595 595 'url': url,
596 596 'label': label
597 597 }
598 598 else:
599 599 return label
600 600 if not self.parsed:
601 601 self.prepare()
602 602
603 603 diff_lines = self.parsed_diff
604 604 if parsed_lines:
605 605 diff_lines = parsed_lines
606 606
607 607 _html_empty = True
608 608 _html = []
609 609 _html.append('''<table class="%(table_class)s">\n''' % {
610 610 'table_class': table_class
611 611 })
612 612
613 613 for diff in diff_lines:
614 614 for line in diff['chunks']:
615 615 _html_empty = False
616 616 for change in line:
617 617 _html.append('''<tr class="%(lc)s %(action)s">\n''' % {
618 618 'lc': line_class,
619 619 'action': change['action']
620 620 })
621 621 anchor_old_id = ''
622 622 anchor_new_id = ''
623 623 anchor_old = "%(filename)s_o%(oldline_no)s" % {
624 624 'filename': self._safe_id(diff['filename']),
625 625 'oldline_no': change['old_lineno']
626 626 }
627 627 anchor_new = "%(filename)s_n%(oldline_no)s" % {
628 628 'filename': self._safe_id(diff['filename']),
629 629 'oldline_no': change['new_lineno']
630 630 }
631 631 cond_old = (change['old_lineno'] != '...' and
632 632 change['old_lineno'])
633 633 cond_new = (change['new_lineno'] != '...' and
634 634 change['new_lineno'])
635 635 if cond_old:
636 636 anchor_old_id = 'id="%s"' % anchor_old
637 637 if cond_new:
638 638 anchor_new_id = 'id="%s"' % anchor_new
639 639 ###########################################################
640 640 # OLD LINE NUMBER
641 641 ###########################################################
642 642 _html.append('''\t<td %(a_id)s class="%(olc)s">''' % {
643 643 'a_id': anchor_old_id,
644 644 'olc': old_lineno_class
645 645 })
646 646
647 647 _html.append('''%(link)s''' % {
648 648 'link': _link_to_if(True, change['old_lineno'],
649 649 '#%s' % anchor_old)
650 650 })
651 651 _html.append('''</td>\n''')
652 652 ###########################################################
653 653 # NEW LINE NUMBER
654 654 ###########################################################
655 655
656 656 _html.append('''\t<td %(a_id)s class="%(nlc)s">''' % {
657 657 'a_id': anchor_new_id,
658 658 'nlc': new_lineno_class
659 659 })
660 660
661 661 _html.append('''%(link)s''' % {
662 662 'link': _link_to_if(True, change['new_lineno'],
663 663 '#%s' % anchor_new)
664 664 })
665 665 _html.append('''</td>\n''')
666 666 ###########################################################
667 667 # CODE
668 668 ###########################################################
669 669 comments = '' if enable_comments else 'no-comment'
670 670 _html.append('''\t<td class="%(cc)s %(inc)s">''' % {
671 671 'cc': code_class,
672 672 'inc': comments
673 673 })
674 674 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' % {
675 675 'code': change['line']
676 676 })
677 677
678 678 _html.append('''\t</td>''')
679 679 _html.append('''\n</tr>\n''')
680 680 _html.append('''</table>''')
681 681 if _html_empty:
682 682 return None
683 683 return ''.join(_html)
684 684
685 685 def stat(self):
686 686 """
687 687 Returns tuple of added, and removed lines for this instance
688 688 """
689 689 return self.adds, self.removes
690 690
691 691
692 692 class InMemoryBundleRepo(bundlerepository):
693 693 def __init__(self, ui, path, bundlestream):
694 694 self._tempparent = None
695 695 localrepo.localrepository.__init__(self, ui, path)
696 696 self.ui.setconfig('phases', 'publish', False)
697 697
698 698 self.bundle = bundlestream
699 699
700 700 # dict with the mapping 'filename' -> position in the bundle
701 701 self.bundlefilespos = {}
702 702
703 703
704 704 def differ(org_repo, org_ref, other_repo, other_ref, discovery_data=None,
705 bundle_compare=False, context=3, ignore_whitespace=False):
705 remote_compare=False, context=3, ignore_whitespace=False):
706 706 """
707 General differ between branches, bookmarks, revisions of two remote related
708 repositories
707 General differ between branches, bookmarks, revisions of two remote or
708 local but related repositories
709 709
710 710 :param org_repo:
711 :type org_repo:
712 711 :param org_ref:
713 :type org_ref:
714 712 :param other_repo:
715 713 :type other_repo:
716 :param other_ref:
717 714 :type other_ref:
718 715 """
719 716
720 bundlerepo = None
721 ignore_whitespace = ignore_whitespace
722 context = context
723 717 org_repo_scm = org_repo.scm_instance
718 other_repo_scm = other_repo.scm_instance
719
724 720 org_repo = org_repo_scm._repo
725 other_repo = other_repo.scm_instance._repo
726 opts = diffopts(git=True, ignorews=ignore_whitespace, context=context)
721 other_repo = other_repo_scm._repo
722
727 723 org_ref = org_ref[1]
728 724 other_ref = other_ref[1]
729 725
730 726 if org_repo == other_repo:
731 727 log.debug('running diff between %s@%s and %s@%s'
732 728 % (org_repo, org_ref, other_repo, other_ref))
733 _diff = org_repo_scm.get_diff(rev1=other_ref, rev2=org_ref,
729 _diff = org_repo_scm.get_diff(rev1=org_ref, rev2=other_ref,
734 730 ignore_whitespace=ignore_whitespace, context=context)
735 731 return _diff
736 732
737 elif bundle_compare:
738
733 elif remote_compare:
734 opts = diffopts(git=True, ignorews=ignore_whitespace, context=context)
739 735 common, incoming, rheads = discovery_data
740 other_repo_peer = localrepo.locallegacypeer(other_repo.local())
736 org_repo_peer = localrepo.locallegacypeer(org_repo.local())
741 737 # create a bundle (uncompressed if other repo is not local)
742 if other_repo_peer.capable('getbundle') and incoming:
738 if org_repo_peer.capable('getbundle'):
743 739 # disable repo hooks here since it's just bundle !
744 740 # patch and reset hooks section of UI config to not run any
745 741 # hooks on fetching archives with subrepos
746 for k, _ in other_repo.ui.configitems('hooks'):
747 other_repo.ui.setconfig('hooks', k, None)
742 for k, _ in org_repo.ui.configitems('hooks'):
743 org_repo.ui.setconfig('hooks', k, None)
748 744
749 unbundle = other_repo.getbundle('incoming', common=common,
750 heads=None)
745 unbundle = org_repo.getbundle('incoming', common=common,
746 heads=None)
751 747
752 748 buf = BytesIO()
753 749 while True:
754 750 chunk = unbundle._stream.read(1024 * 4)
755 751 if not chunk:
756 752 break
757 753 buf.write(chunk)
758 754
759 755 buf.seek(0)
760 756 # replace chunked _stream with data that can do tell() and seek()
761 757 unbundle._stream = buf
762 758
763 759 ui = make_ui('db')
764 760 bundlerepo = InMemoryBundleRepo(ui, path=org_repo.root,
765 761 bundlestream=unbundle)
766 762
767 return ''.join(patch.diff(bundlerepo or org_repo,
768 node1=org_repo[org_ref].node(),
769 node2=other_repo[other_ref].node(),
770 opts=opts))
763 return ''.join(patch.diff(bundlerepo,
764 node1=other_repo[other_ref].node(),
765 node2=org_repo[org_ref].node(),
766 opts=opts))
771 767
768 return '' No newline at end of file
@@ -1,277 +1,277
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.model.pull_request
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 pull request model for RhodeCode
7 7
8 8 :created_on: Jun 6, 2012
9 9 :author: marcink
10 10 :copyright: (C) 2012-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import logging
27 27 import binascii
28 28 import datetime
29 29 import re
30 30
31 31 from pylons.i18n.translation import _
32 32
33 33 from rhodecode.model.meta import Session
34 34 from rhodecode.lib import helpers as h
35 35 from rhodecode.model import BaseModel
36 36 from rhodecode.model.db import PullRequest, PullRequestReviewers, Notification
37 37 from rhodecode.model.notification import NotificationModel
38 38 from rhodecode.lib.utils2 import safe_unicode
39 39
40 40 from rhodecode.lib.vcs.utils.hgcompat import discovery, localrepo, scmutil, \
41 41 findcommonoutgoing
42 42
43 43 log = logging.getLogger(__name__)
44 44
45 45
46 46 class PullRequestModel(BaseModel):
47 47
48 48 cls = PullRequest
49 49
50 50 def __get_pull_request(self, pull_request):
51 51 return self._get_instance(PullRequest, pull_request)
52 52
53 53 def get_all(self, repo):
54 54 repo = self._get_repo(repo)
55 55 return PullRequest.query().filter(PullRequest.other_repo == repo).all()
56 56
57 57 def create(self, created_by, org_repo, org_ref, other_repo,
58 58 other_ref, revisions, reviewers, title, description=None):
59 59
60 60 created_by_user = self._get_user(created_by)
61 61 org_repo = self._get_repo(org_repo)
62 62 other_repo = self._get_repo(other_repo)
63 63
64 64 new = PullRequest()
65 65 new.org_repo = org_repo
66 66 new.org_ref = org_ref
67 67 new.other_repo = other_repo
68 68 new.other_ref = other_ref
69 69 new.revisions = revisions
70 70 new.title = title
71 71 new.description = description
72 72 new.author = created_by_user
73 73 self.sa.add(new)
74 74 Session().flush()
75 75 #members
76 76 for member in reviewers:
77 77 _usr = self._get_user(member)
78 78 reviewer = PullRequestReviewers(_usr, new)
79 79 self.sa.add(reviewer)
80 80
81 81 #notification to reviewers
82 82 notif = NotificationModel()
83 83
84 84 pr_url = h.url('pullrequest_show', repo_name=other_repo.repo_name,
85 85 pull_request_id=new.pull_request_id,
86 86 qualified=True,
87 87 )
88 88 subject = safe_unicode(
89 89 h.link_to(
90 90 _('%(user)s wants you to review pull request #%(pr_id)s') % \
91 91 {'user': created_by_user.username,
92 92 'pr_id': new.pull_request_id},
93 93 pr_url
94 94 )
95 95 )
96 96 body = description
97 97 kwargs = {
98 98 'pr_title': title,
99 99 'pr_user_created': h.person(created_by_user.email),
100 100 'pr_repo_url': h.url('summary_home', repo_name=other_repo.repo_name,
101 101 qualified=True,),
102 102 'pr_url': pr_url,
103 103 'pr_revisions': revisions
104 104 }
105 105 notif.create(created_by=created_by_user, subject=subject, body=body,
106 106 recipients=reviewers,
107 107 type_=Notification.TYPE_PULL_REQUEST, email_kwargs=kwargs)
108 108 return new
109 109
110 110 def update_reviewers(self, pull_request, reviewers_ids):
111 111 reviewers_ids = set(reviewers_ids)
112 112 pull_request = self.__get_pull_request(pull_request)
113 113 current_reviewers = PullRequestReviewers.query()\
114 114 .filter(PullRequestReviewers.pull_request==
115 115 pull_request)\
116 116 .all()
117 117 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
118 118
119 119 to_add = reviewers_ids.difference(current_reviewers_ids)
120 120 to_remove = current_reviewers_ids.difference(reviewers_ids)
121 121
122 122 log.debug("Adding %s reviewers" % to_add)
123 123 log.debug("Removing %s reviewers" % to_remove)
124 124
125 125 for uid in to_add:
126 126 _usr = self._get_user(uid)
127 127 reviewer = PullRequestReviewers(_usr, pull_request)
128 128 self.sa.add(reviewer)
129 129
130 130 for uid in to_remove:
131 131 reviewer = PullRequestReviewers.query()\
132 132 .filter(PullRequestReviewers.user_id==uid,
133 133 PullRequestReviewers.pull_request==pull_request)\
134 134 .scalar()
135 135 if reviewer:
136 136 self.sa.delete(reviewer)
137 137
138 138 def delete(self, pull_request):
139 139 pull_request = self.__get_pull_request(pull_request)
140 140 Session().delete(pull_request)
141 141
142 142 def close_pull_request(self, pull_request):
143 143 pull_request = self.__get_pull_request(pull_request)
144 144 pull_request.status = PullRequest.STATUS_CLOSED
145 145 pull_request.updated_on = datetime.datetime.now()
146 146 self.sa.add(pull_request)
147 147
148 148 def _get_changesets(self, alias, org_repo, org_ref, other_repo, other_ref,
149 149 discovery_data):
150 150 """
151 151 Returns a list of changesets that are incoming from org_repo@org_ref
152 152 to other_repo@other_ref
153 153
154 154 :param org_repo:
155 155 :type org_repo:
156 156 :param org_ref:
157 157 :type org_ref:
158 158 :param other_repo:
159 159 :type other_repo:
160 160 :param other_ref:
161 161 :type other_ref:
162 162 :param tmp:
163 163 :type tmp:
164 164 """
165 165 changesets = []
166 166 #case two independent repos
167 167 common, incoming, rheads = discovery_data
168 168 if org_repo != other_repo and incoming:
169 169 obj = findcommonoutgoing(org_repo._repo,
170 170 localrepo.locallegacypeer(other_repo._repo.local()),
171 171 force=True)
172 172 revs = obj.missing
173 173
174 174 for cs in reversed(map(binascii.hexlify, revs)):
175 175 changesets.append(org_repo.get_changeset(cs))
176 176 else:
177 177 #no remote compare do it on the same repository
178 178 if alias == 'hg':
179 179 _revset_predicates = {
180 180 'branch': 'branch',
181 181 'book': 'bookmark',
182 182 'tag': 'tag',
183 183 'rev': 'id',
184 184 }
185 185
186 186 revs = [
187 187 "ancestors(%s('%s')) and not ancestors(%s('%s'))" % (
188 _revset_predicates[other_ref[0]], other_ref[1],
188 189 _revset_predicates[org_ref[0]], org_ref[1],
189 _revset_predicates[other_ref[0]], other_ref[1]
190 190 )
191 191 ]
192 192
193 193 out = scmutil.revrange(org_repo._repo, revs)
194 for cs in reversed(out):
194 for cs in (out):
195 195 changesets.append(org_repo.get_changeset(cs))
196 196 elif alias == 'git':
197 197 so, se = org_repo.run_git_command(
198 'log --pretty="format: %%H" -s -p %s..%s' % (org_ref[1],
198 'log --reverse --pretty="format: %%H" -s -p %s..%s' % (org_ref[1],
199 199 other_ref[1])
200 200 )
201 201 ids = re.findall(r'[0-9a-fA-F]{40}', so)
202 for cs in reversed(ids):
202 for cs in (ids):
203 203 changesets.append(org_repo.get_changeset(cs))
204 204
205 205 return changesets
206 206
207 207 def _get_discovery(self, org_repo, org_ref, other_repo, other_ref):
208 208 """
209 209 Get's mercurial discovery data used to calculate difference between
210 210 repos and refs
211 211
212 212 :param org_repo:
213 213 :type org_repo:
214 214 :param org_ref:
215 215 :type org_ref:
216 216 :param other_repo:
217 217 :type other_repo:
218 218 :param other_ref:
219 219 :type other_ref:
220 220 """
221 221
222 222 _org_repo = org_repo._repo
223 223 org_rev_type, org_rev = org_ref
224 224
225 225 _other_repo = other_repo._repo
226 226 other_rev_type, other_rev = other_ref
227 227
228 228 log.debug('Doing discovery for %s@%s vs %s@%s' % (
229 229 org_repo, org_ref, other_repo, other_ref)
230 230 )
231 231
232 232 #log.debug('Filter heads are %s[%s]' % ('', org_ref[1]))
233 233 org_peer = localrepo.locallegacypeer(_org_repo.local())
234 234 tmp = discovery.findcommonincoming(
235 235 repo=_other_repo, # other_repo we check for incoming
236 236 remote=org_peer, # org_repo source for incoming
237 237 heads=[_other_repo[other_rev].node(),
238 238 _org_repo[org_rev].node()],
239 239 force=True
240 240 )
241 241 return tmp
242 242
243 243 def get_compare_data(self, org_repo, org_ref, other_repo, other_ref):
244 244 """
245 245 Returns a tuple of incomming changesets, and discoverydata cache for
246 246 mercurial repositories
247 247
248 248 :param org_repo:
249 249 :type org_repo:
250 250 :param org_ref:
251 251 :type org_ref:
252 252 :param other_repo:
253 253 :type other_repo:
254 254 :param other_ref:
255 255 :type other_ref:
256 256 """
257 257
258 258 if len(org_ref) != 2 or not isinstance(org_ref, (list, tuple)):
259 259 raise Exception('org_ref must be a two element list/tuple')
260 260
261 261 if len(other_ref) != 2 or not isinstance(org_ref, (list, tuple)):
262 262 raise Exception('other_ref must be a two element list/tuple')
263 263
264 264 org_repo_scm = org_repo.scm_instance
265 265 other_repo_scm = other_repo.scm_instance
266 266
267 267 alias = org_repo.scm_instance.alias
268 268 discovery_data = [None, None, None]
269 269 if alias == 'hg':
270 270 discovery_data = self._get_discovery(org_repo_scm, org_ref,
271 271 other_repo_scm, other_ref)
272 272 cs_ranges = self._get_changesets(alias,
273 273 org_repo_scm, org_ref,
274 274 other_repo_scm, other_ref,
275 275 discovery_data)
276 276
277 277 return cs_ranges, discovery_data
@@ -1,122 +1,122
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="/base/base.html"/>
3 3
4 4 <%def name="title()">
5 5 ${_('%s Changesets') % c.repo_name} - r${c.cs_ranges[0].revision}:${h.short_id(c.cs_ranges[0].raw_id)} -> r${c.cs_ranges[-1].revision}:${h.short_id(c.cs_ranges[-1].raw_id)} - ${c.rhodecode_name}
6 6 </%def>
7 7
8 8 <%def name="breadcrumbs_links()">
9 9 ${h.link_to(_(u'Home'),h.url('/'))}
10 10 &raquo;
11 11 ${h.link_to(c.repo_name,h.url('summary_home',repo_name=c.repo_name))}
12 12 &raquo;
13 13 ${_('Changesets')} - r${c.cs_ranges[0].revision}:${h.short_id(c.cs_ranges[0].raw_id)} -> r${c.cs_ranges[-1].revision}:${h.short_id(c.cs_ranges[-1].raw_id)}
14 14 </%def>
15 15
16 16 <%def name="page_nav()">
17 17 ${self.menu('changelog')}
18 18 </%def>
19 19
20 20 <%def name="main()">
21 21 <div class="box">
22 22 <!-- box / title -->
23 23 <div class="title">
24 24 ${self.breadcrumbs()}
25 25 </div>
26 26 <div class="table">
27 27 <div id="body" class="diffblock">
28 28 <div class="code-header cv">
29 <h3 class="code-header-title">${_('Compare View')}</h3>
29 <h3 class="code-header-title">${_('Compare View')} / ${h.link_to(_('Show combined compare'),h.url('compare_url',repo_name=c.repo_name,org_ref_type='rev',org_ref=getattr(c.cs_ranges[0].parents[0] if c.cs_ranges[0].parents else h.EmptyChangeset(),'raw_id'),other_ref_type='rev',other_ref=c.cs_ranges[-1].raw_id))}</h3>
30 30 <div>
31 31 ${_('Changesets')} - r${c.cs_ranges[0].revision}:${h.short_id(c.cs_ranges[0].raw_id)} -> r${c.cs_ranges[-1].revision}:${h.short_id(c.cs_ranges[-1].raw_id)}
32 32 </div>
33 33 </div>
34 34 </div>
35 35 <div id="changeset_compare_view_content">
36 36 <div class="container">
37 37 <table class="compare_view_commits noborder">
38 38 %for cnt,cs in enumerate(c.cs_ranges):
39 39 <tr>
40 40 <td><div class="gravatar"><img alt="gravatar" src="${h.gravatar_url(h.email_or_none(cs.author),14)}"/></div></td>
41 41 <td>${h.link_to('r%s:%s' % (cs.revision,h.short_id(cs.raw_id)),h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id))}</td>
42 42 <td><div class="author">${h.person(cs.author)}</div></td>
43 43 <td><span class="tooltip" title="${h.age(cs.date)}">${cs.date}</span></td>
44 44 <td>
45 45 %if c.statuses:
46 46 <div title="${h.tooltip(_('Changeset status'))}" class="changeset-status-ico"><img src="${h.url('/images/icons/flag_status_%s.png' % c.statuses[cnt])}" /></div>
47 47 %endif
48 48 </td>
49 49 <td><div class="message">${h.urlify_commit(h.wrap_paragraphs(cs.message),c.repo_name)}</div></td>
50 50 </tr>
51 51 %endfor
52 52 </table>
53 53 </div>
54 54 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">${_('Files affected')}</div>
55 55 <div class="cs_files">
56 56 %for cs in c.cs_ranges:
57 57 <div class="cur_cs">${h.link_to('r%s:%s' % (cs.revision,h.short_id(cs.raw_id)),h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id))}</div>
58 58 %for FID, (cs1, cs2, change, path, diff, stats) in c.changes[cs.raw_id].iteritems():
59 59 <div class="cs_${change}">${h.link_to(h.safe_unicode(path),h.url.current(anchor=FID))}</div>
60 60 %endfor
61 61 %endfor
62 62 </div>
63 63 </div>
64 64
65 65 </div>
66 66 <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
67 67 <%namespace name="diff_block" file="/changeset/diff_block.html"/>
68 68 %for cs in c.cs_ranges:
69 69 ##${comment.comment_inline_form(cs)}
70 70 ## diff block
71 71 <div class="h3">
72 72 <a class="tooltip" title="${h.tooltip(cs.message)}" href="${h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id)}">${'r%s:%s' % (cs.revision,h.short_id(cs.raw_id))}</a>
73 73 <div class="gravatar">
74 74 <img alt="gravatar" src="${h.gravatar_url(h.email_or_none(cs.author),20)}"/>
75 75 </div>
76 76 <div class="right">
77 77 <span class="logtags">
78 78 %if len(cs.parents)>1:
79 79 <span class="merge">${_('merge')}</span>
80 80 %endif
81 81 %if cs.branch:
82 82 <span class="branchtag" title="${'%s %s' % (_('branch'),cs.branch)}">
83 83 ${h.link_to(h.shorter(cs.branch),h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id))}
84 84 </span>
85 85 %endif
86 86 %if h.is_hg(c.rhodecode_repo):
87 87 %for book in cs.bookmarks:
88 88 <span class="bookbook" title="${'%s %s' % (_('bookmark'),book)}">
89 89 ${h.link_to(h.shorter(book),h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id))}
90 90 </span>
91 91 %endfor
92 92 %endif
93 93 %for tag in cs.tags:
94 94 <span class="tagtag" title="${'%s %s' % (_('tag'),tag)}">
95 95 ${h.link_to(h.shorter(tag),h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id))}</span>
96 96 %endfor
97 97 </span>
98 98 </div>
99 99 </div>
100 100 ${diff_block.diff_block(c.changes[cs.raw_id])}
101 101 ##${comment.comments(cs)}
102 102
103 103 %endfor
104 104 <script type="text/javascript">
105 105
106 106 YUE.onDOMReady(function(){
107 107
108 108 YUE.on(YUQ('.diff-menu-activate'),'click',function(e){
109 109 var act = e.currentTarget.nextElementSibling;
110 110
111 111 if(YUD.hasClass(act,'active')){
112 112 YUD.removeClass(act,'active');
113 113 YUD.setStyle(act,'display','none');
114 114 }else{
115 115 YUD.addClass(act,'active');
116 116 YUD.setStyle(act,'display','');
117 117 }
118 118 });
119 119 })
120 120 </script>
121 121 </div>
122 122 </%def>
@@ -1,28 +1,28
1 1 ## Changesets table !
2 2 <div class="container">
3 3 <table class="compare_view_commits noborder">
4 4 %if not c.cs_ranges:
5 <tr><td>${_('No changesets')}</td></tr>
5 <span class="empty_data">${_('No changesets')}</span>
6 6 %else:
7 7 %for cnt, cs in enumerate(c.cs_ranges):
8 8 <tr>
9 9 <td><div class="gravatar"><img alt="gravatar" src="${h.gravatar_url(h.email_or_none(cs.author),14)}"/></div></td>
10 10 <td>
11 11 %if cs.raw_id in c.statuses:
12 12 <div title="${c.statuses[cs.raw_id][1]}" class="changeset-status-ico"><img src="${h.url('/images/icons/flag_status_%s.png' % c.statuses[cs.raw_id][0])}" /></div>
13 13 %endif
14 14 </td>
15 15 <td>${h.link_to('r%s:%s' % (cs.revision,h.short_id(cs.raw_id)),h.url('changeset_home',repo_name=c.target_repo,revision=cs.raw_id))}
16 16 %if c.as_form:
17 17 ${h.hidden('revisions',cs.raw_id)}
18 18 %endif
19 19 </td>
20 20 <td><div class="author">${h.person(cs.author)}</div></td>
21 21 <td><span class="tooltip" title="${h.tooltip(h.age(cs.date))}">${cs.date}</span></td>
22 22 <td><div class="message tooltip" title="${h.tooltip(cs.message)}" style="white-space:normal">${h.urlify_commit(h.shorter(cs.message, 60),c.repo_name)}</div></td>
23 23 </tr>
24 24 %endfor
25 25
26 26 %endif
27 27 </table>
28 28 </div>
@@ -1,82 +1,93
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="/base/base.html"/>
3 3
4 4 <%def name="title()">
5 5 ${c.repo_name} ${_('Compare')} ${'%s@%s' % (c.org_repo.repo_name, c.org_ref)} -> ${'%s@%s' % (c.other_repo.repo_name, c.other_ref)}
6 6 </%def>
7 7
8 8 <%def name="breadcrumbs_links()">
9 9 ${h.link_to(_(u'Home'),h.url('/'))}
10 10 &raquo;
11 11 ${h.link_to(c.repo_name,h.url('summary_home',repo_name=c.repo_name))}
12 12 &raquo;
13 13 ${_('Compare')}
14 14 </%def>
15 15
16 16 <%def name="page_nav()">
17 17 ${self.menu('changelog')}
18 18 </%def>
19 19
20 20 <%def name="main()">
21 21 <div class="box">
22 22 <!-- box / title -->
23 23 <div class="title">
24 24 ${self.breadcrumbs()}
25 25 </div>
26 26 <div class="table">
27 27 <div id="body" class="diffblock">
28 28 <div class="code-header cv">
29 29 <h3 class="code-header-title">${_('Compare View')}</h3>
30 30 <div>
31 31 ${'%s@%s' % (c.org_repo.repo_name, c.org_ref)} -> ${'%s@%s' % (c.other_repo.repo_name, c.other_ref)} <a href="${c.swap_url}">[swap]</a>
32 32 </div>
33 33 </div>
34 34 </div>
35 35 <div id="changeset_compare_view_content">
36 36 ##CS
37 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">${_('Outgoing changesets')}</div>
37 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">${ungettext('Showing %s commit','Showing %s commits', len(c.cs_ranges)) % len(c.cs_ranges)}</div>
38 38 <%include file="compare_cs.html" />
39 39
40 40 ## FILES
41 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">${_('Files affected')}</div>
41 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">
42
43 % if c.limited_diff:
44 ${ungettext('%s file changed', '%s files changed', len(c.files)) % len(c.files)}
45 % else:
46 ${ungettext('%s file changed with %s insertions and %s deletions','%s files changed with %s insertions and %s deletions', len(c.files)) % (len(c.files),c.lines_added,c.lines_deleted)}:
47 %endif
48
49 </div>
42 50 <div class="cs_files">
51 %if not c.files:
52 <span class="empty_data">${_('No files')}</span>
53 %endif
43 54 %for fid, change, f, stat in c.files:
44 55 <div class="cs_${change}">
45 56 <div class="node">${h.link_to(h.safe_unicode(f),h.url.current(anchor=fid))}</div>
46 57 <div class="changes">${h.fancy_file_stats(stat)}</div>
47 58 </div>
48 59 %endfor
49 60 </div>
50 61 % if c.limited_diff:
51 62 <h5>${_('Changeset was too big and was cut off...')}</h5>
52 63 % endif
53 64 </div>
54 65 </div>
55 66
56 67 ## diff block
57 68 <%namespace name="diff_block" file="/changeset/diff_block.html"/>
58 69 %for fid, change, f, stat in c.files:
59 70 ${diff_block.diff_block_simple([c.changes[fid]])}
60 71 %endfor
61 72 % if c.limited_diff:
62 73 <h4>${_('Changeset was too big and was cut off...')}</h4>
63 74 % endif
64 75 <script type="text/javascript">
65 76
66 77 YUE.onDOMReady(function(){
67 78
68 79 YUE.on(YUQ('.diff-menu-activate'),'click',function(e){
69 80 var act = e.currentTarget.nextElementSibling;
70 81
71 82 if(YUD.hasClass(act,'active')){
72 83 YUD.removeClass(act,'active');
73 84 YUD.setStyle(act,'display','none');
74 85 }else{
75 86 YUD.addClass(act,'active');
76 87 YUD.setStyle(act,'display','');
77 88 }
78 89 });
79 90 })
80 91 </script>
81 92 </div>
82 93 </%def>
@@ -1,403 +1,563
1 1 from rhodecode.tests import *
2 2 from rhodecode.model.repo import RepoModel
3 3 from rhodecode.model.meta import Session
4 4 from rhodecode.model.db import Repository
5 5 from rhodecode.model.scm import ScmModel
6 6 from rhodecode.lib.vcs.backends.base import EmptyChangeset
7 7
8 8
9 9 class TestCompareController(TestController):
10 10
11 def test_index_tag(self):
11 def test_compare_tag_hg(self):
12 12 self.log_user()
13 tag1 = '0.1.3'
14 tag2 = '0.1.2'
13 tag1 = '0.1.2'
14 tag2 = '0.1.3'
15 15 response = self.app.get(url(controller='compare', action='index',
16 16 repo_name=HG_REPO,
17 17 org_ref_type="tag",
18 18 org_ref=tag1,
19 19 other_ref_type="tag",
20 20 other_ref=tag2,
21 21 ))
22 22 response.mustcontain('%s@%s -> %s@%s' % (HG_REPO, tag1, HG_REPO, tag2))
23 23 ## outgoing changesets between tags
24 response.mustcontain('''<a href="/%s/changeset/17544fbfcd33ffb439e2b728b5d526b1ef30bfcf">r120:17544fbfcd33</a>''' % HG_REPO)
25 response.mustcontain('''<a href="/%s/changeset/36e0fc9d2808c5022a24f49d6658330383ed8666">r119:36e0fc9d2808</a>''' % HG_REPO)
26 response.mustcontain('''<a href="/%s/changeset/bb1a3ab98cc45cb934a77dcabf87a5a598b59e97">r118:bb1a3ab98cc4</a>''' % HG_REPO)
24 response.mustcontain('''<a href="/%s/changeset/c5ddebc06eaaba3010c2d66ea6ec9d074eb0f678">r112:c5ddebc06eaa</a>''' % HG_REPO)
25 response.mustcontain('''<a href="/%s/changeset/70d4cef8a37657ee4cf5aabb3bd9f68879769816">r115:70d4cef8a376</a>''' % HG_REPO)
26 response.mustcontain('''<a href="/%s/changeset/9749bfbfc0d2eba208d7947de266303b67c87cda">r116:9749bfbfc0d2</a>''' % HG_REPO)
27 27 response.mustcontain('''<a href="/%s/changeset/41fda979f02fda216374bf8edac4e83f69e7581c">r117:41fda979f02f</a>''' % HG_REPO)
28 response.mustcontain('''<a href="/%s/changeset/9749bfbfc0d2eba208d7947de266303b67c87cda">r116:9749bfbfc0d2</a>''' % HG_REPO)
29 response.mustcontain('''<a href="/%s/changeset/70d4cef8a37657ee4cf5aabb3bd9f68879769816">r115:70d4cef8a376</a>''' % HG_REPO)
30 response.mustcontain('''<a href="/%s/changeset/c5ddebc06eaaba3010c2d66ea6ec9d074eb0f678">r112:c5ddebc06eaa</a>''' % HG_REPO)
28 response.mustcontain('''<a href="/%s/changeset/bb1a3ab98cc45cb934a77dcabf87a5a598b59e97">r118:bb1a3ab98cc4</a>''' % HG_REPO)
29 response.mustcontain('''<a href="/%s/changeset/36e0fc9d2808c5022a24f49d6658330383ed8666">r119:36e0fc9d2808</a>''' % HG_REPO)
30 response.mustcontain('''<a href="/%s/changeset/17544fbfcd33ffb439e2b728b5d526b1ef30bfcf">r120:17544fbfcd33</a>''' % HG_REPO)
31 31
32 32 ## files diff
33 33 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))
34 34 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))
35 35 response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--c8e92ef85cd1">.hgignore</a></div>''' % (HG_REPO, tag1, tag2))
36 36 response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--6e08b694d687">.hgtags</a></div>''' % (HG_REPO, tag1, tag2))
37 37 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))
38 38 response.mustcontain('''<div class="node"><a href="/%s/compare/tag@%s...tag@%s#C--430ccbc82bdf">vcs/__init__.py</a></div>''' % (HG_REPO, tag1, tag2))
39 39 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))
40 40 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))
41 41 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))
42 42 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))
43 43 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))
44 44
45 def test_index_branch(self):
45 def test_compare_tag_git(self):
46 self.log_user()
47 tag1 = 'v0.1.2'
48 tag2 = 'v0.1.3'
49 response = self.app.get(url(controller='compare', action='index',
50 repo_name=GIT_REPO,
51 org_ref_type="tag",
52 org_ref=tag1,
53 other_ref_type="tag",
54 other_ref=tag2,
55 bundle=False
56 ))
57 response.mustcontain('%s@%s -> %s@%s' % (GIT_REPO, tag1, GIT_REPO, tag2))
58
59 ## outgoing changesets between tags
60 response.mustcontain('''<a href="/%s/changeset/794bbdd31545c199f74912709ea350dedcd189a2">r113:794bbdd31545</a>''' % GIT_REPO)
61 response.mustcontain('''<a href="/%s/changeset/e36d8c5025329bdd4212bd53d4ed8a70ff44985f">r115:e36d8c502532</a>''' % GIT_REPO)
62 response.mustcontain('''<a href="/%s/changeset/5c9ff4f6d7508db0e72b1d2991c357d0d8e07af2">r116:5c9ff4f6d750</a>''' % GIT_REPO)
63 response.mustcontain('''<a href="/%s/changeset/b7187fa2b8c1d773ec35e9dee12f01f74808c879">r117:b7187fa2b8c1</a>''' % GIT_REPO)
64 response.mustcontain('''<a href="/%s/changeset/5f3b74262014a8de2dc7dade1152de9fd0c8efef">r118:5f3b74262014</a>''' % GIT_REPO)
65 response.mustcontain('''<a href="/%s/changeset/17438a11f72b93f56d0e08e7d1fa79a378578a82">r119:17438a11f72b</a>''' % GIT_REPO)
66 response.mustcontain('''<a href="/%s/changeset/5a3a8fb005554692b16e21dee62bf02667d8dc3e">r120:5a3a8fb00555</a>''' % GIT_REPO)
67
68 #files
69 response.mustcontain('''<a href="/%s/compare/tag@%s...tag@%s#C--1c5cf9e91c12">docs/api/utils/index.rst</a>''' % (GIT_REPO, tag1, tag2))
70 response.mustcontain('''<a href="/%s/compare/tag@%s...tag@%s#C--e3305437df55">test_and_report.sh</a>''' % (GIT_REPO, tag1, tag2))
71 response.mustcontain('''<a href="/%s/compare/tag@%s...tag@%s#C--c8e92ef85cd1">.hgignore</a>''' % (GIT_REPO, tag1, tag2))
72 response.mustcontain('''<a href="/%s/compare/tag@%s...tag@%s#C--6e08b694d687">.hgtags</a>''' % (GIT_REPO, tag1, tag2))
73 response.mustcontain('''<a href="/%s/compare/tag@%s...tag@%s#C--2c14b00f3393">docs/api/index.rst</a>''' % (GIT_REPO, tag1, tag2))
74 response.mustcontain('''<a href="/%s/compare/tag@%s...tag@%s#C--430ccbc82bdf">vcs/__init__.py</a>''' % (GIT_REPO, tag1, tag2))
75 response.mustcontain('''<a href="/%s/compare/tag@%s...tag@%s#C--9c390eb52cd6">vcs/backends/hg.py</a>''' % (GIT_REPO, tag1, tag2))
76 response.mustcontain('''<a href="/%s/compare/tag@%s...tag@%s#C--ebb592c595c0">vcs/utils/__init__.py</a>''' % (GIT_REPO, tag1, tag2))
77 response.mustcontain('''<a href="/%s/compare/tag@%s...tag@%s#C--7abc741b5052">vcs/utils/annotate.py</a>''' % (GIT_REPO, tag1, tag2))
78 response.mustcontain('''<a href="/%s/compare/tag@%s...tag@%s#C--2ef0ef106c56">vcs/utils/diffs.py</a>''' % (GIT_REPO, tag1, tag2))
79 response.mustcontain('''<a href="/%s/compare/tag@%s...tag@%s#C--3150cb87d4b7">vcs/utils/lazy.py</a>''' % (GIT_REPO, tag1, tag2))
80
81 def test_index_branch_hg(self):
46 82 self.log_user()
47 83 response = self.app.get(url(controller='compare', action='index',
48 84 repo_name=HG_REPO,
49 85 org_ref_type="branch",
50 86 org_ref='default',
51 87 other_ref_type="branch",
52 88 other_ref='default',
53 89 ))
54 90
55 91 response.mustcontain('%s@default -> %s@default' % (HG_REPO, HG_REPO))
56 92 # branch are equal
57 response.mustcontain('<tr><td>No changesets</td></tr>')
93 response.mustcontain('<span class="empty_data">No files</span>')
94 response.mustcontain('<span class="empty_data">No changesets</span>')
95
96 def test_index_branch_git(self):
97 self.log_user()
98 response = self.app.get(url(controller='compare', action='index',
99 repo_name=GIT_REPO,
100 org_ref_type="branch",
101 org_ref='master',
102 other_ref_type="branch",
103 other_ref='master',
104 ))
105
106 response.mustcontain('%s@master -> %s@master' % (GIT_REPO, GIT_REPO))
107 # branch are equal
108 response.mustcontain('<span class="empty_data">No files</span>')
109 response.mustcontain('<span class="empty_data">No changesets</span>')
58 110
59 111 def test_compare_revisions(self):
60 112 self.log_user()
61 rev1 = '3d8f361e72ab'
62 rev2 = 'b986218ba1c9'
113 rev1 = 'b986218ba1c9'
114 rev2 = '3d8f361e72ab'
115
63 116 response = self.app.get(url(controller='compare', action='index',
64 117 repo_name=HG_REPO,
65 118 org_ref_type="rev",
66 119 org_ref=rev1,
67 120 other_ref_type="rev",
68 121 other_ref=rev2,
69 122 ))
70 123 response.mustcontain('%s@%s -> %s@%s' % (HG_REPO, rev1, HG_REPO, rev2))
71 124 ## outgoing changesets between those revisions
72 response.mustcontain("""<a href="/%s/changeset/3d8f361e72ab303da48d799ff1ac40d5ac37c67e">r1:%s</a>""" % (HG_REPO, rev1))
73
125 response.mustcontain("""<a href="/%s/changeset/3d8f361e72ab303da48d799ff1ac40d5ac37c67e">r1:%s</a>""" % (HG_REPO, rev2))
74 126 ## files
75 127 response.mustcontain("""<a href="/%s/compare/rev@%s...rev@%s#C--c8e92ef85cd1">.hgignore</a>""" % (HG_REPO, rev1, rev2))
76 128
77 129 def test_compare_remote_repos(self):
78 130 self.log_user()
79 131
80 132 form_data = dict(
81 133 repo_name=HG_FORK,
82 134 repo_name_full=HG_FORK,
83 135 repo_group=None,
84 136 repo_type='hg',
85 137 description='',
86 138 private=False,
87 139 copy_permissions=False,
88 140 landing_rev='tip',
89 141 update_after_clone=False,
90 142 fork_parent_id=Repository.get_by_repo_name(HG_REPO),
91 143 )
92 144 RepoModel().create_fork(form_data, cur_user=TEST_USER_ADMIN_LOGIN)
93 145
94 146 Session().commit()
95 147
96 rev1 = '7d4bc8ec6be5'
97 rev2 = '56349e29c2af'
148 rev1 = '56349e29c2af'
149 rev2 = '7d4bc8ec6be5'
150
151 response = self.app.get(url(controller='compare', action='index',
152 repo_name=HG_REPO,
153 org_ref_type="rev",
154 org_ref=rev1,
155 other_ref_type="rev",
156 other_ref=rev2,
157 repo=HG_FORK,
158 ))
159
160 try:
161 response.mustcontain('%s@%s -> %s@%s' % (HG_REPO, rev1, HG_FORK, rev2))
162 ## outgoing changesets between those revisions
163
164 response.mustcontain("""<a href="/%s/changeset/2dda4e345facb0ccff1a191052dd1606dba6781d">r4:2dda4e345fac</a>""" % (HG_REPO))
165 response.mustcontain("""<a href="/%s/changeset/6fff84722075f1607a30f436523403845f84cd9e">r5:6fff84722075</a>""" % (HG_REPO))
166 response.mustcontain("""<a href="/%s/changeset/7d4bc8ec6be56c0f10425afb40b6fc315a4c25e7">r6:%s</a>""" % (HG_REPO, rev2))
167
168 ## files
169 response.mustcontain("""<a href="/%s/compare/rev@%s...rev@%s#C--9c390eb52cd6">vcs/backends/hg.py</a>""" % (HG_REPO, rev1, rev2))
170 response.mustcontain("""<a href="/%s/compare/rev@%s...rev@%s#C--41b41c1f2796">vcs/backends/__init__.py</a>""" % (HG_REPO, rev1, rev2))
171 response.mustcontain("""<a href="/%s/compare/rev@%s...rev@%s#C--2f574d260608">vcs/backends/base.py</a>""" % (HG_REPO, rev1, rev2))
172 finally:
173 RepoModel().delete(HG_FORK)
174
175 def test_compare_remote_repos_remote_flag_off(self):
176 self.log_user()
177
178 form_data = dict(
179 repo_name=HG_FORK,
180 repo_name_full=HG_FORK,
181 repo_group=None,
182 repo_type='hg',
183 description='',
184 private=False,
185 copy_permissions=False,
186 landing_rev='tip',
187 update_after_clone=False,
188 fork_parent_id=Repository.get_by_repo_name(HG_REPO),
189 )
190 RepoModel().create_fork(form_data, cur_user=TEST_USER_ADMIN_LOGIN)
191
192 Session().commit()
193
194 rev1 = '56349e29c2af'
195 rev2 = '7d4bc8ec6be5'
98 196
99 197 response = self.app.get(url(controller='compare', action='index',
100 198 repo_name=HG_REPO,
101 199 org_ref_type="rev",
102 200 org_ref=rev1,
103 201 other_ref_type="rev",
104 202 other_ref=rev2,
105 repo=HG_FORK
203 repo=HG_FORK,
204 bundle=False,
106 205 ))
107 206
108 207 try:
109 208 response.mustcontain('%s@%s -> %s@%s' % (HG_REPO, rev1, HG_FORK, rev2))
110 209 ## outgoing changesets between those revisions
111 210
112 response.mustcontain("""<a href="/%s/changeset/7d4bc8ec6be56c0f10425afb40b6fc315a4c25e7">r6:%s</a>""" % (HG_REPO, rev1))
211 response.mustcontain("""<a href="/%s/changeset/2dda4e345facb0ccff1a191052dd1606dba6781d">r4:2dda4e345fac</a>""" % (HG_REPO))
113 212 response.mustcontain("""<a href="/%s/changeset/6fff84722075f1607a30f436523403845f84cd9e">r5:6fff84722075</a>""" % (HG_REPO))
114 response.mustcontain("""<a href="/%s/changeset/2dda4e345facb0ccff1a191052dd1606dba6781d">r4:2dda4e345fac</a>""" % (HG_REPO))
213 response.mustcontain("""<a href="/%s/changeset/7d4bc8ec6be56c0f10425afb40b6fc315a4c25e7">r6:%s</a>""" % (HG_REPO, rev2))
115 214
116 215 ## files
117 216 response.mustcontain("""<a href="/%s/compare/rev@%s...rev@%s#C--9c390eb52cd6">vcs/backends/hg.py</a>""" % (HG_REPO, rev1, rev2))
118 217 response.mustcontain("""<a href="/%s/compare/rev@%s...rev@%s#C--41b41c1f2796">vcs/backends/__init__.py</a>""" % (HG_REPO, rev1, rev2))
119 218 response.mustcontain("""<a href="/%s/compare/rev@%s...rev@%s#C--2f574d260608">vcs/backends/base.py</a>""" % (HG_REPO, rev1, rev2))
120 219 finally:
121 220 RepoModel().delete(HG_FORK)
122 221
222 # def test_compare_origin_ahead_of_fork(self):
223 # self.log_user()
224 #
225 # form_data = dict(
226 # repo_name=HG_FORK,
227 # repo_name_full=HG_FORK,
228 # repo_group=None,
229 # repo_type='hg',
230 # description='',
231 # private=False,
232 # copy_permissions=False,
233 # landing_rev='tip',
234 # update_after_clone=False,
235 # fork_parent_id=Repository.get_by_repo_name(HG_REPO),
236 # )
237 # RepoModel().create_fork(form_data, cur_user=TEST_USER_ADMIN_LOGIN)
238 #
239 # Session().commit()
240 #
241 # repo1 = Repository.get_by_repo_name(HG_REPO)
242 # r1_name = HG_REPO
243 #
244 # #commit something !
245 # cs0 = ScmModel().create_node(
246 # repo=repo1.scm_instance, repo_name=r1_name,
247 # cs=EmptyChangeset(alias='hg'), user=TEST_USER_ADMIN_LOGIN,
248 # author=TEST_USER_ADMIN_LOGIN,
249 # message='extra commit1',
250 # content='line1',
251 # f_path='file1'
252 # )
253 #
254 #
255 # rev1 = '56349e29c2af'
256 # rev2 = '7d4bc8ec6be5'
257 #
258 # response = self.app.get(url(controller='compare', action='index',
259 # repo_name=HG_REPO,
260 # org_ref_type="rev",
261 # org_ref=rev1,
262 # other_ref_type="rev",
263 # other_ref=rev2,
264 # repo=HG_FORK,
265 # bundle=False,
266 # ))
267 #
268 # try:
269 # response.mustcontain('%s@%s -> %s@%s' % (HG_REPO, rev1, HG_REPO, rev2))
270 # ## outgoing changesets between those revisions
271 #
272 # response.mustcontain("""<a href="/%s/changeset/2dda4e345facb0ccff1a191052dd1606dba6781d">r4:2dda4e345fac</a>""" % (HG_REPO))
273 # response.mustcontain("""<a href="/%s/changeset/6fff84722075f1607a30f436523403845f84cd9e">r5:6fff84722075</a>""" % (HG_REPO))
274 # response.mustcontain("""<a href="/%s/changeset/7d4bc8ec6be56c0f10425afb40b6fc315a4c25e7">r6:%s</a>""" % (HG_REPO, rev2))
275 #
276 # ## files
277 # response.mustcontain("""<a href="/%s/compare/rev@%s...rev@%s#C--9c390eb52cd6">vcs/backends/hg.py</a>""" % (HG_REPO, rev1, rev2))
278 # response.mustcontain("""<a href="/%s/compare/rev@%s...rev@%s#C--41b41c1f2796">vcs/backends/__init__.py</a>""" % (HG_REPO, rev1, rev2))
279 # response.mustcontain("""<a href="/%s/compare/rev@%s...rev@%s#C--2f574d260608">vcs/backends/base.py</a>""" % (HG_REPO, rev1, rev2))
280 # finally:
281 # RepoModel().delete(HG_FORK)
282
123 283 def test_compare_extra_commits(self):
124 284 self.log_user()
125 285
126 286 repo1 = RepoModel().create_repo(repo_name='one', repo_type='hg',
127 287 description='diff-test',
128 288 owner=TEST_USER_ADMIN_LOGIN)
129 289
130 290 repo2 = RepoModel().create_repo(repo_name='one-fork', repo_type='hg',
131 291 description='diff-test',
132 292 owner=TEST_USER_ADMIN_LOGIN)
133 293
134 294 Session().commit()
135 295 r1_id = repo1.repo_id
136 296 r1_name = repo1.repo_name
137 297 r2_id = repo2.repo_id
138 298 r2_name = repo2.repo_name
139 299
140 300 #commit something !
141 301 cs0 = ScmModel().create_node(
142 302 repo=repo1.scm_instance, repo_name=r1_name,
143 303 cs=EmptyChangeset(alias='hg'), user=TEST_USER_ADMIN_LOGIN,
144 304 author=TEST_USER_ADMIN_LOGIN,
145 305 message='commit1',
146 306 content='line1',
147 307 f_path='file1'
148 308 )
149 309
150 310 cs0_prim = ScmModel().create_node(
151 311 repo=repo2.scm_instance, repo_name=r2_name,
152 312 cs=EmptyChangeset(alias='hg'), user=TEST_USER_ADMIN_LOGIN,
153 313 author=TEST_USER_ADMIN_LOGIN,
154 314 message='commit1',
155 315 content='line1',
156 316 f_path='file1'
157 317 )
158 318
159 319 cs1 = ScmModel().commit_change(
160 320 repo=repo2.scm_instance, repo_name=r2_name,
161 321 cs=cs0_prim, user=TEST_USER_ADMIN_LOGIN, author=TEST_USER_ADMIN_LOGIN,
162 322 message='commit2',
163 323 content='line1\nline2',
164 324 f_path='file1'
165 325 )
166 326
167 327 rev1 = 'default'
168 328 rev2 = 'default'
169 329 response = self.app.get(url(controller='compare', action='index',
170 330 repo_name=r2_name,
171 331 org_ref_type="branch",
172 332 org_ref=rev1,
173 333 other_ref_type="branch",
174 334 other_ref=rev2,
175 335 repo=r1_name
176 336 ))
177 337
178 338 try:
179 339 response.mustcontain('%s@%s -> %s@%s' % (r2_name, rev1, r1_name, rev2))
180 340
181 response.mustcontain("""<div class="message">commit2</div>""")
341 response.mustcontain("""<div class="message tooltip" title="commit2" style="white-space:normal">commit2</div>""")
182 342 response.mustcontain("""<a href="/%s/changeset/%s">r1:%s</a>""" % (r2_name, cs1.raw_id, cs1.short_id))
183 343 ## files
184 344 response.mustcontain("""<a href="/%s/compare/branch@%s...branch@%s#C--826e8142e6ba">file1</a>""" % (r2_name, rev1, rev2))
185 345
186 346 finally:
187 347 RepoModel().delete(r1_id)
188 348 RepoModel().delete(r2_id)
189 349
190 350 def test_org_repo_new_commits_after_forking(self):
191 351 self.log_user()
192 352
193 353 repo1 = RepoModel().create_repo(repo_name='one', repo_type='hg',
194 354 description='diff-test',
195 355 owner=TEST_USER_ADMIN_LOGIN)
196 356
197 357 Session().commit()
198 358 r1_id = repo1.repo_id
199 359 r1_name = repo1.repo_name
200 360
201 361 #commit something initially !
202 362 cs0 = ScmModel().create_node(
203 363 repo=repo1.scm_instance, repo_name=r1_name,
204 364 cs=EmptyChangeset(alias='hg'), user=TEST_USER_ADMIN_LOGIN,
205 365 author=TEST_USER_ADMIN_LOGIN,
206 366 message='commit1',
207 367 content='line1',
208 368 f_path='file1'
209 369 )
210 370 Session().commit()
211 371 self.assertEqual(repo1.scm_instance.revisions, [cs0.raw_id])
212 372 #fork the repo1
213 373 repo2 = RepoModel().create_repo(repo_name='one-fork', repo_type='hg',
214 374 description='compare-test',
215 375 clone_uri=repo1.repo_full_path,
216 376 owner=TEST_USER_ADMIN_LOGIN, fork_of='one')
217 377 Session().commit()
218 378 self.assertEqual(repo2.scm_instance.revisions, [cs0.raw_id])
219 379 r2_id = repo2.repo_id
220 380 r2_name = repo2.repo_name
221 381
222 382 #make 3 new commits in fork
223 383 cs1 = ScmModel().create_node(
224 384 repo=repo2.scm_instance, repo_name=r2_name,
225 385 cs=repo2.scm_instance[-1], user=TEST_USER_ADMIN_LOGIN,
226 386 author=TEST_USER_ADMIN_LOGIN,
227 387 message='commit1-fork',
228 388 content='file1-line1-from-fork',
229 389 f_path='file1-fork'
230 390 )
231 391 cs2 = ScmModel().create_node(
232 392 repo=repo2.scm_instance, repo_name=r2_name,
233 393 cs=cs1, user=TEST_USER_ADMIN_LOGIN,
234 394 author=TEST_USER_ADMIN_LOGIN,
235 395 message='commit2-fork',
236 396 content='file2-line1-from-fork',
237 397 f_path='file2-fork'
238 398 )
239 399 cs3 = ScmModel().create_node(
240 400 repo=repo2.scm_instance, repo_name=r2_name,
241 401 cs=cs2, user=TEST_USER_ADMIN_LOGIN,
242 402 author=TEST_USER_ADMIN_LOGIN,
243 403 message='commit3-fork',
244 404 content='file3-line1-from-fork',
245 405 f_path='file3-fork'
246 406 )
247 407
248 408 #compare !
249 409 rev1 = 'default'
250 410 rev2 = 'default'
251 411 response = self.app.get(url(controller='compare', action='index',
252 412 repo_name=r2_name,
253 413 org_ref_type="branch",
254 414 org_ref=rev1,
255 415 other_ref_type="branch",
256 416 other_ref=rev2,
257 417 repo=r1_name,
258 418 bundle=True,
259 419 ))
260 420
261 421 try:
262 422 response.mustcontain('%s@%s -> %s@%s' % (r2_name, rev1, r1_name, rev2))
263 423 response.mustcontain("""file1-line1-from-fork""")
264 424 response.mustcontain("""file2-line1-from-fork""")
265 425 response.mustcontain("""file3-line1-from-fork""")
266 426
267 427 #add new commit into parent !
268 428 cs0 = ScmModel().create_node(
269 429 repo=repo1.scm_instance, repo_name=r1_name,
270 430 cs=EmptyChangeset(alias='hg'), user=TEST_USER_ADMIN_LOGIN,
271 431 author=TEST_USER_ADMIN_LOGIN,
272 432 message='commit2',
273 433 content='line1-from-new-parent',
274 434 f_path='file2'
275 435 )
276 436 #compare !
277 437 rev1 = 'default'
278 438 rev2 = 'default'
279 439 response = self.app.get(url(controller='compare', action='index',
280 440 repo_name=r2_name,
281 441 org_ref_type="branch",
282 442 org_ref=rev1,
283 443 other_ref_type="branch",
284 444 other_ref=rev2,
285 445 repo=r1_name,
286 446 bundle=True,
287 447 ))
288 448
289 449 response.mustcontain('%s@%s -> %s@%s' % (r2_name, rev1, r1_name, rev2))
290 450 response.mustcontain("""<a href="#">file2</a>""") # new commit from parent
291 451 response.mustcontain("""line1-from-new-parent""")
292 452 response.mustcontain("""file1-line1-from-fork""")
293 453 response.mustcontain("""file2-line1-from-fork""")
294 454 response.mustcontain("""file3-line1-from-fork""")
295 455 finally:
296 456 RepoModel().delete(r2_id)
297 457 RepoModel().delete(r1_id)
298 458
299 459 def test_org_repo_new_commits_after_forking_simple_diff(self):
300 460 self.log_user()
301 461
302 462 repo1 = RepoModel().create_repo(repo_name='one', repo_type='hg',
303 463 description='diff-test',
304 464 owner=TEST_USER_ADMIN_LOGIN)
305 465
306 466 Session().commit()
307 467 r1_id = repo1.repo_id
308 468 r1_name = repo1.repo_name
309 469
310 470 #commit something initially !
311 471 cs0 = ScmModel().create_node(
312 472 repo=repo1.scm_instance, repo_name=r1_name,
313 473 cs=EmptyChangeset(alias='hg'), user=TEST_USER_ADMIN_LOGIN,
314 474 author=TEST_USER_ADMIN_LOGIN,
315 475 message='commit1',
316 476 content='line1',
317 477 f_path='file1'
318 478 )
319 479 Session().commit()
320 480 self.assertEqual(repo1.scm_instance.revisions, [cs0.raw_id])
321 481 #fork the repo1
322 482 repo2 = RepoModel().create_repo(repo_name='one-fork', repo_type='hg',
323 483 description='compare-test',
324 484 clone_uri=repo1.repo_full_path,
325 485 owner=TEST_USER_ADMIN_LOGIN, fork_of='one')
326 486 Session().commit()
327 487 self.assertEqual(repo2.scm_instance.revisions, [cs0.raw_id])
328 488 r2_id = repo2.repo_id
329 489 r2_name = repo2.repo_name
330 490
331 491 #make 3 new commits in fork
332 492 cs1 = ScmModel().create_node(
333 493 repo=repo2.scm_instance, repo_name=r2_name,
334 494 cs=repo2.scm_instance[-1], user=TEST_USER_ADMIN_LOGIN,
335 495 author=TEST_USER_ADMIN_LOGIN,
336 496 message='commit1-fork',
337 497 content='file1-line1-from-fork',
338 498 f_path='file1-fork'
339 499 )
340 500 cs2 = ScmModel().create_node(
341 501 repo=repo2.scm_instance, repo_name=r2_name,
342 502 cs=cs1, user=TEST_USER_ADMIN_LOGIN,
343 503 author=TEST_USER_ADMIN_LOGIN,
344 504 message='commit2-fork',
345 505 content='file2-line1-from-fork',
346 506 f_path='file2-fork'
347 507 )
348 508 cs3 = ScmModel().create_node(
349 509 repo=repo2.scm_instance, repo_name=r2_name,
350 510 cs=cs2, user=TEST_USER_ADMIN_LOGIN,
351 511 author=TEST_USER_ADMIN_LOGIN,
352 512 message='commit3-fork',
353 513 content='file3-line1-from-fork',
354 514 f_path='file3-fork'
355 515 )
356 516
357 517 #compare !
358 518 rev1 = 'default'
359 519 rev2 = 'default'
360 520 response = self.app.get(url(controller='compare', action='index',
361 521 repo_name=r2_name,
362 522 org_ref_type="branch",
363 523 org_ref=rev1,
364 524 other_ref_type="branch",
365 525 other_ref=rev2,
366 526 repo=r1_name,
367 527 bundle=False,
368 528 ))
369 529
370 530 try:
371 531 #response.mustcontain('%s@%s -> %s@%s' % (r2_name, rev1, r1_name, rev2))
372 532
373 533 #add new commit into parent !
374 534 cs0 = ScmModel().create_node(
375 535 repo=repo1.scm_instance, repo_name=r1_name,
376 536 cs=EmptyChangeset(alias='hg'), user=TEST_USER_ADMIN_LOGIN,
377 537 author=TEST_USER_ADMIN_LOGIN,
378 538 message='commit2',
379 539 content='line1',
380 540 f_path='file2'
381 541 )
382 542 #compare !
383 543 rev1 = 'default'
384 544 rev2 = 'default'
385 545 response = self.app.get(url(controller='compare', action='index',
386 546 repo_name=r2_name,
387 547 org_ref_type="branch",
388 548 org_ref=rev1,
389 549 other_ref_type="branch",
390 550 other_ref=rev2,
391 551 repo=r1_name,
392 552 bundle=False
393 553 ))
394 rev2 = cs0.parents[0].raw_id
554
395 555 response.mustcontain('%s@%s -> %s@%s' % (r2_name, rev1, r1_name, rev2))
396 556 response.mustcontain("""file1-line1-from-fork""")
397 557 response.mustcontain("""file2-line1-from-fork""")
398 558 response.mustcontain("""file3-line1-from-fork""")
399 559 self.assertFalse("""<a href="#">file2</a>""" in response.body) # new commit from parent
400 560 self.assertFalse("""line1-from-new-parent""" in response.body)
401 561 finally:
402 562 RepoModel().delete(r2_id)
403 563 RepoModel().delete(r1_id)
General Comments 0
You need to be logged in to leave comments. Login now