##// END OF EJS Templates
pull-requests: added observers, and fix few problems with versioned comments
marcink -
r4481:d52ab7ab default
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
@@ -0,0 +1,52 b''
1 # -*- coding: utf-8 -*-
2
3 import logging
4 from sqlalchemy import *
5
6 from alembic.migration import MigrationContext
7 from alembic.operations import Operations
8
9 from rhodecode.lib.dbmigrate.versions import _reset_base
10 from rhodecode.model import meta, init_model_encryption
11
12
13 log = logging.getLogger(__name__)
14
15
16 def upgrade(migrate_engine):
17 """
18 Upgrade operations go here.
19 Don't create your own engine; bind migrate_engine to your metadata
20 """
21 _reset_base(migrate_engine)
22 from rhodecode.lib.dbmigrate.schema import db_4_20_0_0 as db
23
24 init_model_encryption(db)
25
26 context = MigrationContext.configure(migrate_engine.connect())
27 op = Operations(context)
28
29 table = db.PullRequestReviewers.__table__
30 with op.batch_alter_table(table.name) as batch_op:
31 new_column = Column('role', Unicode(255), nullable=True)
32 batch_op.add_column(new_column)
33
34 _fill_reviewers_role(db, op, meta.Session)
35
36
37 def downgrade(migrate_engine):
38 meta = MetaData()
39 meta.bind = migrate_engine
40
41
42 def fixups(models, _SESSION):
43 pass
44
45
46 def _fill_reviewers_role(models, op, session):
47 params = {'role': 'reviewer'}
48 query = text(
49 'UPDATE pull_request_reviewers SET role = :role'
50 ).bindparams(**params)
51 op.execute(query)
52 session().commit()
@@ -1,60 +1,60 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import os
21 import os
22 from collections import OrderedDict
22 from collections import OrderedDict
23
23
24 import sys
24 import sys
25 import platform
25 import platform
26
26
27 VERSION = tuple(open(os.path.join(
27 VERSION = tuple(open(os.path.join(
28 os.path.dirname(__file__), 'VERSION')).read().split('.'))
28 os.path.dirname(__file__), 'VERSION')).read().split('.'))
29
29
30 BACKENDS = OrderedDict()
30 BACKENDS = OrderedDict()
31
31
32 BACKENDS['hg'] = 'Mercurial repository'
32 BACKENDS['hg'] = 'Mercurial repository'
33 BACKENDS['git'] = 'Git repository'
33 BACKENDS['git'] = 'Git repository'
34 BACKENDS['svn'] = 'Subversion repository'
34 BACKENDS['svn'] = 'Subversion repository'
35
35
36
36
37 CELERY_ENABLED = False
37 CELERY_ENABLED = False
38 CELERY_EAGER = False
38 CELERY_EAGER = False
39
39
40 # link to config for pyramid
40 # link to config for pyramid
41 CONFIG = {}
41 CONFIG = {}
42
42
43 # Populated with the settings dictionary from application init in
43 # Populated with the settings dictionary from application init in
44 # rhodecode.conf.environment.load_pyramid_environment
44 # rhodecode.conf.environment.load_pyramid_environment
45 PYRAMID_SETTINGS = {}
45 PYRAMID_SETTINGS = {}
46
46
47 # Linked module for extensions
47 # Linked module for extensions
48 EXTENSIONS = {}
48 EXTENSIONS = {}
49
49
50 __version__ = ('.'.join((str(each) for each in VERSION[:3])))
50 __version__ = ('.'.join((str(each) for each in VERSION[:3])))
51 __dbversion__ = 108 # defines current db version for migrations
51 __dbversion__ = 109 # defines current db version for migrations
52 __platform__ = platform.system()
52 __platform__ = platform.system()
53 __license__ = 'AGPLv3, and Commercial License'
53 __license__ = 'AGPLv3, and Commercial License'
54 __author__ = 'RhodeCode GmbH'
54 __author__ = 'RhodeCode GmbH'
55 __url__ = 'https://code.rhodecode.com'
55 __url__ = 'https://code.rhodecode.com'
56
56
57 is_windows = __platform__ in ['Windows']
57 is_windows = __platform__ in ['Windows']
58 is_unix = not is_windows
58 is_unix = not is_windows
59 is_test = False
59 is_test = False
60 disable_error_handler = False
60 disable_error_handler = False
@@ -1,723 +1,723 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 import logging
22 import logging
23
23
24 from pyramid.httpexceptions import (
24 from pyramid.httpexceptions import (
25 HTTPNotFound, HTTPBadRequest, HTTPFound, HTTPForbidden, HTTPConflict)
25 HTTPNotFound, HTTPBadRequest, HTTPFound, HTTPForbidden, HTTPConflict)
26 from pyramid.view import view_config
26 from pyramid.view import view_config
27 from pyramid.renderers import render
27 from pyramid.renderers import render
28 from pyramid.response import Response
28 from pyramid.response import Response
29
29
30 from rhodecode.apps._base import RepoAppView
30 from rhodecode.apps._base import RepoAppView
31 from rhodecode.apps.file_store import utils as store_utils
31 from rhodecode.apps.file_store import utils as store_utils
32 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, FileOverSizeException
32 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, FileOverSizeException
33
33
34 from rhodecode.lib import diffs, codeblocks
34 from rhodecode.lib import diffs, codeblocks
35 from rhodecode.lib.auth import (
35 from rhodecode.lib.auth import (
36 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
36 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
37
37
38 from rhodecode.lib.compat import OrderedDict
38 from rhodecode.lib.compat import OrderedDict
39 from rhodecode.lib.diffs import (
39 from rhodecode.lib.diffs import (
40 cache_diff, load_cached_diff, diff_cache_exist, get_diff_context,
40 cache_diff, load_cached_diff, diff_cache_exist, get_diff_context,
41 get_diff_whitespace_flag)
41 get_diff_whitespace_flag)
42 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError, CommentVersionMismatch
42 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError, CommentVersionMismatch
43 import rhodecode.lib.helpers as h
43 import rhodecode.lib.helpers as h
44 from rhodecode.lib.utils2 import safe_unicode, str2bool
44 from rhodecode.lib.utils2 import safe_unicode, str2bool
45 from rhodecode.lib.vcs.backends.base import EmptyCommit
45 from rhodecode.lib.vcs.backends.base import EmptyCommit
46 from rhodecode.lib.vcs.exceptions import (
46 from rhodecode.lib.vcs.exceptions import (
47 RepositoryError, CommitDoesNotExistError)
47 RepositoryError, CommitDoesNotExistError)
48 from rhodecode.model.db import ChangesetComment, ChangesetStatus, FileStore, \
48 from rhodecode.model.db import ChangesetComment, ChangesetStatus, FileStore, \
49 ChangesetCommentHistory
49 ChangesetCommentHistory
50 from rhodecode.model.changeset_status import ChangesetStatusModel
50 from rhodecode.model.changeset_status import ChangesetStatusModel
51 from rhodecode.model.comment import CommentsModel
51 from rhodecode.model.comment import CommentsModel
52 from rhodecode.model.meta import Session
52 from rhodecode.model.meta import Session
53 from rhodecode.model.settings import VcsSettingsModel
53 from rhodecode.model.settings import VcsSettingsModel
54
54
55 log = logging.getLogger(__name__)
55 log = logging.getLogger(__name__)
56
56
57
57
58 def _update_with_GET(params, request):
58 def _update_with_GET(params, request):
59 for k in ['diff1', 'diff2', 'diff']:
59 for k in ['diff1', 'diff2', 'diff']:
60 params[k] += request.GET.getall(k)
60 params[k] += request.GET.getall(k)
61
61
62
62
63 class RepoCommitsView(RepoAppView):
63 class RepoCommitsView(RepoAppView):
64 def load_default_context(self):
64 def load_default_context(self):
65 c = self._get_local_tmpl_context(include_app_defaults=True)
65 c = self._get_local_tmpl_context(include_app_defaults=True)
66 c.rhodecode_repo = self.rhodecode_vcs_repo
66 c.rhodecode_repo = self.rhodecode_vcs_repo
67
67
68 return c
68 return c
69
69
70 def _is_diff_cache_enabled(self, target_repo):
70 def _is_diff_cache_enabled(self, target_repo):
71 caching_enabled = self._get_general_setting(
71 caching_enabled = self._get_general_setting(
72 target_repo, 'rhodecode_diff_cache')
72 target_repo, 'rhodecode_diff_cache')
73 log.debug('Diff caching enabled: %s', caching_enabled)
73 log.debug('Diff caching enabled: %s', caching_enabled)
74 return caching_enabled
74 return caching_enabled
75
75
76 def _commit(self, commit_id_range, method):
76 def _commit(self, commit_id_range, method):
77 _ = self.request.translate
77 _ = self.request.translate
78 c = self.load_default_context()
78 c = self.load_default_context()
79 c.fulldiff = self.request.GET.get('fulldiff')
79 c.fulldiff = self.request.GET.get('fulldiff')
80
80
81 # fetch global flags of ignore ws or context lines
81 # fetch global flags of ignore ws or context lines
82 diff_context = get_diff_context(self.request)
82 diff_context = get_diff_context(self.request)
83 hide_whitespace_changes = get_diff_whitespace_flag(self.request)
83 hide_whitespace_changes = get_diff_whitespace_flag(self.request)
84
84
85 # diff_limit will cut off the whole diff if the limit is applied
85 # diff_limit will cut off the whole diff if the limit is applied
86 # otherwise it will just hide the big files from the front-end
86 # otherwise it will just hide the big files from the front-end
87 diff_limit = c.visual.cut_off_limit_diff
87 diff_limit = c.visual.cut_off_limit_diff
88 file_limit = c.visual.cut_off_limit_file
88 file_limit = c.visual.cut_off_limit_file
89
89
90 # get ranges of commit ids if preset
90 # get ranges of commit ids if preset
91 commit_range = commit_id_range.split('...')[:2]
91 commit_range = commit_id_range.split('...')[:2]
92
92
93 try:
93 try:
94 pre_load = ['affected_files', 'author', 'branch', 'date',
94 pre_load = ['affected_files', 'author', 'branch', 'date',
95 'message', 'parents']
95 'message', 'parents']
96 if self.rhodecode_vcs_repo.alias == 'hg':
96 if self.rhodecode_vcs_repo.alias == 'hg':
97 pre_load += ['hidden', 'obsolete', 'phase']
97 pre_load += ['hidden', 'obsolete', 'phase']
98
98
99 if len(commit_range) == 2:
99 if len(commit_range) == 2:
100 commits = self.rhodecode_vcs_repo.get_commits(
100 commits = self.rhodecode_vcs_repo.get_commits(
101 start_id=commit_range[0], end_id=commit_range[1],
101 start_id=commit_range[0], end_id=commit_range[1],
102 pre_load=pre_load, translate_tags=False)
102 pre_load=pre_load, translate_tags=False)
103 commits = list(commits)
103 commits = list(commits)
104 else:
104 else:
105 commits = [self.rhodecode_vcs_repo.get_commit(
105 commits = [self.rhodecode_vcs_repo.get_commit(
106 commit_id=commit_id_range, pre_load=pre_load)]
106 commit_id=commit_id_range, pre_load=pre_load)]
107
107
108 c.commit_ranges = commits
108 c.commit_ranges = commits
109 if not c.commit_ranges:
109 if not c.commit_ranges:
110 raise RepositoryError('The commit range returned an empty result')
110 raise RepositoryError('The commit range returned an empty result')
111 except CommitDoesNotExistError as e:
111 except CommitDoesNotExistError as e:
112 msg = _('No such commit exists. Org exception: `{}`').format(e)
112 msg = _('No such commit exists. Org exception: `{}`').format(e)
113 h.flash(msg, category='error')
113 h.flash(msg, category='error')
114 raise HTTPNotFound()
114 raise HTTPNotFound()
115 except Exception:
115 except Exception:
116 log.exception("General failure")
116 log.exception("General failure")
117 raise HTTPNotFound()
117 raise HTTPNotFound()
118
118
119 c.changes = OrderedDict()
119 c.changes = OrderedDict()
120 c.lines_added = 0
120 c.lines_added = 0
121 c.lines_deleted = 0
121 c.lines_deleted = 0
122
122
123 # auto collapse if we have more than limit
123 # auto collapse if we have more than limit
124 collapse_limit = diffs.DiffProcessor._collapse_commits_over
124 collapse_limit = diffs.DiffProcessor._collapse_commits_over
125 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
125 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
126
126
127 c.commit_statuses = ChangesetStatus.STATUSES
127 c.commit_statuses = ChangesetStatus.STATUSES
128 c.inline_comments = []
128 c.inline_comments = []
129 c.files = []
129 c.files = []
130
130
131 c.statuses = []
131 c.statuses = []
132 c.comments = []
132 c.comments = []
133 c.unresolved_comments = []
133 c.unresolved_comments = []
134 c.resolved_comments = []
134 c.resolved_comments = []
135 if len(c.commit_ranges) == 1:
135 if len(c.commit_ranges) == 1:
136 commit = c.commit_ranges[0]
136 commit = c.commit_ranges[0]
137 c.comments = CommentsModel().get_comments(
137 c.comments = CommentsModel().get_comments(
138 self.db_repo.repo_id,
138 self.db_repo.repo_id,
139 revision=commit.raw_id)
139 revision=commit.raw_id)
140 c.statuses.append(ChangesetStatusModel().get_status(
140 c.statuses.append(ChangesetStatusModel().get_status(
141 self.db_repo.repo_id, commit.raw_id))
141 self.db_repo.repo_id, commit.raw_id))
142 # comments from PR
142 # comments from PR
143 statuses = ChangesetStatusModel().get_statuses(
143 statuses = ChangesetStatusModel().get_statuses(
144 self.db_repo.repo_id, commit.raw_id,
144 self.db_repo.repo_id, commit.raw_id,
145 with_revisions=True)
145 with_revisions=True)
146 prs = set(st.pull_request for st in statuses
146 prs = set(st.pull_request for st in statuses
147 if st.pull_request is not None)
147 if st.pull_request is not None)
148 # from associated statuses, check the pull requests, and
148 # from associated statuses, check the pull requests, and
149 # show comments from them
149 # show comments from them
150 for pr in prs:
150 for pr in prs:
151 c.comments.extend(pr.comments)
151 c.comments.extend(pr.comments)
152
152
153 c.unresolved_comments = CommentsModel()\
153 c.unresolved_comments = CommentsModel()\
154 .get_commit_unresolved_todos(commit.raw_id)
154 .get_commit_unresolved_todos(commit.raw_id)
155 c.resolved_comments = CommentsModel()\
155 c.resolved_comments = CommentsModel()\
156 .get_commit_resolved_todos(commit.raw_id)
156 .get_commit_resolved_todos(commit.raw_id)
157
157
158 diff = None
158 diff = None
159 # Iterate over ranges (default commit view is always one commit)
159 # Iterate over ranges (default commit view is always one commit)
160 for commit in c.commit_ranges:
160 for commit in c.commit_ranges:
161 c.changes[commit.raw_id] = []
161 c.changes[commit.raw_id] = []
162
162
163 commit2 = commit
163 commit2 = commit
164 commit1 = commit.first_parent
164 commit1 = commit.first_parent
165
165
166 if method == 'show':
166 if method == 'show':
167 inline_comments = CommentsModel().get_inline_comments(
167 inline_comments = CommentsModel().get_inline_comments(
168 self.db_repo.repo_id, revision=commit.raw_id)
168 self.db_repo.repo_id, revision=commit.raw_id)
169 c.inline_cnt = CommentsModel().get_inline_comments_count(
169 c.inline_cnt = len(CommentsModel().get_inline_comments_as_list(
170 inline_comments)
170 inline_comments))
171 c.inline_comments = inline_comments
171 c.inline_comments = inline_comments
172
172
173 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(
173 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(
174 self.db_repo)
174 self.db_repo)
175 cache_file_path = diff_cache_exist(
175 cache_file_path = diff_cache_exist(
176 cache_path, 'diff', commit.raw_id,
176 cache_path, 'diff', commit.raw_id,
177 hide_whitespace_changes, diff_context, c.fulldiff)
177 hide_whitespace_changes, diff_context, c.fulldiff)
178
178
179 caching_enabled = self._is_diff_cache_enabled(self.db_repo)
179 caching_enabled = self._is_diff_cache_enabled(self.db_repo)
180 force_recache = str2bool(self.request.GET.get('force_recache'))
180 force_recache = str2bool(self.request.GET.get('force_recache'))
181
181
182 cached_diff = None
182 cached_diff = None
183 if caching_enabled:
183 if caching_enabled:
184 cached_diff = load_cached_diff(cache_file_path)
184 cached_diff = load_cached_diff(cache_file_path)
185
185
186 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
186 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
187 if not force_recache and has_proper_diff_cache:
187 if not force_recache and has_proper_diff_cache:
188 diffset = cached_diff['diff']
188 diffset = cached_diff['diff']
189 else:
189 else:
190 vcs_diff = self.rhodecode_vcs_repo.get_diff(
190 vcs_diff = self.rhodecode_vcs_repo.get_diff(
191 commit1, commit2,
191 commit1, commit2,
192 ignore_whitespace=hide_whitespace_changes,
192 ignore_whitespace=hide_whitespace_changes,
193 context=diff_context)
193 context=diff_context)
194
194
195 diff_processor = diffs.DiffProcessor(
195 diff_processor = diffs.DiffProcessor(
196 vcs_diff, format='newdiff', diff_limit=diff_limit,
196 vcs_diff, format='newdiff', diff_limit=diff_limit,
197 file_limit=file_limit, show_full_diff=c.fulldiff)
197 file_limit=file_limit, show_full_diff=c.fulldiff)
198
198
199 _parsed = diff_processor.prepare()
199 _parsed = diff_processor.prepare()
200
200
201 diffset = codeblocks.DiffSet(
201 diffset = codeblocks.DiffSet(
202 repo_name=self.db_repo_name,
202 repo_name=self.db_repo_name,
203 source_node_getter=codeblocks.diffset_node_getter(commit1),
203 source_node_getter=codeblocks.diffset_node_getter(commit1),
204 target_node_getter=codeblocks.diffset_node_getter(commit2))
204 target_node_getter=codeblocks.diffset_node_getter(commit2))
205
205
206 diffset = self.path_filter.render_patchset_filtered(
206 diffset = self.path_filter.render_patchset_filtered(
207 diffset, _parsed, commit1.raw_id, commit2.raw_id)
207 diffset, _parsed, commit1.raw_id, commit2.raw_id)
208
208
209 # save cached diff
209 # save cached diff
210 if caching_enabled:
210 if caching_enabled:
211 cache_diff(cache_file_path, diffset, None)
211 cache_diff(cache_file_path, diffset, None)
212
212
213 c.limited_diff = diffset.limited_diff
213 c.limited_diff = diffset.limited_diff
214 c.changes[commit.raw_id] = diffset
214 c.changes[commit.raw_id] = diffset
215 else:
215 else:
216 # TODO(marcink): no cache usage here...
216 # TODO(marcink): no cache usage here...
217 _diff = self.rhodecode_vcs_repo.get_diff(
217 _diff = self.rhodecode_vcs_repo.get_diff(
218 commit1, commit2,
218 commit1, commit2,
219 ignore_whitespace=hide_whitespace_changes, context=diff_context)
219 ignore_whitespace=hide_whitespace_changes, context=diff_context)
220 diff_processor = diffs.DiffProcessor(
220 diff_processor = diffs.DiffProcessor(
221 _diff, format='newdiff', diff_limit=diff_limit,
221 _diff, format='newdiff', diff_limit=diff_limit,
222 file_limit=file_limit, show_full_diff=c.fulldiff)
222 file_limit=file_limit, show_full_diff=c.fulldiff)
223 # downloads/raw we only need RAW diff nothing else
223 # downloads/raw we only need RAW diff nothing else
224 diff = self.path_filter.get_raw_patch(diff_processor)
224 diff = self.path_filter.get_raw_patch(diff_processor)
225 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
225 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
226
226
227 # sort comments by how they were generated
227 # sort comments by how they were generated
228 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
228 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
229
229
230 if len(c.commit_ranges) == 1:
230 if len(c.commit_ranges) == 1:
231 c.commit = c.commit_ranges[0]
231 c.commit = c.commit_ranges[0]
232 c.parent_tmpl = ''.join(
232 c.parent_tmpl = ''.join(
233 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
233 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
234
234
235 if method == 'download':
235 if method == 'download':
236 response = Response(diff)
236 response = Response(diff)
237 response.content_type = 'text/plain'
237 response.content_type = 'text/plain'
238 response.content_disposition = (
238 response.content_disposition = (
239 'attachment; filename=%s.diff' % commit_id_range[:12])
239 'attachment; filename=%s.diff' % commit_id_range[:12])
240 return response
240 return response
241 elif method == 'patch':
241 elif method == 'patch':
242 c.diff = safe_unicode(diff)
242 c.diff = safe_unicode(diff)
243 patch = render(
243 patch = render(
244 'rhodecode:templates/changeset/patch_changeset.mako',
244 'rhodecode:templates/changeset/patch_changeset.mako',
245 self._get_template_context(c), self.request)
245 self._get_template_context(c), self.request)
246 response = Response(patch)
246 response = Response(patch)
247 response.content_type = 'text/plain'
247 response.content_type = 'text/plain'
248 return response
248 return response
249 elif method == 'raw':
249 elif method == 'raw':
250 response = Response(diff)
250 response = Response(diff)
251 response.content_type = 'text/plain'
251 response.content_type = 'text/plain'
252 return response
252 return response
253 elif method == 'show':
253 elif method == 'show':
254 if len(c.commit_ranges) == 1:
254 if len(c.commit_ranges) == 1:
255 html = render(
255 html = render(
256 'rhodecode:templates/changeset/changeset.mako',
256 'rhodecode:templates/changeset/changeset.mako',
257 self._get_template_context(c), self.request)
257 self._get_template_context(c), self.request)
258 return Response(html)
258 return Response(html)
259 else:
259 else:
260 c.ancestor = None
260 c.ancestor = None
261 c.target_repo = self.db_repo
261 c.target_repo = self.db_repo
262 html = render(
262 html = render(
263 'rhodecode:templates/changeset/changeset_range.mako',
263 'rhodecode:templates/changeset/changeset_range.mako',
264 self._get_template_context(c), self.request)
264 self._get_template_context(c), self.request)
265 return Response(html)
265 return Response(html)
266
266
267 raise HTTPBadRequest()
267 raise HTTPBadRequest()
268
268
269 @LoginRequired()
269 @LoginRequired()
270 @HasRepoPermissionAnyDecorator(
270 @HasRepoPermissionAnyDecorator(
271 'repository.read', 'repository.write', 'repository.admin')
271 'repository.read', 'repository.write', 'repository.admin')
272 @view_config(
272 @view_config(
273 route_name='repo_commit', request_method='GET',
273 route_name='repo_commit', request_method='GET',
274 renderer=None)
274 renderer=None)
275 def repo_commit_show(self):
275 def repo_commit_show(self):
276 commit_id = self.request.matchdict['commit_id']
276 commit_id = self.request.matchdict['commit_id']
277 return self._commit(commit_id, method='show')
277 return self._commit(commit_id, method='show')
278
278
279 @LoginRequired()
279 @LoginRequired()
280 @HasRepoPermissionAnyDecorator(
280 @HasRepoPermissionAnyDecorator(
281 'repository.read', 'repository.write', 'repository.admin')
281 'repository.read', 'repository.write', 'repository.admin')
282 @view_config(
282 @view_config(
283 route_name='repo_commit_raw', request_method='GET',
283 route_name='repo_commit_raw', request_method='GET',
284 renderer=None)
284 renderer=None)
285 @view_config(
285 @view_config(
286 route_name='repo_commit_raw_deprecated', request_method='GET',
286 route_name='repo_commit_raw_deprecated', request_method='GET',
287 renderer=None)
287 renderer=None)
288 def repo_commit_raw(self):
288 def repo_commit_raw(self):
289 commit_id = self.request.matchdict['commit_id']
289 commit_id = self.request.matchdict['commit_id']
290 return self._commit(commit_id, method='raw')
290 return self._commit(commit_id, method='raw')
291
291
292 @LoginRequired()
292 @LoginRequired()
293 @HasRepoPermissionAnyDecorator(
293 @HasRepoPermissionAnyDecorator(
294 'repository.read', 'repository.write', 'repository.admin')
294 'repository.read', 'repository.write', 'repository.admin')
295 @view_config(
295 @view_config(
296 route_name='repo_commit_patch', request_method='GET',
296 route_name='repo_commit_patch', request_method='GET',
297 renderer=None)
297 renderer=None)
298 def repo_commit_patch(self):
298 def repo_commit_patch(self):
299 commit_id = self.request.matchdict['commit_id']
299 commit_id = self.request.matchdict['commit_id']
300 return self._commit(commit_id, method='patch')
300 return self._commit(commit_id, method='patch')
301
301
302 @LoginRequired()
302 @LoginRequired()
303 @HasRepoPermissionAnyDecorator(
303 @HasRepoPermissionAnyDecorator(
304 'repository.read', 'repository.write', 'repository.admin')
304 'repository.read', 'repository.write', 'repository.admin')
305 @view_config(
305 @view_config(
306 route_name='repo_commit_download', request_method='GET',
306 route_name='repo_commit_download', request_method='GET',
307 renderer=None)
307 renderer=None)
308 def repo_commit_download(self):
308 def repo_commit_download(self):
309 commit_id = self.request.matchdict['commit_id']
309 commit_id = self.request.matchdict['commit_id']
310 return self._commit(commit_id, method='download')
310 return self._commit(commit_id, method='download')
311
311
312 @LoginRequired()
312 @LoginRequired()
313 @NotAnonymous()
313 @NotAnonymous()
314 @HasRepoPermissionAnyDecorator(
314 @HasRepoPermissionAnyDecorator(
315 'repository.read', 'repository.write', 'repository.admin')
315 'repository.read', 'repository.write', 'repository.admin')
316 @CSRFRequired()
316 @CSRFRequired()
317 @view_config(
317 @view_config(
318 route_name='repo_commit_comment_create', request_method='POST',
318 route_name='repo_commit_comment_create', request_method='POST',
319 renderer='json_ext')
319 renderer='json_ext')
320 def repo_commit_comment_create(self):
320 def repo_commit_comment_create(self):
321 _ = self.request.translate
321 _ = self.request.translate
322 commit_id = self.request.matchdict['commit_id']
322 commit_id = self.request.matchdict['commit_id']
323
323
324 c = self.load_default_context()
324 c = self.load_default_context()
325 status = self.request.POST.get('changeset_status', None)
325 status = self.request.POST.get('changeset_status', None)
326 text = self.request.POST.get('text')
326 text = self.request.POST.get('text')
327 comment_type = self.request.POST.get('comment_type')
327 comment_type = self.request.POST.get('comment_type')
328 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
328 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
329
329
330 if status:
330 if status:
331 text = text or (_('Status change %(transition_icon)s %(status)s')
331 text = text or (_('Status change %(transition_icon)s %(status)s')
332 % {'transition_icon': '>',
332 % {'transition_icon': '>',
333 'status': ChangesetStatus.get_status_lbl(status)})
333 'status': ChangesetStatus.get_status_lbl(status)})
334
334
335 multi_commit_ids = []
335 multi_commit_ids = []
336 for _commit_id in self.request.POST.get('commit_ids', '').split(','):
336 for _commit_id in self.request.POST.get('commit_ids', '').split(','):
337 if _commit_id not in ['', None, EmptyCommit.raw_id]:
337 if _commit_id not in ['', None, EmptyCommit.raw_id]:
338 if _commit_id not in multi_commit_ids:
338 if _commit_id not in multi_commit_ids:
339 multi_commit_ids.append(_commit_id)
339 multi_commit_ids.append(_commit_id)
340
340
341 commit_ids = multi_commit_ids or [commit_id]
341 commit_ids = multi_commit_ids or [commit_id]
342
342
343 comment = None
343 comment = None
344 for current_id in filter(None, commit_ids):
344 for current_id in filter(None, commit_ids):
345 comment = CommentsModel().create(
345 comment = CommentsModel().create(
346 text=text,
346 text=text,
347 repo=self.db_repo.repo_id,
347 repo=self.db_repo.repo_id,
348 user=self._rhodecode_db_user.user_id,
348 user=self._rhodecode_db_user.user_id,
349 commit_id=current_id,
349 commit_id=current_id,
350 f_path=self.request.POST.get('f_path'),
350 f_path=self.request.POST.get('f_path'),
351 line_no=self.request.POST.get('line'),
351 line_no=self.request.POST.get('line'),
352 status_change=(ChangesetStatus.get_status_lbl(status)
352 status_change=(ChangesetStatus.get_status_lbl(status)
353 if status else None),
353 if status else None),
354 status_change_type=status,
354 status_change_type=status,
355 comment_type=comment_type,
355 comment_type=comment_type,
356 resolves_comment_id=resolves_comment_id,
356 resolves_comment_id=resolves_comment_id,
357 auth_user=self._rhodecode_user
357 auth_user=self._rhodecode_user
358 )
358 )
359
359
360 # get status if set !
360 # get status if set !
361 if status:
361 if status:
362 # if latest status was from pull request and it's closed
362 # if latest status was from pull request and it's closed
363 # disallow changing status !
363 # disallow changing status !
364 # dont_allow_on_closed_pull_request = True !
364 # dont_allow_on_closed_pull_request = True !
365
365
366 try:
366 try:
367 ChangesetStatusModel().set_status(
367 ChangesetStatusModel().set_status(
368 self.db_repo.repo_id,
368 self.db_repo.repo_id,
369 status,
369 status,
370 self._rhodecode_db_user.user_id,
370 self._rhodecode_db_user.user_id,
371 comment,
371 comment,
372 revision=current_id,
372 revision=current_id,
373 dont_allow_on_closed_pull_request=True
373 dont_allow_on_closed_pull_request=True
374 )
374 )
375 except StatusChangeOnClosedPullRequestError:
375 except StatusChangeOnClosedPullRequestError:
376 msg = _('Changing the status of a commit associated with '
376 msg = _('Changing the status of a commit associated with '
377 'a closed pull request is not allowed')
377 'a closed pull request is not allowed')
378 log.exception(msg)
378 log.exception(msg)
379 h.flash(msg, category='warning')
379 h.flash(msg, category='warning')
380 raise HTTPFound(h.route_path(
380 raise HTTPFound(h.route_path(
381 'repo_commit', repo_name=self.db_repo_name,
381 'repo_commit', repo_name=self.db_repo_name,
382 commit_id=current_id))
382 commit_id=current_id))
383
383
384 commit = self.db_repo.get_commit(current_id)
384 commit = self.db_repo.get_commit(current_id)
385 CommentsModel().trigger_commit_comment_hook(
385 CommentsModel().trigger_commit_comment_hook(
386 self.db_repo, self._rhodecode_user, 'create',
386 self.db_repo, self._rhodecode_user, 'create',
387 data={'comment': comment, 'commit': commit})
387 data={'comment': comment, 'commit': commit})
388
388
389 # finalize, commit and redirect
389 # finalize, commit and redirect
390 Session().commit()
390 Session().commit()
391
391
392 data = {
392 data = {
393 'target_id': h.safeid(h.safe_unicode(
393 'target_id': h.safeid(h.safe_unicode(
394 self.request.POST.get('f_path'))),
394 self.request.POST.get('f_path'))),
395 }
395 }
396 if comment:
396 if comment:
397 c.co = comment
397 c.co = comment
398 rendered_comment = render(
398 rendered_comment = render(
399 'rhodecode:templates/changeset/changeset_comment_block.mako',
399 'rhodecode:templates/changeset/changeset_comment_block.mako',
400 self._get_template_context(c), self.request)
400 self._get_template_context(c), self.request)
401
401
402 data.update(comment.get_dict())
402 data.update(comment.get_dict())
403 data.update({'rendered_text': rendered_comment})
403 data.update({'rendered_text': rendered_comment})
404
404
405 return data
405 return data
406
406
407 @LoginRequired()
407 @LoginRequired()
408 @NotAnonymous()
408 @NotAnonymous()
409 @HasRepoPermissionAnyDecorator(
409 @HasRepoPermissionAnyDecorator(
410 'repository.read', 'repository.write', 'repository.admin')
410 'repository.read', 'repository.write', 'repository.admin')
411 @CSRFRequired()
411 @CSRFRequired()
412 @view_config(
412 @view_config(
413 route_name='repo_commit_comment_preview', request_method='POST',
413 route_name='repo_commit_comment_preview', request_method='POST',
414 renderer='string', xhr=True)
414 renderer='string', xhr=True)
415 def repo_commit_comment_preview(self):
415 def repo_commit_comment_preview(self):
416 # Technically a CSRF token is not needed as no state changes with this
416 # Technically a CSRF token is not needed as no state changes with this
417 # call. However, as this is a POST is better to have it, so automated
417 # call. However, as this is a POST is better to have it, so automated
418 # tools don't flag it as potential CSRF.
418 # tools don't flag it as potential CSRF.
419 # Post is required because the payload could be bigger than the maximum
419 # Post is required because the payload could be bigger than the maximum
420 # allowed by GET.
420 # allowed by GET.
421
421
422 text = self.request.POST.get('text')
422 text = self.request.POST.get('text')
423 renderer = self.request.POST.get('renderer') or 'rst'
423 renderer = self.request.POST.get('renderer') or 'rst'
424 if text:
424 if text:
425 return h.render(text, renderer=renderer, mentions=True,
425 return h.render(text, renderer=renderer, mentions=True,
426 repo_name=self.db_repo_name)
426 repo_name=self.db_repo_name)
427 return ''
427 return ''
428
428
429 @LoginRequired()
429 @LoginRequired()
430 @NotAnonymous()
430 @NotAnonymous()
431 @HasRepoPermissionAnyDecorator(
431 @HasRepoPermissionAnyDecorator(
432 'repository.read', 'repository.write', 'repository.admin')
432 'repository.read', 'repository.write', 'repository.admin')
433 @CSRFRequired()
433 @CSRFRequired()
434 @view_config(
434 @view_config(
435 route_name='repo_commit_comment_history_view', request_method='POST',
435 route_name='repo_commit_comment_history_view', request_method='POST',
436 renderer='string', xhr=True)
436 renderer='string', xhr=True)
437 def repo_commit_comment_history_view(self):
437 def repo_commit_comment_history_view(self):
438 c = self.load_default_context()
438 c = self.load_default_context()
439
439
440 comment_history_id = self.request.matchdict['comment_history_id']
440 comment_history_id = self.request.matchdict['comment_history_id']
441 comment_history = ChangesetCommentHistory.get_or_404(comment_history_id)
441 comment_history = ChangesetCommentHistory.get_or_404(comment_history_id)
442 is_repo_comment = comment_history.comment.repo.repo_id == self.db_repo.repo_id
442 is_repo_comment = comment_history.comment.repo.repo_id == self.db_repo.repo_id
443
443
444 if is_repo_comment:
444 if is_repo_comment:
445 c.comment_history = comment_history
445 c.comment_history = comment_history
446
446
447 rendered_comment = render(
447 rendered_comment = render(
448 'rhodecode:templates/changeset/comment_history.mako',
448 'rhodecode:templates/changeset/comment_history.mako',
449 self._get_template_context(c)
449 self._get_template_context(c)
450 , self.request)
450 , self.request)
451 return rendered_comment
451 return rendered_comment
452 else:
452 else:
453 log.warning('No permissions for user %s to show comment_history_id: %s',
453 log.warning('No permissions for user %s to show comment_history_id: %s',
454 self._rhodecode_db_user, comment_history_id)
454 self._rhodecode_db_user, comment_history_id)
455 raise HTTPNotFound()
455 raise HTTPNotFound()
456
456
457 @LoginRequired()
457 @LoginRequired()
458 @NotAnonymous()
458 @NotAnonymous()
459 @HasRepoPermissionAnyDecorator(
459 @HasRepoPermissionAnyDecorator(
460 'repository.read', 'repository.write', 'repository.admin')
460 'repository.read', 'repository.write', 'repository.admin')
461 @CSRFRequired()
461 @CSRFRequired()
462 @view_config(
462 @view_config(
463 route_name='repo_commit_comment_attachment_upload', request_method='POST',
463 route_name='repo_commit_comment_attachment_upload', request_method='POST',
464 renderer='json_ext', xhr=True)
464 renderer='json_ext', xhr=True)
465 def repo_commit_comment_attachment_upload(self):
465 def repo_commit_comment_attachment_upload(self):
466 c = self.load_default_context()
466 c = self.load_default_context()
467 upload_key = 'attachment'
467 upload_key = 'attachment'
468
468
469 file_obj = self.request.POST.get(upload_key)
469 file_obj = self.request.POST.get(upload_key)
470
470
471 if file_obj is None:
471 if file_obj is None:
472 self.request.response.status = 400
472 self.request.response.status = 400
473 return {'store_fid': None,
473 return {'store_fid': None,
474 'access_path': None,
474 'access_path': None,
475 'error': '{} data field is missing'.format(upload_key)}
475 'error': '{} data field is missing'.format(upload_key)}
476
476
477 if not hasattr(file_obj, 'filename'):
477 if not hasattr(file_obj, 'filename'):
478 self.request.response.status = 400
478 self.request.response.status = 400
479 return {'store_fid': None,
479 return {'store_fid': None,
480 'access_path': None,
480 'access_path': None,
481 'error': 'filename cannot be read from the data field'}
481 'error': 'filename cannot be read from the data field'}
482
482
483 filename = file_obj.filename
483 filename = file_obj.filename
484 file_display_name = filename
484 file_display_name = filename
485
485
486 metadata = {
486 metadata = {
487 'user_uploaded': {'username': self._rhodecode_user.username,
487 'user_uploaded': {'username': self._rhodecode_user.username,
488 'user_id': self._rhodecode_user.user_id,
488 'user_id': self._rhodecode_user.user_id,
489 'ip': self._rhodecode_user.ip_addr}}
489 'ip': self._rhodecode_user.ip_addr}}
490
490
491 # TODO(marcink): allow .ini configuration for allowed_extensions, and file-size
491 # TODO(marcink): allow .ini configuration for allowed_extensions, and file-size
492 allowed_extensions = [
492 allowed_extensions = [
493 'gif', '.jpeg', '.jpg', '.png', '.docx', '.gz', '.log', '.pdf',
493 'gif', '.jpeg', '.jpg', '.png', '.docx', '.gz', '.log', '.pdf',
494 '.pptx', '.txt', '.xlsx', '.zip']
494 '.pptx', '.txt', '.xlsx', '.zip']
495 max_file_size = 10 * 1024 * 1024 # 10MB, also validated via dropzone.js
495 max_file_size = 10 * 1024 * 1024 # 10MB, also validated via dropzone.js
496
496
497 try:
497 try:
498 storage = store_utils.get_file_storage(self.request.registry.settings)
498 storage = store_utils.get_file_storage(self.request.registry.settings)
499 store_uid, metadata = storage.save_file(
499 store_uid, metadata = storage.save_file(
500 file_obj.file, filename, extra_metadata=metadata,
500 file_obj.file, filename, extra_metadata=metadata,
501 extensions=allowed_extensions, max_filesize=max_file_size)
501 extensions=allowed_extensions, max_filesize=max_file_size)
502 except FileNotAllowedException:
502 except FileNotAllowedException:
503 self.request.response.status = 400
503 self.request.response.status = 400
504 permitted_extensions = ', '.join(allowed_extensions)
504 permitted_extensions = ', '.join(allowed_extensions)
505 error_msg = 'File `{}` is not allowed. ' \
505 error_msg = 'File `{}` is not allowed. ' \
506 'Only following extensions are permitted: {}'.format(
506 'Only following extensions are permitted: {}'.format(
507 filename, permitted_extensions)
507 filename, permitted_extensions)
508 return {'store_fid': None,
508 return {'store_fid': None,
509 'access_path': None,
509 'access_path': None,
510 'error': error_msg}
510 'error': error_msg}
511 except FileOverSizeException:
511 except FileOverSizeException:
512 self.request.response.status = 400
512 self.request.response.status = 400
513 limit_mb = h.format_byte_size_binary(max_file_size)
513 limit_mb = h.format_byte_size_binary(max_file_size)
514 return {'store_fid': None,
514 return {'store_fid': None,
515 'access_path': None,
515 'access_path': None,
516 'error': 'File {} is exceeding allowed limit of {}.'.format(
516 'error': 'File {} is exceeding allowed limit of {}.'.format(
517 filename, limit_mb)}
517 filename, limit_mb)}
518
518
519 try:
519 try:
520 entry = FileStore.create(
520 entry = FileStore.create(
521 file_uid=store_uid, filename=metadata["filename"],
521 file_uid=store_uid, filename=metadata["filename"],
522 file_hash=metadata["sha256"], file_size=metadata["size"],
522 file_hash=metadata["sha256"], file_size=metadata["size"],
523 file_display_name=file_display_name,
523 file_display_name=file_display_name,
524 file_description=u'comment attachment `{}`'.format(safe_unicode(filename)),
524 file_description=u'comment attachment `{}`'.format(safe_unicode(filename)),
525 hidden=True, check_acl=True, user_id=self._rhodecode_user.user_id,
525 hidden=True, check_acl=True, user_id=self._rhodecode_user.user_id,
526 scope_repo_id=self.db_repo.repo_id
526 scope_repo_id=self.db_repo.repo_id
527 )
527 )
528 Session().add(entry)
528 Session().add(entry)
529 Session().commit()
529 Session().commit()
530 log.debug('Stored upload in DB as %s', entry)
530 log.debug('Stored upload in DB as %s', entry)
531 except Exception:
531 except Exception:
532 log.exception('Failed to store file %s', filename)
532 log.exception('Failed to store file %s', filename)
533 self.request.response.status = 400
533 self.request.response.status = 400
534 return {'store_fid': None,
534 return {'store_fid': None,
535 'access_path': None,
535 'access_path': None,
536 'error': 'File {} failed to store in DB.'.format(filename)}
536 'error': 'File {} failed to store in DB.'.format(filename)}
537
537
538 Session().commit()
538 Session().commit()
539
539
540 return {
540 return {
541 'store_fid': store_uid,
541 'store_fid': store_uid,
542 'access_path': h.route_path(
542 'access_path': h.route_path(
543 'download_file', fid=store_uid),
543 'download_file', fid=store_uid),
544 'fqn_access_path': h.route_url(
544 'fqn_access_path': h.route_url(
545 'download_file', fid=store_uid),
545 'download_file', fid=store_uid),
546 'repo_access_path': h.route_path(
546 'repo_access_path': h.route_path(
547 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
547 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
548 'repo_fqn_access_path': h.route_url(
548 'repo_fqn_access_path': h.route_url(
549 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
549 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
550 }
550 }
551
551
552 @LoginRequired()
552 @LoginRequired()
553 @NotAnonymous()
553 @NotAnonymous()
554 @HasRepoPermissionAnyDecorator(
554 @HasRepoPermissionAnyDecorator(
555 'repository.read', 'repository.write', 'repository.admin')
555 'repository.read', 'repository.write', 'repository.admin')
556 @CSRFRequired()
556 @CSRFRequired()
557 @view_config(
557 @view_config(
558 route_name='repo_commit_comment_delete', request_method='POST',
558 route_name='repo_commit_comment_delete', request_method='POST',
559 renderer='json_ext')
559 renderer='json_ext')
560 def repo_commit_comment_delete(self):
560 def repo_commit_comment_delete(self):
561 commit_id = self.request.matchdict['commit_id']
561 commit_id = self.request.matchdict['commit_id']
562 comment_id = self.request.matchdict['comment_id']
562 comment_id = self.request.matchdict['comment_id']
563
563
564 comment = ChangesetComment.get_or_404(comment_id)
564 comment = ChangesetComment.get_or_404(comment_id)
565 if not comment:
565 if not comment:
566 log.debug('Comment with id:%s not found, skipping', comment_id)
566 log.debug('Comment with id:%s not found, skipping', comment_id)
567 # comment already deleted in another call probably
567 # comment already deleted in another call probably
568 return True
568 return True
569
569
570 if comment.immutable:
570 if comment.immutable:
571 # don't allow deleting comments that are immutable
571 # don't allow deleting comments that are immutable
572 raise HTTPForbidden()
572 raise HTTPForbidden()
573
573
574 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
574 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
575 super_admin = h.HasPermissionAny('hg.admin')()
575 super_admin = h.HasPermissionAny('hg.admin')()
576 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
576 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
577 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
577 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
578 comment_repo_admin = is_repo_admin and is_repo_comment
578 comment_repo_admin = is_repo_admin and is_repo_comment
579
579
580 if super_admin or comment_owner or comment_repo_admin:
580 if super_admin or comment_owner or comment_repo_admin:
581 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
581 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
582 Session().commit()
582 Session().commit()
583 return True
583 return True
584 else:
584 else:
585 log.warning('No permissions for user %s to delete comment_id: %s',
585 log.warning('No permissions for user %s to delete comment_id: %s',
586 self._rhodecode_db_user, comment_id)
586 self._rhodecode_db_user, comment_id)
587 raise HTTPNotFound()
587 raise HTTPNotFound()
588
588
589 @LoginRequired()
589 @LoginRequired()
590 @NotAnonymous()
590 @NotAnonymous()
591 @HasRepoPermissionAnyDecorator(
591 @HasRepoPermissionAnyDecorator(
592 'repository.read', 'repository.write', 'repository.admin')
592 'repository.read', 'repository.write', 'repository.admin')
593 @CSRFRequired()
593 @CSRFRequired()
594 @view_config(
594 @view_config(
595 route_name='repo_commit_comment_edit', request_method='POST',
595 route_name='repo_commit_comment_edit', request_method='POST',
596 renderer='json_ext')
596 renderer='json_ext')
597 def repo_commit_comment_edit(self):
597 def repo_commit_comment_edit(self):
598 self.load_default_context()
598 self.load_default_context()
599
599
600 comment_id = self.request.matchdict['comment_id']
600 comment_id = self.request.matchdict['comment_id']
601 comment = ChangesetComment.get_or_404(comment_id)
601 comment = ChangesetComment.get_or_404(comment_id)
602
602
603 if comment.immutable:
603 if comment.immutable:
604 # don't allow deleting comments that are immutable
604 # don't allow deleting comments that are immutable
605 raise HTTPForbidden()
605 raise HTTPForbidden()
606
606
607 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
607 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
608 super_admin = h.HasPermissionAny('hg.admin')()
608 super_admin = h.HasPermissionAny('hg.admin')()
609 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
609 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
610 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
610 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
611 comment_repo_admin = is_repo_admin and is_repo_comment
611 comment_repo_admin = is_repo_admin and is_repo_comment
612
612
613 if super_admin or comment_owner or comment_repo_admin:
613 if super_admin or comment_owner or comment_repo_admin:
614 text = self.request.POST.get('text')
614 text = self.request.POST.get('text')
615 version = self.request.POST.get('version')
615 version = self.request.POST.get('version')
616 if text == comment.text:
616 if text == comment.text:
617 log.warning(
617 log.warning(
618 'Comment(repo): '
618 'Comment(repo): '
619 'Trying to create new version '
619 'Trying to create new version '
620 'with the same comment body {}'.format(
620 'with the same comment body {}'.format(
621 comment_id,
621 comment_id,
622 )
622 )
623 )
623 )
624 raise HTTPNotFound()
624 raise HTTPNotFound()
625
625
626 if version.isdigit():
626 if version.isdigit():
627 version = int(version)
627 version = int(version)
628 else:
628 else:
629 log.warning(
629 log.warning(
630 'Comment(repo): Wrong version type {} {} '
630 'Comment(repo): Wrong version type {} {} '
631 'for comment {}'.format(
631 'for comment {}'.format(
632 version,
632 version,
633 type(version),
633 type(version),
634 comment_id,
634 comment_id,
635 )
635 )
636 )
636 )
637 raise HTTPNotFound()
637 raise HTTPNotFound()
638
638
639 try:
639 try:
640 comment_history = CommentsModel().edit(
640 comment_history = CommentsModel().edit(
641 comment_id=comment_id,
641 comment_id=comment_id,
642 text=text,
642 text=text,
643 auth_user=self._rhodecode_user,
643 auth_user=self._rhodecode_user,
644 version=version,
644 version=version,
645 )
645 )
646 except CommentVersionMismatch:
646 except CommentVersionMismatch:
647 raise HTTPConflict()
647 raise HTTPConflict()
648
648
649 if not comment_history:
649 if not comment_history:
650 raise HTTPNotFound()
650 raise HTTPNotFound()
651
651
652 commit_id = self.request.matchdict['commit_id']
652 commit_id = self.request.matchdict['commit_id']
653 commit = self.db_repo.get_commit(commit_id)
653 commit = self.db_repo.get_commit(commit_id)
654 CommentsModel().trigger_commit_comment_hook(
654 CommentsModel().trigger_commit_comment_hook(
655 self.db_repo, self._rhodecode_user, 'edit',
655 self.db_repo, self._rhodecode_user, 'edit',
656 data={'comment': comment, 'commit': commit})
656 data={'comment': comment, 'commit': commit})
657
657
658 Session().commit()
658 Session().commit()
659 return {
659 return {
660 'comment_history_id': comment_history.comment_history_id,
660 'comment_history_id': comment_history.comment_history_id,
661 'comment_id': comment.comment_id,
661 'comment_id': comment.comment_id,
662 'comment_version': comment_history.version,
662 'comment_version': comment_history.version,
663 'comment_author_username': comment_history.author.username,
663 'comment_author_username': comment_history.author.username,
664 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
664 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
665 'comment_created_on': h.age_component(comment_history.created_on,
665 'comment_created_on': h.age_component(comment_history.created_on,
666 time_is_local=True),
666 time_is_local=True),
667 }
667 }
668 else:
668 else:
669 log.warning('No permissions for user %s to edit comment_id: %s',
669 log.warning('No permissions for user %s to edit comment_id: %s',
670 self._rhodecode_db_user, comment_id)
670 self._rhodecode_db_user, comment_id)
671 raise HTTPNotFound()
671 raise HTTPNotFound()
672
672
673 @LoginRequired()
673 @LoginRequired()
674 @HasRepoPermissionAnyDecorator(
674 @HasRepoPermissionAnyDecorator(
675 'repository.read', 'repository.write', 'repository.admin')
675 'repository.read', 'repository.write', 'repository.admin')
676 @view_config(
676 @view_config(
677 route_name='repo_commit_data', request_method='GET',
677 route_name='repo_commit_data', request_method='GET',
678 renderer='json_ext', xhr=True)
678 renderer='json_ext', xhr=True)
679 def repo_commit_data(self):
679 def repo_commit_data(self):
680 commit_id = self.request.matchdict['commit_id']
680 commit_id = self.request.matchdict['commit_id']
681 self.load_default_context()
681 self.load_default_context()
682
682
683 try:
683 try:
684 return self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
684 return self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
685 except CommitDoesNotExistError as e:
685 except CommitDoesNotExistError as e:
686 return EmptyCommit(message=str(e))
686 return EmptyCommit(message=str(e))
687
687
688 @LoginRequired()
688 @LoginRequired()
689 @HasRepoPermissionAnyDecorator(
689 @HasRepoPermissionAnyDecorator(
690 'repository.read', 'repository.write', 'repository.admin')
690 'repository.read', 'repository.write', 'repository.admin')
691 @view_config(
691 @view_config(
692 route_name='repo_commit_children', request_method='GET',
692 route_name='repo_commit_children', request_method='GET',
693 renderer='json_ext', xhr=True)
693 renderer='json_ext', xhr=True)
694 def repo_commit_children(self):
694 def repo_commit_children(self):
695 commit_id = self.request.matchdict['commit_id']
695 commit_id = self.request.matchdict['commit_id']
696 self.load_default_context()
696 self.load_default_context()
697
697
698 try:
698 try:
699 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
699 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
700 children = commit.children
700 children = commit.children
701 except CommitDoesNotExistError:
701 except CommitDoesNotExistError:
702 children = []
702 children = []
703
703
704 result = {"results": children}
704 result = {"results": children}
705 return result
705 return result
706
706
707 @LoginRequired()
707 @LoginRequired()
708 @HasRepoPermissionAnyDecorator(
708 @HasRepoPermissionAnyDecorator(
709 'repository.read', 'repository.write', 'repository.admin')
709 'repository.read', 'repository.write', 'repository.admin')
710 @view_config(
710 @view_config(
711 route_name='repo_commit_parents', request_method='GET',
711 route_name='repo_commit_parents', request_method='GET',
712 renderer='json_ext')
712 renderer='json_ext')
713 def repo_commit_parents(self):
713 def repo_commit_parents(self):
714 commit_id = self.request.matchdict['commit_id']
714 commit_id = self.request.matchdict['commit_id']
715 self.load_default_context()
715 self.load_default_context()
716
716
717 try:
717 try:
718 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
718 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
719 parents = commit.parents
719 parents = commit.parents
720 except CommitDoesNotExistError:
720 except CommitDoesNotExistError:
721 parents = []
721 parents = []
722 result = {"results": parents}
722 result = {"results": parents}
723 return result
723 return result
@@ -1,840 +1,840 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2020 RhodeCode GmbH
3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 comments model for RhodeCode
22 comments model for RhodeCode
23 """
23 """
24 import datetime
24 import datetime
25
25
26 import logging
26 import logging
27 import traceback
27 import traceback
28 import collections
28 import collections
29
29
30 from pyramid.threadlocal import get_current_registry, get_current_request
30 from pyramid.threadlocal import get_current_registry, get_current_request
31 from sqlalchemy.sql.expression import null
31 from sqlalchemy.sql.expression import null
32 from sqlalchemy.sql.functions import coalesce
32 from sqlalchemy.sql.functions import coalesce
33
33
34 from rhodecode.lib import helpers as h, diffs, channelstream, hooks_utils
34 from rhodecode.lib import helpers as h, diffs, channelstream, hooks_utils
35 from rhodecode.lib import audit_logger
35 from rhodecode.lib import audit_logger
36 from rhodecode.lib.exceptions import CommentVersionMismatch
36 from rhodecode.lib.exceptions import CommentVersionMismatch
37 from rhodecode.lib.utils2 import extract_mentioned_users, safe_str, safe_int
37 from rhodecode.lib.utils2 import extract_mentioned_users, safe_str, safe_int
38 from rhodecode.model import BaseModel
38 from rhodecode.model import BaseModel
39 from rhodecode.model.db import (
39 from rhodecode.model.db import (
40 ChangesetComment,
40 ChangesetComment,
41 User,
41 User,
42 Notification,
42 Notification,
43 PullRequest,
43 PullRequest,
44 AttributeDict,
44 AttributeDict,
45 ChangesetCommentHistory,
45 ChangesetCommentHistory,
46 )
46 )
47 from rhodecode.model.notification import NotificationModel
47 from rhodecode.model.notification import NotificationModel
48 from rhodecode.model.meta import Session
48 from rhodecode.model.meta import Session
49 from rhodecode.model.settings import VcsSettingsModel
49 from rhodecode.model.settings import VcsSettingsModel
50 from rhodecode.model.notification import EmailNotificationModel
50 from rhodecode.model.notification import EmailNotificationModel
51 from rhodecode.model.validation_schema.schemas import comment_schema
51 from rhodecode.model.validation_schema.schemas import comment_schema
52
52
53
53
54 log = logging.getLogger(__name__)
54 log = logging.getLogger(__name__)
55
55
56
56
57 class CommentsModel(BaseModel):
57 class CommentsModel(BaseModel):
58
58
59 cls = ChangesetComment
59 cls = ChangesetComment
60
60
61 DIFF_CONTEXT_BEFORE = 3
61 DIFF_CONTEXT_BEFORE = 3
62 DIFF_CONTEXT_AFTER = 3
62 DIFF_CONTEXT_AFTER = 3
63
63
64 def __get_commit_comment(self, changeset_comment):
64 def __get_commit_comment(self, changeset_comment):
65 return self._get_instance(ChangesetComment, changeset_comment)
65 return self._get_instance(ChangesetComment, changeset_comment)
66
66
67 def __get_pull_request(self, pull_request):
67 def __get_pull_request(self, pull_request):
68 return self._get_instance(PullRequest, pull_request)
68 return self._get_instance(PullRequest, pull_request)
69
69
70 def _extract_mentions(self, s):
70 def _extract_mentions(self, s):
71 user_objects = []
71 user_objects = []
72 for username in extract_mentioned_users(s):
72 for username in extract_mentioned_users(s):
73 user_obj = User.get_by_username(username, case_insensitive=True)
73 user_obj = User.get_by_username(username, case_insensitive=True)
74 if user_obj:
74 if user_obj:
75 user_objects.append(user_obj)
75 user_objects.append(user_obj)
76 return user_objects
76 return user_objects
77
77
78 def _get_renderer(self, global_renderer='rst', request=None):
78 def _get_renderer(self, global_renderer='rst', request=None):
79 request = request or get_current_request()
79 request = request or get_current_request()
80
80
81 try:
81 try:
82 global_renderer = request.call_context.visual.default_renderer
82 global_renderer = request.call_context.visual.default_renderer
83 except AttributeError:
83 except AttributeError:
84 log.debug("Renderer not set, falling back "
84 log.debug("Renderer not set, falling back "
85 "to default renderer '%s'", global_renderer)
85 "to default renderer '%s'", global_renderer)
86 except Exception:
86 except Exception:
87 log.error(traceback.format_exc())
87 log.error(traceback.format_exc())
88 return global_renderer
88 return global_renderer
89
89
90 def aggregate_comments(self, comments, versions, show_version, inline=False):
90 def aggregate_comments(self, comments, versions, show_version, inline=False):
91 # group by versions, and count until, and display objects
91 # group by versions, and count until, and display objects
92
92
93 comment_groups = collections.defaultdict(list)
93 comment_groups = collections.defaultdict(list)
94 [comment_groups[
94 [comment_groups[
95 _co.pull_request_version_id].append(_co) for _co in comments]
95 _co.pull_request_version_id].append(_co) for _co in comments]
96
96
97 def yield_comments(pos):
97 def yield_comments(pos):
98 for co in comment_groups[pos]:
98 for co in comment_groups[pos]:
99 yield co
99 yield co
100
100
101 comment_versions = collections.defaultdict(
101 comment_versions = collections.defaultdict(
102 lambda: collections.defaultdict(list))
102 lambda: collections.defaultdict(list))
103 prev_prvid = -1
103 prev_prvid = -1
104 # fake last entry with None, to aggregate on "latest" version which
104 # fake last entry with None, to aggregate on "latest" version which
105 # doesn't have an pull_request_version_id
105 # doesn't have an pull_request_version_id
106 for ver in versions + [AttributeDict({'pull_request_version_id': None})]:
106 for ver in versions + [AttributeDict({'pull_request_version_id': None})]:
107 prvid = ver.pull_request_version_id
107 prvid = ver.pull_request_version_id
108 if prev_prvid == -1:
108 if prev_prvid == -1:
109 prev_prvid = prvid
109 prev_prvid = prvid
110
110
111 for co in yield_comments(prvid):
111 for co in yield_comments(prvid):
112 comment_versions[prvid]['at'].append(co)
112 comment_versions[prvid]['at'].append(co)
113
113
114 # save until
114 # save until
115 current = comment_versions[prvid]['at']
115 current = comment_versions[prvid]['at']
116 prev_until = comment_versions[prev_prvid]['until']
116 prev_until = comment_versions[prev_prvid]['until']
117 cur_until = prev_until + current
117 cur_until = prev_until + current
118 comment_versions[prvid]['until'].extend(cur_until)
118 comment_versions[prvid]['until'].extend(cur_until)
119
119
120 # save outdated
120 # save outdated
121 if inline:
121 if inline:
122 outdated = [x for x in cur_until
122 outdated = [x for x in cur_until
123 if x.outdated_at_version(show_version)]
123 if x.outdated_at_version(show_version)]
124 else:
124 else:
125 outdated = [x for x in cur_until
125 outdated = [x for x in cur_until
126 if x.older_than_version(show_version)]
126 if x.older_than_version(show_version)]
127 display = [x for x in cur_until if x not in outdated]
127 display = [x for x in cur_until if x not in outdated]
128
128
129 comment_versions[prvid]['outdated'] = outdated
129 comment_versions[prvid]['outdated'] = outdated
130 comment_versions[prvid]['display'] = display
130 comment_versions[prvid]['display'] = display
131
131
132 prev_prvid = prvid
132 prev_prvid = prvid
133
133
134 return comment_versions
134 return comment_versions
135
135
136 def get_repository_comments(self, repo, comment_type=None, user=None, commit_id=None):
136 def get_repository_comments(self, repo, comment_type=None, user=None, commit_id=None):
137 qry = Session().query(ChangesetComment) \
137 qry = Session().query(ChangesetComment) \
138 .filter(ChangesetComment.repo == repo)
138 .filter(ChangesetComment.repo == repo)
139
139
140 if comment_type and comment_type in ChangesetComment.COMMENT_TYPES:
140 if comment_type and comment_type in ChangesetComment.COMMENT_TYPES:
141 qry = qry.filter(ChangesetComment.comment_type == comment_type)
141 qry = qry.filter(ChangesetComment.comment_type == comment_type)
142
142
143 if user:
143 if user:
144 user = self._get_user(user)
144 user = self._get_user(user)
145 if user:
145 if user:
146 qry = qry.filter(ChangesetComment.user_id == user.user_id)
146 qry = qry.filter(ChangesetComment.user_id == user.user_id)
147
147
148 if commit_id:
148 if commit_id:
149 qry = qry.filter(ChangesetComment.revision == commit_id)
149 qry = qry.filter(ChangesetComment.revision == commit_id)
150
150
151 qry = qry.order_by(ChangesetComment.created_on)
151 qry = qry.order_by(ChangesetComment.created_on)
152 return qry.all()
152 return qry.all()
153
153
154 def get_repository_unresolved_todos(self, repo):
154 def get_repository_unresolved_todos(self, repo):
155 todos = Session().query(ChangesetComment) \
155 todos = Session().query(ChangesetComment) \
156 .filter(ChangesetComment.repo == repo) \
156 .filter(ChangesetComment.repo == repo) \
157 .filter(ChangesetComment.resolved_by == None) \
157 .filter(ChangesetComment.resolved_by == None) \
158 .filter(ChangesetComment.comment_type
158 .filter(ChangesetComment.comment_type
159 == ChangesetComment.COMMENT_TYPE_TODO)
159 == ChangesetComment.COMMENT_TYPE_TODO)
160 todos = todos.all()
160 todos = todos.all()
161
161
162 return todos
162 return todos
163
163
164 def get_pull_request_unresolved_todos(self, pull_request, show_outdated=True):
164 def get_pull_request_unresolved_todos(self, pull_request, show_outdated=True):
165
165
166 todos = Session().query(ChangesetComment) \
166 todos = Session().query(ChangesetComment) \
167 .filter(ChangesetComment.pull_request == pull_request) \
167 .filter(ChangesetComment.pull_request == pull_request) \
168 .filter(ChangesetComment.resolved_by == None) \
168 .filter(ChangesetComment.resolved_by == None) \
169 .filter(ChangesetComment.comment_type
169 .filter(ChangesetComment.comment_type
170 == ChangesetComment.COMMENT_TYPE_TODO)
170 == ChangesetComment.COMMENT_TYPE_TODO)
171
171
172 if not show_outdated:
172 if not show_outdated:
173 todos = todos.filter(
173 todos = todos.filter(
174 coalesce(ChangesetComment.display_state, '') !=
174 coalesce(ChangesetComment.display_state, '') !=
175 ChangesetComment.COMMENT_OUTDATED)
175 ChangesetComment.COMMENT_OUTDATED)
176
176
177 todos = todos.all()
177 todos = todos.all()
178
178
179 return todos
179 return todos
180
180
181 def get_pull_request_resolved_todos(self, pull_request, show_outdated=True):
181 def get_pull_request_resolved_todos(self, pull_request, show_outdated=True):
182
182
183 todos = Session().query(ChangesetComment) \
183 todos = Session().query(ChangesetComment) \
184 .filter(ChangesetComment.pull_request == pull_request) \
184 .filter(ChangesetComment.pull_request == pull_request) \
185 .filter(ChangesetComment.resolved_by != None) \
185 .filter(ChangesetComment.resolved_by != None) \
186 .filter(ChangesetComment.comment_type
186 .filter(ChangesetComment.comment_type
187 == ChangesetComment.COMMENT_TYPE_TODO)
187 == ChangesetComment.COMMENT_TYPE_TODO)
188
188
189 if not show_outdated:
189 if not show_outdated:
190 todos = todos.filter(
190 todos = todos.filter(
191 coalesce(ChangesetComment.display_state, '') !=
191 coalesce(ChangesetComment.display_state, '') !=
192 ChangesetComment.COMMENT_OUTDATED)
192 ChangesetComment.COMMENT_OUTDATED)
193
193
194 todos = todos.all()
194 todos = todos.all()
195
195
196 return todos
196 return todos
197
197
198 def get_commit_unresolved_todos(self, commit_id, show_outdated=True):
198 def get_commit_unresolved_todos(self, commit_id, show_outdated=True):
199
199
200 todos = Session().query(ChangesetComment) \
200 todos = Session().query(ChangesetComment) \
201 .filter(ChangesetComment.revision == commit_id) \
201 .filter(ChangesetComment.revision == commit_id) \
202 .filter(ChangesetComment.resolved_by == None) \
202 .filter(ChangesetComment.resolved_by == None) \
203 .filter(ChangesetComment.comment_type
203 .filter(ChangesetComment.comment_type
204 == ChangesetComment.COMMENT_TYPE_TODO)
204 == ChangesetComment.COMMENT_TYPE_TODO)
205
205
206 if not show_outdated:
206 if not show_outdated:
207 todos = todos.filter(
207 todos = todos.filter(
208 coalesce(ChangesetComment.display_state, '') !=
208 coalesce(ChangesetComment.display_state, '') !=
209 ChangesetComment.COMMENT_OUTDATED)
209 ChangesetComment.COMMENT_OUTDATED)
210
210
211 todos = todos.all()
211 todos = todos.all()
212
212
213 return todos
213 return todos
214
214
215 def get_commit_resolved_todos(self, commit_id, show_outdated=True):
215 def get_commit_resolved_todos(self, commit_id, show_outdated=True):
216
216
217 todos = Session().query(ChangesetComment) \
217 todos = Session().query(ChangesetComment) \
218 .filter(ChangesetComment.revision == commit_id) \
218 .filter(ChangesetComment.revision == commit_id) \
219 .filter(ChangesetComment.resolved_by != None) \
219 .filter(ChangesetComment.resolved_by != None) \
220 .filter(ChangesetComment.comment_type
220 .filter(ChangesetComment.comment_type
221 == ChangesetComment.COMMENT_TYPE_TODO)
221 == ChangesetComment.COMMENT_TYPE_TODO)
222
222
223 if not show_outdated:
223 if not show_outdated:
224 todos = todos.filter(
224 todos = todos.filter(
225 coalesce(ChangesetComment.display_state, '') !=
225 coalesce(ChangesetComment.display_state, '') !=
226 ChangesetComment.COMMENT_OUTDATED)
226 ChangesetComment.COMMENT_OUTDATED)
227
227
228 todos = todos.all()
228 todos = todos.all()
229
229
230 return todos
230 return todos
231
231
232 def _log_audit_action(self, action, action_data, auth_user, comment):
232 def _log_audit_action(self, action, action_data, auth_user, comment):
233 audit_logger.store(
233 audit_logger.store(
234 action=action,
234 action=action,
235 action_data=action_data,
235 action_data=action_data,
236 user=auth_user,
236 user=auth_user,
237 repo=comment.repo)
237 repo=comment.repo)
238
238
239 def create(self, text, repo, user, commit_id=None, pull_request=None,
239 def create(self, text, repo, user, commit_id=None, pull_request=None,
240 f_path=None, line_no=None, status_change=None,
240 f_path=None, line_no=None, status_change=None,
241 status_change_type=None, comment_type=None,
241 status_change_type=None, comment_type=None,
242 resolves_comment_id=None, closing_pr=False, send_email=True,
242 resolves_comment_id=None, closing_pr=False, send_email=True,
243 renderer=None, auth_user=None, extra_recipients=None):
243 renderer=None, auth_user=None, extra_recipients=None):
244 """
244 """
245 Creates new comment for commit or pull request.
245 Creates new comment for commit or pull request.
246 IF status_change is not none this comment is associated with a
246 IF status_change is not none this comment is associated with a
247 status change of commit or commit associated with pull request
247 status change of commit or commit associated with pull request
248
248
249 :param text:
249 :param text:
250 :param repo:
250 :param repo:
251 :param user:
251 :param user:
252 :param commit_id:
252 :param commit_id:
253 :param pull_request:
253 :param pull_request:
254 :param f_path:
254 :param f_path:
255 :param line_no:
255 :param line_no:
256 :param status_change: Label for status change
256 :param status_change: Label for status change
257 :param comment_type: Type of comment
257 :param comment_type: Type of comment
258 :param resolves_comment_id: id of comment which this one will resolve
258 :param resolves_comment_id: id of comment which this one will resolve
259 :param status_change_type: type of status change
259 :param status_change_type: type of status change
260 :param closing_pr:
260 :param closing_pr:
261 :param send_email:
261 :param send_email:
262 :param renderer: pick renderer for this comment
262 :param renderer: pick renderer for this comment
263 :param auth_user: current authenticated user calling this method
263 :param auth_user: current authenticated user calling this method
264 :param extra_recipients: list of extra users to be added to recipients
264 :param extra_recipients: list of extra users to be added to recipients
265 """
265 """
266
266
267 if not text:
267 if not text:
268 log.warning('Missing text for comment, skipping...')
268 log.warning('Missing text for comment, skipping...')
269 return
269 return
270 request = get_current_request()
270 request = get_current_request()
271 _ = request.translate
271 _ = request.translate
272
272
273 if not renderer:
273 if not renderer:
274 renderer = self._get_renderer(request=request)
274 renderer = self._get_renderer(request=request)
275
275
276 repo = self._get_repo(repo)
276 repo = self._get_repo(repo)
277 user = self._get_user(user)
277 user = self._get_user(user)
278 auth_user = auth_user or user
278 auth_user = auth_user or user
279
279
280 schema = comment_schema.CommentSchema()
280 schema = comment_schema.CommentSchema()
281 validated_kwargs = schema.deserialize(dict(
281 validated_kwargs = schema.deserialize(dict(
282 comment_body=text,
282 comment_body=text,
283 comment_type=comment_type,
283 comment_type=comment_type,
284 comment_file=f_path,
284 comment_file=f_path,
285 comment_line=line_no,
285 comment_line=line_no,
286 renderer_type=renderer,
286 renderer_type=renderer,
287 status_change=status_change_type,
287 status_change=status_change_type,
288 resolves_comment_id=resolves_comment_id,
288 resolves_comment_id=resolves_comment_id,
289 repo=repo.repo_id,
289 repo=repo.repo_id,
290 user=user.user_id,
290 user=user.user_id,
291 ))
291 ))
292
292
293 comment = ChangesetComment()
293 comment = ChangesetComment()
294 comment.renderer = validated_kwargs['renderer_type']
294 comment.renderer = validated_kwargs['renderer_type']
295 comment.text = validated_kwargs['comment_body']
295 comment.text = validated_kwargs['comment_body']
296 comment.f_path = validated_kwargs['comment_file']
296 comment.f_path = validated_kwargs['comment_file']
297 comment.line_no = validated_kwargs['comment_line']
297 comment.line_no = validated_kwargs['comment_line']
298 comment.comment_type = validated_kwargs['comment_type']
298 comment.comment_type = validated_kwargs['comment_type']
299
299
300 comment.repo = repo
300 comment.repo = repo
301 comment.author = user
301 comment.author = user
302 resolved_comment = self.__get_commit_comment(
302 resolved_comment = self.__get_commit_comment(
303 validated_kwargs['resolves_comment_id'])
303 validated_kwargs['resolves_comment_id'])
304 # check if the comment actually belongs to this PR
304 # check if the comment actually belongs to this PR
305 if resolved_comment and resolved_comment.pull_request and \
305 if resolved_comment and resolved_comment.pull_request and \
306 resolved_comment.pull_request != pull_request:
306 resolved_comment.pull_request != pull_request:
307 log.warning('Comment tried to resolved unrelated todo comment: %s',
307 log.warning('Comment tried to resolved unrelated todo comment: %s',
308 resolved_comment)
308 resolved_comment)
309 # comment not bound to this pull request, forbid
309 # comment not bound to this pull request, forbid
310 resolved_comment = None
310 resolved_comment = None
311
311
312 elif resolved_comment and resolved_comment.repo and \
312 elif resolved_comment and resolved_comment.repo and \
313 resolved_comment.repo != repo:
313 resolved_comment.repo != repo:
314 log.warning('Comment tried to resolved unrelated todo comment: %s',
314 log.warning('Comment tried to resolved unrelated todo comment: %s',
315 resolved_comment)
315 resolved_comment)
316 # comment not bound to this repo, forbid
316 # comment not bound to this repo, forbid
317 resolved_comment = None
317 resolved_comment = None
318
318
319 comment.resolved_comment = resolved_comment
319 comment.resolved_comment = resolved_comment
320
320
321 pull_request_id = pull_request
321 pull_request_id = pull_request
322
322
323 commit_obj = None
323 commit_obj = None
324 pull_request_obj = None
324 pull_request_obj = None
325
325
326 if commit_id:
326 if commit_id:
327 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
327 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
328 # do a lookup, so we don't pass something bad here
328 # do a lookup, so we don't pass something bad here
329 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
329 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
330 comment.revision = commit_obj.raw_id
330 comment.revision = commit_obj.raw_id
331
331
332 elif pull_request_id:
332 elif pull_request_id:
333 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
333 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
334 pull_request_obj = self.__get_pull_request(pull_request_id)
334 pull_request_obj = self.__get_pull_request(pull_request_id)
335 comment.pull_request = pull_request_obj
335 comment.pull_request = pull_request_obj
336 else:
336 else:
337 raise Exception('Please specify commit or pull_request_id')
337 raise Exception('Please specify commit or pull_request_id')
338
338
339 Session().add(comment)
339 Session().add(comment)
340 Session().flush()
340 Session().flush()
341 kwargs = {
341 kwargs = {
342 'user': user,
342 'user': user,
343 'renderer_type': renderer,
343 'renderer_type': renderer,
344 'repo_name': repo.repo_name,
344 'repo_name': repo.repo_name,
345 'status_change': status_change,
345 'status_change': status_change,
346 'status_change_type': status_change_type,
346 'status_change_type': status_change_type,
347 'comment_body': text,
347 'comment_body': text,
348 'comment_file': f_path,
348 'comment_file': f_path,
349 'comment_line': line_no,
349 'comment_line': line_no,
350 'comment_type': comment_type or 'note',
350 'comment_type': comment_type or 'note',
351 'comment_id': comment.comment_id
351 'comment_id': comment.comment_id
352 }
352 }
353
353
354 if commit_obj:
354 if commit_obj:
355 recipients = ChangesetComment.get_users(
355 recipients = ChangesetComment.get_users(
356 revision=commit_obj.raw_id)
356 revision=commit_obj.raw_id)
357 # add commit author if it's in RhodeCode system
357 # add commit author if it's in RhodeCode system
358 cs_author = User.get_from_cs_author(commit_obj.author)
358 cs_author = User.get_from_cs_author(commit_obj.author)
359 if not cs_author:
359 if not cs_author:
360 # use repo owner if we cannot extract the author correctly
360 # use repo owner if we cannot extract the author correctly
361 cs_author = repo.user
361 cs_author = repo.user
362 recipients += [cs_author]
362 recipients += [cs_author]
363
363
364 commit_comment_url = self.get_url(comment, request=request)
364 commit_comment_url = self.get_url(comment, request=request)
365 commit_comment_reply_url = self.get_url(
365 commit_comment_reply_url = self.get_url(
366 comment, request=request,
366 comment, request=request,
367 anchor='comment-{}/?/ReplyToComment'.format(comment.comment_id))
367 anchor='comment-{}/?/ReplyToComment'.format(comment.comment_id))
368
368
369 target_repo_url = h.link_to(
369 target_repo_url = h.link_to(
370 repo.repo_name,
370 repo.repo_name,
371 h.route_url('repo_summary', repo_name=repo.repo_name))
371 h.route_url('repo_summary', repo_name=repo.repo_name))
372
372
373 commit_url = h.route_url('repo_commit', repo_name=repo.repo_name,
373 commit_url = h.route_url('repo_commit', repo_name=repo.repo_name,
374 commit_id=commit_id)
374 commit_id=commit_id)
375
375
376 # commit specifics
376 # commit specifics
377 kwargs.update({
377 kwargs.update({
378 'commit': commit_obj,
378 'commit': commit_obj,
379 'commit_message': commit_obj.message,
379 'commit_message': commit_obj.message,
380 'commit_target_repo_url': target_repo_url,
380 'commit_target_repo_url': target_repo_url,
381 'commit_comment_url': commit_comment_url,
381 'commit_comment_url': commit_comment_url,
382 'commit_comment_reply_url': commit_comment_reply_url,
382 'commit_comment_reply_url': commit_comment_reply_url,
383 'commit_url': commit_url,
383 'commit_url': commit_url,
384 'thread_ids': [commit_url, commit_comment_url],
384 'thread_ids': [commit_url, commit_comment_url],
385 })
385 })
386
386
387 elif pull_request_obj:
387 elif pull_request_obj:
388 # get the current participants of this pull request
388 # get the current participants of this pull request
389 recipients = ChangesetComment.get_users(
389 recipients = ChangesetComment.get_users(
390 pull_request_id=pull_request_obj.pull_request_id)
390 pull_request_id=pull_request_obj.pull_request_id)
391 # add pull request author
391 # add pull request author
392 recipients += [pull_request_obj.author]
392 recipients += [pull_request_obj.author]
393
393
394 # add the reviewers to notification
394 # add the reviewers to notification
395 recipients += [x.user for x in pull_request_obj.reviewers]
395 recipients += [x.user for x in pull_request_obj.reviewers]
396
396
397 pr_target_repo = pull_request_obj.target_repo
397 pr_target_repo = pull_request_obj.target_repo
398 pr_source_repo = pull_request_obj.source_repo
398 pr_source_repo = pull_request_obj.source_repo
399
399
400 pr_comment_url = self.get_url(comment, request=request)
400 pr_comment_url = self.get_url(comment, request=request)
401 pr_comment_reply_url = self.get_url(
401 pr_comment_reply_url = self.get_url(
402 comment, request=request,
402 comment, request=request,
403 anchor='comment-{}/?/ReplyToComment'.format(comment.comment_id))
403 anchor='comment-{}/?/ReplyToComment'.format(comment.comment_id))
404
404
405 pr_url = h.route_url(
405 pr_url = h.route_url(
406 'pullrequest_show',
406 'pullrequest_show',
407 repo_name=pr_target_repo.repo_name,
407 repo_name=pr_target_repo.repo_name,
408 pull_request_id=pull_request_obj.pull_request_id, )
408 pull_request_id=pull_request_obj.pull_request_id, )
409
409
410 # set some variables for email notification
410 # set some variables for email notification
411 pr_target_repo_url = h.route_url(
411 pr_target_repo_url = h.route_url(
412 'repo_summary', repo_name=pr_target_repo.repo_name)
412 'repo_summary', repo_name=pr_target_repo.repo_name)
413
413
414 pr_source_repo_url = h.route_url(
414 pr_source_repo_url = h.route_url(
415 'repo_summary', repo_name=pr_source_repo.repo_name)
415 'repo_summary', repo_name=pr_source_repo.repo_name)
416
416
417 # pull request specifics
417 # pull request specifics
418 kwargs.update({
418 kwargs.update({
419 'pull_request': pull_request_obj,
419 'pull_request': pull_request_obj,
420 'pr_id': pull_request_obj.pull_request_id,
420 'pr_id': pull_request_obj.pull_request_id,
421 'pull_request_url': pr_url,
421 'pull_request_url': pr_url,
422 'pull_request_target_repo': pr_target_repo,
422 'pull_request_target_repo': pr_target_repo,
423 'pull_request_target_repo_url': pr_target_repo_url,
423 'pull_request_target_repo_url': pr_target_repo_url,
424 'pull_request_source_repo': pr_source_repo,
424 'pull_request_source_repo': pr_source_repo,
425 'pull_request_source_repo_url': pr_source_repo_url,
425 'pull_request_source_repo_url': pr_source_repo_url,
426 'pr_comment_url': pr_comment_url,
426 'pr_comment_url': pr_comment_url,
427 'pr_comment_reply_url': pr_comment_reply_url,
427 'pr_comment_reply_url': pr_comment_reply_url,
428 'pr_closing': closing_pr,
428 'pr_closing': closing_pr,
429 'thread_ids': [pr_url, pr_comment_url],
429 'thread_ids': [pr_url, pr_comment_url],
430 })
430 })
431
431
432 recipients += [self._get_user(u) for u in (extra_recipients or [])]
432 recipients += [self._get_user(u) for u in (extra_recipients or [])]
433
433
434 if send_email:
434 if send_email:
435 # pre-generate the subject for notification itself
435 # pre-generate the subject for notification itself
436 (subject, _e, body_plaintext) = EmailNotificationModel().render_email(
436 (subject, _e, body_plaintext) = EmailNotificationModel().render_email(
437 notification_type, **kwargs)
437 notification_type, **kwargs)
438
438
439 mention_recipients = set(
439 mention_recipients = set(
440 self._extract_mentions(text)).difference(recipients)
440 self._extract_mentions(text)).difference(recipients)
441
441
442 # create notification objects, and emails
442 # create notification objects, and emails
443 NotificationModel().create(
443 NotificationModel().create(
444 created_by=user,
444 created_by=user,
445 notification_subject=subject,
445 notification_subject=subject,
446 notification_body=body_plaintext,
446 notification_body=body_plaintext,
447 notification_type=notification_type,
447 notification_type=notification_type,
448 recipients=recipients,
448 recipients=recipients,
449 mention_recipients=mention_recipients,
449 mention_recipients=mention_recipients,
450 email_kwargs=kwargs,
450 email_kwargs=kwargs,
451 )
451 )
452
452
453 Session().flush()
453 Session().flush()
454 if comment.pull_request:
454 if comment.pull_request:
455 action = 'repo.pull_request.comment.create'
455 action = 'repo.pull_request.comment.create'
456 else:
456 else:
457 action = 'repo.commit.comment.create'
457 action = 'repo.commit.comment.create'
458
458
459 comment_data = comment.get_api_data()
459 comment_data = comment.get_api_data()
460 self._log_audit_action(
460 self._log_audit_action(
461 action, {'data': comment_data}, auth_user, comment)
461 action, {'data': comment_data}, auth_user, comment)
462
462
463 msg_url = ''
463 msg_url = ''
464 channel = None
464 channel = None
465 if commit_obj:
465 if commit_obj:
466 msg_url = commit_comment_url
466 msg_url = commit_comment_url
467 repo_name = repo.repo_name
467 repo_name = repo.repo_name
468 channel = u'/repo${}$/commit/{}'.format(
468 channel = u'/repo${}$/commit/{}'.format(
469 repo_name,
469 repo_name,
470 commit_obj.raw_id
470 commit_obj.raw_id
471 )
471 )
472 elif pull_request_obj:
472 elif pull_request_obj:
473 msg_url = pr_comment_url
473 msg_url = pr_comment_url
474 repo_name = pr_target_repo.repo_name
474 repo_name = pr_target_repo.repo_name
475 channel = u'/repo${}$/pr/{}'.format(
475 channel = u'/repo${}$/pr/{}'.format(
476 repo_name,
476 repo_name,
477 pull_request_id
477 pull_request_id
478 )
478 )
479
479
480 message = '<strong>{}</strong> {} - ' \
480 message = '<strong>{}</strong> {} - ' \
481 '<a onclick="window.location=\'{}\';' \
481 '<a onclick="window.location=\'{}\';' \
482 'window.location.reload()">' \
482 'window.location.reload()">' \
483 '<strong>{}</strong></a>'
483 '<strong>{}</strong></a>'
484 message = message.format(
484 message = message.format(
485 user.username, _('made a comment'), msg_url,
485 user.username, _('made a comment'), msg_url,
486 _('Show it now'))
486 _('Show it now'))
487
487
488 channelstream.post_message(
488 channelstream.post_message(
489 channel, message, user.username,
489 channel, message, user.username,
490 registry=get_current_registry())
490 registry=get_current_registry())
491
491
492 return comment
492 return comment
493
493
494 def edit(self, comment_id, text, auth_user, version):
494 def edit(self, comment_id, text, auth_user, version):
495 """
495 """
496 Change existing comment for commit or pull request.
496 Change existing comment for commit or pull request.
497
497
498 :param comment_id:
498 :param comment_id:
499 :param text:
499 :param text:
500 :param auth_user: current authenticated user calling this method
500 :param auth_user: current authenticated user calling this method
501 :param version: last comment version
501 :param version: last comment version
502 """
502 """
503 if not text:
503 if not text:
504 log.warning('Missing text for comment, skipping...')
504 log.warning('Missing text for comment, skipping...')
505 return
505 return
506
506
507 comment = ChangesetComment.get(comment_id)
507 comment = ChangesetComment.get(comment_id)
508 old_comment_text = comment.text
508 old_comment_text = comment.text
509 comment.text = text
509 comment.text = text
510 comment.modified_at = datetime.datetime.now()
510 comment.modified_at = datetime.datetime.now()
511 version = safe_int(version)
511 version = safe_int(version)
512
512
513 # NOTE(marcink): this returns initial comment + edits, so v2 from ui
513 # NOTE(marcink): this returns initial comment + edits, so v2 from ui
514 # would return 3 here
514 # would return 3 here
515 comment_version = ChangesetCommentHistory.get_version(comment_id)
515 comment_version = ChangesetCommentHistory.get_version(comment_id)
516
516
517 if isinstance(version, (int, long)) and (comment_version - version) != 1:
517 if isinstance(version, (int, long)) and (comment_version - version) != 1:
518 log.warning(
518 log.warning(
519 'Version mismatch comment_version {} submitted {}, skipping'.format(
519 'Version mismatch comment_version {} submitted {}, skipping'.format(
520 comment_version-1, # -1 since note above
520 comment_version-1, # -1 since note above
521 version
521 version
522 )
522 )
523 )
523 )
524 raise CommentVersionMismatch()
524 raise CommentVersionMismatch()
525
525
526 comment_history = ChangesetCommentHistory()
526 comment_history = ChangesetCommentHistory()
527 comment_history.comment_id = comment_id
527 comment_history.comment_id = comment_id
528 comment_history.version = comment_version
528 comment_history.version = comment_version
529 comment_history.created_by_user_id = auth_user.user_id
529 comment_history.created_by_user_id = auth_user.user_id
530 comment_history.text = old_comment_text
530 comment_history.text = old_comment_text
531 # TODO add email notification
531 # TODO add email notification
532 Session().add(comment_history)
532 Session().add(comment_history)
533 Session().add(comment)
533 Session().add(comment)
534 Session().flush()
534 Session().flush()
535
535
536 if comment.pull_request:
536 if comment.pull_request:
537 action = 'repo.pull_request.comment.edit'
537 action = 'repo.pull_request.comment.edit'
538 else:
538 else:
539 action = 'repo.commit.comment.edit'
539 action = 'repo.commit.comment.edit'
540
540
541 comment_data = comment.get_api_data()
541 comment_data = comment.get_api_data()
542 comment_data['old_comment_text'] = old_comment_text
542 comment_data['old_comment_text'] = old_comment_text
543 self._log_audit_action(
543 self._log_audit_action(
544 action, {'data': comment_data}, auth_user, comment)
544 action, {'data': comment_data}, auth_user, comment)
545
545
546 return comment_history
546 return comment_history
547
547
548 def delete(self, comment, auth_user):
548 def delete(self, comment, auth_user):
549 """
549 """
550 Deletes given comment
550 Deletes given comment
551 """
551 """
552 comment = self.__get_commit_comment(comment)
552 comment = self.__get_commit_comment(comment)
553 old_data = comment.get_api_data()
553 old_data = comment.get_api_data()
554 Session().delete(comment)
554 Session().delete(comment)
555
555
556 if comment.pull_request:
556 if comment.pull_request:
557 action = 'repo.pull_request.comment.delete'
557 action = 'repo.pull_request.comment.delete'
558 else:
558 else:
559 action = 'repo.commit.comment.delete'
559 action = 'repo.commit.comment.delete'
560
560
561 self._log_audit_action(
561 self._log_audit_action(
562 action, {'old_data': old_data}, auth_user, comment)
562 action, {'old_data': old_data}, auth_user, comment)
563
563
564 return comment
564 return comment
565
565
566 def get_all_comments(self, repo_id, revision=None, pull_request=None):
566 def get_all_comments(self, repo_id, revision=None, pull_request=None):
567 q = ChangesetComment.query()\
567 q = ChangesetComment.query()\
568 .filter(ChangesetComment.repo_id == repo_id)
568 .filter(ChangesetComment.repo_id == repo_id)
569 if revision:
569 if revision:
570 q = q.filter(ChangesetComment.revision == revision)
570 q = q.filter(ChangesetComment.revision == revision)
571 elif pull_request:
571 elif pull_request:
572 pull_request = self.__get_pull_request(pull_request)
572 pull_request = self.__get_pull_request(pull_request)
573 q = q.filter(ChangesetComment.pull_request == pull_request)
573 q = q.filter(ChangesetComment.pull_request == pull_request)
574 else:
574 else:
575 raise Exception('Please specify commit or pull_request')
575 raise Exception('Please specify commit or pull_request')
576 q = q.order_by(ChangesetComment.created_on)
576 q = q.order_by(ChangesetComment.created_on)
577 return q.all()
577 return q.all()
578
578
579 def get_url(self, comment, request=None, permalink=False, anchor=None):
579 def get_url(self, comment, request=None, permalink=False, anchor=None):
580 if not request:
580 if not request:
581 request = get_current_request()
581 request = get_current_request()
582
582
583 comment = self.__get_commit_comment(comment)
583 comment = self.__get_commit_comment(comment)
584 if anchor is None:
584 if anchor is None:
585 anchor = 'comment-{}'.format(comment.comment_id)
585 anchor = 'comment-{}'.format(comment.comment_id)
586
586
587 if comment.pull_request:
587 if comment.pull_request:
588 pull_request = comment.pull_request
588 pull_request = comment.pull_request
589 if permalink:
589 if permalink:
590 return request.route_url(
590 return request.route_url(
591 'pull_requests_global',
591 'pull_requests_global',
592 pull_request_id=pull_request.pull_request_id,
592 pull_request_id=pull_request.pull_request_id,
593 _anchor=anchor)
593 _anchor=anchor)
594 else:
594 else:
595 return request.route_url(
595 return request.route_url(
596 'pullrequest_show',
596 'pullrequest_show',
597 repo_name=safe_str(pull_request.target_repo.repo_name),
597 repo_name=safe_str(pull_request.target_repo.repo_name),
598 pull_request_id=pull_request.pull_request_id,
598 pull_request_id=pull_request.pull_request_id,
599 _anchor=anchor)
599 _anchor=anchor)
600
600
601 else:
601 else:
602 repo = comment.repo
602 repo = comment.repo
603 commit_id = comment.revision
603 commit_id = comment.revision
604
604
605 if permalink:
605 if permalink:
606 return request.route_url(
606 return request.route_url(
607 'repo_commit', repo_name=safe_str(repo.repo_id),
607 'repo_commit', repo_name=safe_str(repo.repo_id),
608 commit_id=commit_id,
608 commit_id=commit_id,
609 _anchor=anchor)
609 _anchor=anchor)
610
610
611 else:
611 else:
612 return request.route_url(
612 return request.route_url(
613 'repo_commit', repo_name=safe_str(repo.repo_name),
613 'repo_commit', repo_name=safe_str(repo.repo_name),
614 commit_id=commit_id,
614 commit_id=commit_id,
615 _anchor=anchor)
615 _anchor=anchor)
616
616
617 def get_comments(self, repo_id, revision=None, pull_request=None):
617 def get_comments(self, repo_id, revision=None, pull_request=None):
618 """
618 """
619 Gets main comments based on revision or pull_request_id
619 Gets main comments based on revision or pull_request_id
620
620
621 :param repo_id:
621 :param repo_id:
622 :param revision:
622 :param revision:
623 :param pull_request:
623 :param pull_request:
624 """
624 """
625
625
626 q = ChangesetComment.query()\
626 q = ChangesetComment.query()\
627 .filter(ChangesetComment.repo_id == repo_id)\
627 .filter(ChangesetComment.repo_id == repo_id)\
628 .filter(ChangesetComment.line_no == None)\
628 .filter(ChangesetComment.line_no == None)\
629 .filter(ChangesetComment.f_path == None)
629 .filter(ChangesetComment.f_path == None)
630 if revision:
630 if revision:
631 q = q.filter(ChangesetComment.revision == revision)
631 q = q.filter(ChangesetComment.revision == revision)
632 elif pull_request:
632 elif pull_request:
633 pull_request = self.__get_pull_request(pull_request)
633 pull_request = self.__get_pull_request(pull_request)
634 q = q.filter(ChangesetComment.pull_request == pull_request)
634 q = q.filter(ChangesetComment.pull_request == pull_request)
635 else:
635 else:
636 raise Exception('Please specify commit or pull_request')
636 raise Exception('Please specify commit or pull_request')
637 q = q.order_by(ChangesetComment.created_on)
637 q = q.order_by(ChangesetComment.created_on)
638 return q.all()
638 return q.all()
639
639
640 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
640 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
641 q = self._get_inline_comments_query(repo_id, revision, pull_request)
641 q = self._get_inline_comments_query(repo_id, revision, pull_request)
642 return self._group_comments_by_path_and_line_number(q)
642 return self._group_comments_by_path_and_line_number(q)
643
643
644 def get_inline_comments_count(self, inline_comments, skip_outdated=True,
644 def get_inline_comments_as_list(self, inline_comments, skip_outdated=True,
645 version=None):
645 version=None):
646 inline_cnt = 0
646 inline_cnt = 0
647 for fname, per_line_comments in inline_comments.iteritems():
647 for fname, per_line_comments in inline_comments.iteritems():
648 for lno, comments in per_line_comments.iteritems():
648 for lno, comments in per_line_comments.iteritems():
649 for comm in comments:
649 for comm in comments:
650 if not comm.outdated_at_version(version) and skip_outdated:
650 if not comm.outdated_at_version(version) and skip_outdated:
651 inline_cnt += 1
651 inline_cnt += 1
652
652
653 return inline_cnt
653 return inline_cnt
654
654
655 def get_outdated_comments(self, repo_id, pull_request):
655 def get_outdated_comments(self, repo_id, pull_request):
656 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
656 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
657 # of a pull request.
657 # of a pull request.
658 q = self._all_inline_comments_of_pull_request(pull_request)
658 q = self._all_inline_comments_of_pull_request(pull_request)
659 q = q.filter(
659 q = q.filter(
660 ChangesetComment.display_state ==
660 ChangesetComment.display_state ==
661 ChangesetComment.COMMENT_OUTDATED
661 ChangesetComment.COMMENT_OUTDATED
662 ).order_by(ChangesetComment.comment_id.asc())
662 ).order_by(ChangesetComment.comment_id.asc())
663
663
664 return self._group_comments_by_path_and_line_number(q)
664 return self._group_comments_by_path_and_line_number(q)
665
665
666 def _get_inline_comments_query(self, repo_id, revision, pull_request):
666 def _get_inline_comments_query(self, repo_id, revision, pull_request):
667 # TODO: johbo: Split this into two methods: One for PR and one for
667 # TODO: johbo: Split this into two methods: One for PR and one for
668 # commit.
668 # commit.
669 if revision:
669 if revision:
670 q = Session().query(ChangesetComment).filter(
670 q = Session().query(ChangesetComment).filter(
671 ChangesetComment.repo_id == repo_id,
671 ChangesetComment.repo_id == repo_id,
672 ChangesetComment.line_no != null(),
672 ChangesetComment.line_no != null(),
673 ChangesetComment.f_path != null(),
673 ChangesetComment.f_path != null(),
674 ChangesetComment.revision == revision)
674 ChangesetComment.revision == revision)
675
675
676 elif pull_request:
676 elif pull_request:
677 pull_request = self.__get_pull_request(pull_request)
677 pull_request = self.__get_pull_request(pull_request)
678 if not CommentsModel.use_outdated_comments(pull_request):
678 if not CommentsModel.use_outdated_comments(pull_request):
679 q = self._visible_inline_comments_of_pull_request(pull_request)
679 q = self._visible_inline_comments_of_pull_request(pull_request)
680 else:
680 else:
681 q = self._all_inline_comments_of_pull_request(pull_request)
681 q = self._all_inline_comments_of_pull_request(pull_request)
682
682
683 else:
683 else:
684 raise Exception('Please specify commit or pull_request_id')
684 raise Exception('Please specify commit or pull_request_id')
685 q = q.order_by(ChangesetComment.comment_id.asc())
685 q = q.order_by(ChangesetComment.comment_id.asc())
686 return q
686 return q
687
687
688 def _group_comments_by_path_and_line_number(self, q):
688 def _group_comments_by_path_and_line_number(self, q):
689 comments = q.all()
689 comments = q.all()
690 paths = collections.defaultdict(lambda: collections.defaultdict(list))
690 paths = collections.defaultdict(lambda: collections.defaultdict(list))
691 for co in comments:
691 for co in comments:
692 paths[co.f_path][co.line_no].append(co)
692 paths[co.f_path][co.line_no].append(co)
693 return paths
693 return paths
694
694
695 @classmethod
695 @classmethod
696 def needed_extra_diff_context(cls):
696 def needed_extra_diff_context(cls):
697 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
697 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
698
698
699 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
699 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
700 if not CommentsModel.use_outdated_comments(pull_request):
700 if not CommentsModel.use_outdated_comments(pull_request):
701 return
701 return
702
702
703 comments = self._visible_inline_comments_of_pull_request(pull_request)
703 comments = self._visible_inline_comments_of_pull_request(pull_request)
704 comments_to_outdate = comments.all()
704 comments_to_outdate = comments.all()
705
705
706 for comment in comments_to_outdate:
706 for comment in comments_to_outdate:
707 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
707 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
708
708
709 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
709 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
710 diff_line = _parse_comment_line_number(comment.line_no)
710 diff_line = _parse_comment_line_number(comment.line_no)
711
711
712 try:
712 try:
713 old_context = old_diff_proc.get_context_of_line(
713 old_context = old_diff_proc.get_context_of_line(
714 path=comment.f_path, diff_line=diff_line)
714 path=comment.f_path, diff_line=diff_line)
715 new_context = new_diff_proc.get_context_of_line(
715 new_context = new_diff_proc.get_context_of_line(
716 path=comment.f_path, diff_line=diff_line)
716 path=comment.f_path, diff_line=diff_line)
717 except (diffs.LineNotInDiffException,
717 except (diffs.LineNotInDiffException,
718 diffs.FileNotInDiffException):
718 diffs.FileNotInDiffException):
719 comment.display_state = ChangesetComment.COMMENT_OUTDATED
719 comment.display_state = ChangesetComment.COMMENT_OUTDATED
720 return
720 return
721
721
722 if old_context == new_context:
722 if old_context == new_context:
723 return
723 return
724
724
725 if self._should_relocate_diff_line(diff_line):
725 if self._should_relocate_diff_line(diff_line):
726 new_diff_lines = new_diff_proc.find_context(
726 new_diff_lines = new_diff_proc.find_context(
727 path=comment.f_path, context=old_context,
727 path=comment.f_path, context=old_context,
728 offset=self.DIFF_CONTEXT_BEFORE)
728 offset=self.DIFF_CONTEXT_BEFORE)
729 if not new_diff_lines:
729 if not new_diff_lines:
730 comment.display_state = ChangesetComment.COMMENT_OUTDATED
730 comment.display_state = ChangesetComment.COMMENT_OUTDATED
731 else:
731 else:
732 new_diff_line = self._choose_closest_diff_line(
732 new_diff_line = self._choose_closest_diff_line(
733 diff_line, new_diff_lines)
733 diff_line, new_diff_lines)
734 comment.line_no = _diff_to_comment_line_number(new_diff_line)
734 comment.line_no = _diff_to_comment_line_number(new_diff_line)
735 else:
735 else:
736 comment.display_state = ChangesetComment.COMMENT_OUTDATED
736 comment.display_state = ChangesetComment.COMMENT_OUTDATED
737
737
738 def _should_relocate_diff_line(self, diff_line):
738 def _should_relocate_diff_line(self, diff_line):
739 """
739 """
740 Checks if relocation shall be tried for the given `diff_line`.
740 Checks if relocation shall be tried for the given `diff_line`.
741
741
742 If a comment points into the first lines, then we can have a situation
742 If a comment points into the first lines, then we can have a situation
743 that after an update another line has been added on top. In this case
743 that after an update another line has been added on top. In this case
744 we would find the context still and move the comment around. This
744 we would find the context still and move the comment around. This
745 would be wrong.
745 would be wrong.
746 """
746 """
747 should_relocate = (
747 should_relocate = (
748 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
748 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
749 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
749 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
750 return should_relocate
750 return should_relocate
751
751
752 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
752 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
753 candidate = new_diff_lines[0]
753 candidate = new_diff_lines[0]
754 best_delta = _diff_line_delta(diff_line, candidate)
754 best_delta = _diff_line_delta(diff_line, candidate)
755 for new_diff_line in new_diff_lines[1:]:
755 for new_diff_line in new_diff_lines[1:]:
756 delta = _diff_line_delta(diff_line, new_diff_line)
756 delta = _diff_line_delta(diff_line, new_diff_line)
757 if delta < best_delta:
757 if delta < best_delta:
758 candidate = new_diff_line
758 candidate = new_diff_line
759 best_delta = delta
759 best_delta = delta
760 return candidate
760 return candidate
761
761
762 def _visible_inline_comments_of_pull_request(self, pull_request):
762 def _visible_inline_comments_of_pull_request(self, pull_request):
763 comments = self._all_inline_comments_of_pull_request(pull_request)
763 comments = self._all_inline_comments_of_pull_request(pull_request)
764 comments = comments.filter(
764 comments = comments.filter(
765 coalesce(ChangesetComment.display_state, '') !=
765 coalesce(ChangesetComment.display_state, '') !=
766 ChangesetComment.COMMENT_OUTDATED)
766 ChangesetComment.COMMENT_OUTDATED)
767 return comments
767 return comments
768
768
769 def _all_inline_comments_of_pull_request(self, pull_request):
769 def _all_inline_comments_of_pull_request(self, pull_request):
770 comments = Session().query(ChangesetComment)\
770 comments = Session().query(ChangesetComment)\
771 .filter(ChangesetComment.line_no != None)\
771 .filter(ChangesetComment.line_no != None)\
772 .filter(ChangesetComment.f_path != None)\
772 .filter(ChangesetComment.f_path != None)\
773 .filter(ChangesetComment.pull_request == pull_request)
773 .filter(ChangesetComment.pull_request == pull_request)
774 return comments
774 return comments
775
775
776 def _all_general_comments_of_pull_request(self, pull_request):
776 def _all_general_comments_of_pull_request(self, pull_request):
777 comments = Session().query(ChangesetComment)\
777 comments = Session().query(ChangesetComment)\
778 .filter(ChangesetComment.line_no == None)\
778 .filter(ChangesetComment.line_no == None)\
779 .filter(ChangesetComment.f_path == None)\
779 .filter(ChangesetComment.f_path == None)\
780 .filter(ChangesetComment.pull_request == pull_request)
780 .filter(ChangesetComment.pull_request == pull_request)
781
781
782 return comments
782 return comments
783
783
784 @staticmethod
784 @staticmethod
785 def use_outdated_comments(pull_request):
785 def use_outdated_comments(pull_request):
786 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
786 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
787 settings = settings_model.get_general_settings()
787 settings = settings_model.get_general_settings()
788 return settings.get('rhodecode_use_outdated_comments', False)
788 return settings.get('rhodecode_use_outdated_comments', False)
789
789
790 def trigger_commit_comment_hook(self, repo, user, action, data=None):
790 def trigger_commit_comment_hook(self, repo, user, action, data=None):
791 repo = self._get_repo(repo)
791 repo = self._get_repo(repo)
792 target_scm = repo.scm_instance()
792 target_scm = repo.scm_instance()
793 if action == 'create':
793 if action == 'create':
794 trigger_hook = hooks_utils.trigger_comment_commit_hooks
794 trigger_hook = hooks_utils.trigger_comment_commit_hooks
795 elif action == 'edit':
795 elif action == 'edit':
796 trigger_hook = hooks_utils.trigger_comment_commit_edit_hooks
796 trigger_hook = hooks_utils.trigger_comment_commit_edit_hooks
797 else:
797 else:
798 return
798 return
799
799
800 log.debug('Handling repo %s trigger_commit_comment_hook with action %s: %s',
800 log.debug('Handling repo %s trigger_commit_comment_hook with action %s: %s',
801 repo, action, trigger_hook)
801 repo, action, trigger_hook)
802 trigger_hook(
802 trigger_hook(
803 username=user.username,
803 username=user.username,
804 repo_name=repo.repo_name,
804 repo_name=repo.repo_name,
805 repo_type=target_scm.alias,
805 repo_type=target_scm.alias,
806 repo=repo,
806 repo=repo,
807 data=data)
807 data=data)
808
808
809
809
810 def _parse_comment_line_number(line_no):
810 def _parse_comment_line_number(line_no):
811 """
811 """
812 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
812 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
813 """
813 """
814 old_line = None
814 old_line = None
815 new_line = None
815 new_line = None
816 if line_no.startswith('o'):
816 if line_no.startswith('o'):
817 old_line = int(line_no[1:])
817 old_line = int(line_no[1:])
818 elif line_no.startswith('n'):
818 elif line_no.startswith('n'):
819 new_line = int(line_no[1:])
819 new_line = int(line_no[1:])
820 else:
820 else:
821 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
821 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
822 return diffs.DiffLineNumber(old_line, new_line)
822 return diffs.DiffLineNumber(old_line, new_line)
823
823
824
824
825 def _diff_to_comment_line_number(diff_line):
825 def _diff_to_comment_line_number(diff_line):
826 if diff_line.new is not None:
826 if diff_line.new is not None:
827 return u'n{}'.format(diff_line.new)
827 return u'n{}'.format(diff_line.new)
828 elif diff_line.old is not None:
828 elif diff_line.old is not None:
829 return u'o{}'.format(diff_line.old)
829 return u'o{}'.format(diff_line.old)
830 return u''
830 return u''
831
831
832
832
833 def _diff_line_delta(a, b):
833 def _diff_line_delta(a, b):
834 if None not in (a.new, b.new):
834 if None not in (a.new, b.new):
835 return abs(a.new - b.new)
835 return abs(a.new - b.new)
836 elif None not in (a.old, b.old):
836 elif None not in (a.old, b.old):
837 return abs(a.old - b.old)
837 return abs(a.old - b.old)
838 else:
838 else:
839 raise ValueError(
839 raise ValueError(
840 "Cannot compute delta between {} and {}".format(a, b))
840 "Cannot compute delta between {} and {}".format(a, b))
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now