##// END OF EJS Templates
release: merge back stable branch into default
marcink -
r4469:9d632087 merge default
parent child Browse files
Show More

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

@@ -0,0 +1,42 b''
1 |RCE| 4.20.1 |RNS|
2 ------------------
3
4 Release Date
5 ^^^^^^^^^^^^
6
7 - 2020-07-27
8
9
10 New Features
11 ^^^^^^^^^^^^
12
13
14
15 General
16 ^^^^^^^
17
18 - Permissions: rename write+ to write or higher for more explicit meaning.
19
20
21 Security
22 ^^^^^^^^
23
24
25
26 Performance
27 ^^^^^^^^^^^
28
29
30
31 Fixes
32 ^^^^^
33
34 - Files: fixed creation of new files for empty repos.
35 - Notifications: properly inject the custom email headers into templates.
36 - Store file integration: fixed support for nested subdirs.
37
38
39 Upgrade notes
40 ^^^^^^^^^^^^^
41
42 - Un-scheduled release addressing problems in 4.20.X releases.
@@ -1,68 +1,70 b''
1 1 1bd3e92b7e2e2d2024152b34bb88dff1db544a71 v4.0.0
2 2 170c5398320ea6cddd50955e88d408794c21d43a v4.0.1
3 3 c3fe200198f5aa34cf2e4066df2881a9cefe3704 v4.1.0
4 4 7fd5c850745e2ea821fb4406af5f4bff9b0a7526 v4.1.1
5 5 41c87da28a179953df86061d817bc35533c66dd2 v4.1.2
6 6 baaf9f5bcea3bae0ef12ae20c8b270482e62abb6 v4.2.0
7 7 32a70c7e56844a825f61df496ee5eaf8c3c4e189 v4.2.1
8 8 fa695cdb411d294679ac081d595ac654e5613b03 v4.3.0
9 9 0e4dc11b58cad833c513fe17bac39e6850edf959 v4.3.1
10 10 8a876f48f5cb1d018b837db28ff928500cb32cfb v4.4.0
11 11 8dd86b410b1aac086ffdfc524ef300f896af5047 v4.4.1
12 12 d2514226abc8d3b4f6fb57765f47d1b6fb360a05 v4.4.2
13 13 27d783325930af6dad2741476c0d0b1b7c8415c2 v4.5.0
14 14 7f2016f352abcbdba4a19d4039c386e9629449da v4.5.1
15 15 416fec799314c70a5c780fb28b3357b08869333a v4.5.2
16 16 27c3b85fafc83143e6678fbc3da69e1615bcac55 v4.6.0
17 17 5ad13deb9118c2a5243d4032d4d9cc174e5872db v4.6.1
18 18 2be921e01fa24bb102696ada596f87464c3666f6 v4.7.0
19 19 7198bdec29c2872c974431d55200d0398354cdb1 v4.7.1
20 20 bd1c8d230fe741c2dfd7100a0ef39fd0774fd581 v4.7.2
21 21 9731914f89765d9628dc4dddc84bc9402aa124c8 v4.8.0
22 22 c5a2b7d0e4bbdebc4a62d7b624befe375207b659 v4.9.0
23 23 d9aa3b27ac9f7e78359775c75fedf7bfece232f1 v4.9.1
24 24 4ba4d74981cec5d6b28b158f875a2540952c2f74 v4.10.0
25 25 0a6821cbd6b0b3c21503002f88800679fa35ab63 v4.10.1
26 26 434ad90ec8d621f4416074b84f6e9ce03964defb v4.10.2
27 27 68baee10e698da2724c6e0f698c03a6abb993bf2 v4.10.3
28 28 00821d3afd1dce3f4767cc353f84a17f7d5218a1 v4.10.4
29 29 22f6744ad8cc274311825f63f953e4dee2ea5cb9 v4.10.5
30 30 96eb24bea2f5f9258775245e3f09f6fa0a4dda01 v4.10.6
31 31 3121217a812c956d7dd5a5875821bd73e8002a32 v4.11.0
32 32 fa98b454715ac5b912f39e84af54345909a2a805 v4.11.1
33 33 3982abcfdcc229a723cebe52d3a9bcff10bba08e v4.11.2
34 34 33195f145db9172f0a8f1487e09207178a6ab065 v4.11.3
35 35 194c74f33e32bbae6fc4d71ec5a999cff3c13605 v4.11.4
36 36 8fbd8b0c3ddc2fa4ac9e4ca16942a03eb593df2d v4.11.5
37 37 f0609aa5d5d05a1ca2f97c3995542236131c9d8a v4.11.6
38 38 b5b30547d90d2e088472a70c84878f429ffbf40d v4.12.0
39 39 9072253aa8894d20c00b4a43dc61c2168c1eff94 v4.12.1
40 40 6a517543ea9ef9987d74371bd2a315eb0b232dc9 v4.12.2
41 41 7fc0731b024c3114be87865eda7ab621cc957e32 v4.12.3
42 42 6d531c0b068c6eda62dddceedc9f845ecb6feb6f v4.12.4
43 43 3d6bf2d81b1564830eb5e83396110d2a9a93eb1e v4.13.0
44 44 5468fc89e708bd90e413cd0d54350017abbdbc0e v4.13.1
45 45 610d621550521c314ee97b3d43473ac0bcf06fb8 v4.13.2
46 46 7dc62c090881fb5d03268141e71e0940d7c3295d v4.13.3
47 47 9151328c1c46b72ba6f00d7640d9141e75aa1ca2 v4.14.0
48 48 a47eeac5dfa41fa6779d90452affba4091c3ade8 v4.14.1
49 49 4b34ce0d2c3c10510626b3b65044939bb7a2cddf v4.15.0
50 50 14502561d22e6b70613674cd675ae9a604b7989f v4.15.1
51 51 4aaa40b605b01af78a9f6882eca561c54b525ef0 v4.15.2
52 52 797744642eca86640ed20bef2cd77445780abaec v4.16.0
53 53 6c3452c7c25ed35ff269690929e11960ed6ad7d3 v4.16.1
54 54 5d8057df561c4b6b81b6401aed7d2f911e6e77f7 v4.16.2
55 55 13acfc008896ef4c62546bab5074e8f6f89b4fa7 v4.17.0
56 56 45b9b610976f483877142fe75321808ce9ebac59 v4.17.1
57 57 ad5bd0c4bd322fdbd04bb825a3d027e08f7a3901 v4.17.2
58 58 037f5794b55a6236d68f6485a485372dde6566e0 v4.17.3
59 59 83bc3100cfd6094c1d04f475ddb299b7dc3d0b33 v4.17.4
60 60 e3de8c95baf8cc9109ca56aee8193a2cb6a54c8a v4.17.4
61 61 f37a3126570477543507f0bc9d245ce75546181a v4.18.0
62 62 71d8791463e87b64c1a18475de330ee600d37561 v4.18.1
63 63 4bd6b75dac1d25c64885d4d49385e5533f21c525 v4.18.2
64 64 12ed92fe57f2e9fc7b71dc0b65e26c2da5c7085f v4.18.3
65 65 ddef396a6567117de531d67d44c739cbbfc3eebb v4.19.0
66 66 c0c65acd73914bf4368222d510afe1161ab8c07c v4.19.1
67 67 7ac623a4a2405917e2af660d645ded662011e40d v4.19.2
68 68 ef7ffda65eeb90c3ba88590a6cb816ef9b0bc232 v4.19.3
69 3e635489bb7961df93b01e42454ad1a8730ae968 v4.20.0
70 7e2eb896a02ca7cd2cd9f0f853ef3dac3f0039e3 v4.20.1
@@ -1,145 +1,146 b''
1 1 .. _rhodecode-release-notes-ref:
2 2
3 3 Release Notes
4 4 =============
5 5
6 6 |RCE| 4.x Versions
7 7 ------------------
8 8
9 9 .. toctree::
10 10 :maxdepth: 1
11 11
12 release-notes-4.20.1.rst
12 13 release-notes-4.20.0.rst
13 14 release-notes-4.19.3.rst
14 15 release-notes-4.19.2.rst
15 16 release-notes-4.19.1.rst
16 17 release-notes-4.19.0.rst
17 18 release-notes-4.18.3.rst
18 19 release-notes-4.18.2.rst
19 20 release-notes-4.18.1.rst
20 21 release-notes-4.18.0.rst
21 22 release-notes-4.17.4.rst
22 23 release-notes-4.17.3.rst
23 24 release-notes-4.17.2.rst
24 25 release-notes-4.17.1.rst
25 26 release-notes-4.17.0.rst
26 27 release-notes-4.16.2.rst
27 28 release-notes-4.16.1.rst
28 29 release-notes-4.16.0.rst
29 30 release-notes-4.15.2.rst
30 31 release-notes-4.15.1.rst
31 32 release-notes-4.15.0.rst
32 33 release-notes-4.14.1.rst
33 34 release-notes-4.14.0.rst
34 35 release-notes-4.13.3.rst
35 36 release-notes-4.13.2.rst
36 37 release-notes-4.13.1.rst
37 38 release-notes-4.13.0.rst
38 39 release-notes-4.12.4.rst
39 40 release-notes-4.12.3.rst
40 41 release-notes-4.12.2.rst
41 42 release-notes-4.12.1.rst
42 43 release-notes-4.12.0.rst
43 44 release-notes-4.11.6.rst
44 45 release-notes-4.11.5.rst
45 46 release-notes-4.11.4.rst
46 47 release-notes-4.11.3.rst
47 48 release-notes-4.11.2.rst
48 49 release-notes-4.11.1.rst
49 50 release-notes-4.11.0.rst
50 51 release-notes-4.10.6.rst
51 52 release-notes-4.10.5.rst
52 53 release-notes-4.10.4.rst
53 54 release-notes-4.10.3.rst
54 55 release-notes-4.10.2.rst
55 56 release-notes-4.10.1.rst
56 57 release-notes-4.10.0.rst
57 58 release-notes-4.9.1.rst
58 59 release-notes-4.9.0.rst
59 60 release-notes-4.8.0.rst
60 61 release-notes-4.7.2.rst
61 62 release-notes-4.7.1.rst
62 63 release-notes-4.7.0.rst
63 64 release-notes-4.6.1.rst
64 65 release-notes-4.6.0.rst
65 66 release-notes-4.5.2.rst
66 67 release-notes-4.5.1.rst
67 68 release-notes-4.5.0.rst
68 69 release-notes-4.4.2.rst
69 70 release-notes-4.4.1.rst
70 71 release-notes-4.4.0.rst
71 72 release-notes-4.3.1.rst
72 73 release-notes-4.3.0.rst
73 74 release-notes-4.2.1.rst
74 75 release-notes-4.2.0.rst
75 76 release-notes-4.1.2.rst
76 77 release-notes-4.1.1.rst
77 78 release-notes-4.1.0.rst
78 79 release-notes-4.0.1.rst
79 80 release-notes-4.0.0.rst
80 81
81 82 |RCE| 3.x Versions
82 83 ------------------
83 84
84 85 .. toctree::
85 86 :maxdepth: 1
86 87
87 88 release-notes-3.8.4.rst
88 89 release-notes-3.8.3.rst
89 90 release-notes-3.8.2.rst
90 91 release-notes-3.8.1.rst
91 92 release-notes-3.8.0.rst
92 93 release-notes-3.7.1.rst
93 94 release-notes-3.7.0.rst
94 95 release-notes-3.6.1.rst
95 96 release-notes-3.6.0.rst
96 97 release-notes-3.5.2.rst
97 98 release-notes-3.5.1.rst
98 99 release-notes-3.5.0.rst
99 100 release-notes-3.4.1.rst
100 101 release-notes-3.4.0.rst
101 102 release-notes-3.3.4.rst
102 103 release-notes-3.3.3.rst
103 104 release-notes-3.3.2.rst
104 105 release-notes-3.3.1.rst
105 106 release-notes-3.3.0.rst
106 107 release-notes-3.2.3.rst
107 108 release-notes-3.2.2.rst
108 109 release-notes-3.2.1.rst
109 110 release-notes-3.2.0.rst
110 111 release-notes-3.1.1.rst
111 112 release-notes-3.1.0.rst
112 113 release-notes-3.0.2.rst
113 114 release-notes-3.0.1.rst
114 115 release-notes-3.0.0.rst
115 116
116 117 |RCE| 2.x Versions
117 118 ------------------
118 119
119 120 .. toctree::
120 121 :maxdepth: 1
121 122
122 123 release-notes-2.2.8.rst
123 124 release-notes-2.2.7.rst
124 125 release-notes-2.2.6.rst
125 126 release-notes-2.2.5.rst
126 127 release-notes-2.2.4.rst
127 128 release-notes-2.2.3.rst
128 129 release-notes-2.2.2.rst
129 130 release-notes-2.2.1.rst
130 131 release-notes-2.2.0.rst
131 132 release-notes-2.1.0.rst
132 133 release-notes-2.0.2.rst
133 134 release-notes-2.0.1.rst
134 135 release-notes-2.0.0.rst
135 136
136 137 |RCE| 1.x Versions
137 138 ------------------
138 139
139 140 .. toctree::
140 141 :maxdepth: 1
141 142
142 143 release-notes-1.7.2.rst
143 144 release-notes-1.7.1.rst
144 145 release-notes-1.7.0.rst
145 146 release-notes-1.6.0.rst
@@ -1,1618 +1,1631 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import itertools
22 22 import logging
23 23 import os
24 24 import shutil
25 25 import tempfile
26 26 import collections
27 27 import urllib
28 28 import pathlib2
29 29
30 30 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPFound
31 31 from pyramid.view import view_config
32 32 from pyramid.renderers import render
33 33 from pyramid.response import Response
34 34
35 35 import rhodecode
36 36 from rhodecode.apps._base import RepoAppView
37 37
38 38
39 39 from rhodecode.lib import diffs, helpers as h, rc_cache
40 40 from rhodecode.lib import audit_logger
41 41 from rhodecode.lib.view_utils import parse_path_ref
42 42 from rhodecode.lib.exceptions import NonRelativePathError
43 43 from rhodecode.lib.codeblocks import (
44 44 filenode_as_lines_tokens, filenode_as_annotated_lines_tokens)
45 45 from rhodecode.lib.utils2 import (
46 46 convert_line_endings, detect_mode, safe_str, str2bool, safe_int, sha1, safe_unicode)
47 47 from rhodecode.lib.auth import (
48 48 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
49 49 from rhodecode.lib.vcs import path as vcspath
50 50 from rhodecode.lib.vcs.backends.base import EmptyCommit
51 51 from rhodecode.lib.vcs.conf import settings
52 52 from rhodecode.lib.vcs.nodes import FileNode
53 53 from rhodecode.lib.vcs.exceptions import (
54 54 RepositoryError, CommitDoesNotExistError, EmptyRepositoryError,
55 55 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError,
56 56 NodeDoesNotExistError, CommitError, NodeError)
57 57
58 58 from rhodecode.model.scm import ScmModel
59 59 from rhodecode.model.db import Repository
60 60
61 61 log = logging.getLogger(__name__)
62 62
63 63
64 64 class RepoFilesView(RepoAppView):
65 65
66 66 @staticmethod
67 67 def adjust_file_path_for_svn(f_path, repo):
68 68 """
69 69 Computes the relative path of `f_path`.
70 70
71 71 This is mainly based on prefix matching of the recognized tags and
72 72 branches in the underlying repository.
73 73 """
74 74 tags_and_branches = itertools.chain(
75 75 repo.branches.iterkeys(),
76 76 repo.tags.iterkeys())
77 77 tags_and_branches = sorted(tags_and_branches, key=len, reverse=True)
78 78
79 79 for name in tags_and_branches:
80 80 if f_path.startswith('{}/'.format(name)):
81 81 f_path = vcspath.relpath(f_path, name)
82 82 break
83 83 return f_path
84 84
85 85 def load_default_context(self):
86 86 c = self._get_local_tmpl_context(include_app_defaults=True)
87 87 c.rhodecode_repo = self.rhodecode_vcs_repo
88 88 c.enable_downloads = self.db_repo.enable_downloads
89 89 return c
90 90
91 91 def _ensure_not_locked(self, commit_id='tip'):
92 92 _ = self.request.translate
93 93
94 94 repo = self.db_repo
95 95 if repo.enable_locking and repo.locked[0]:
96 96 h.flash(_('This repository has been locked by %s on %s')
97 97 % (h.person_by_id(repo.locked[0]),
98 98 h.format_date(h.time_to_datetime(repo.locked[1]))),
99 99 'warning')
100 100 files_url = h.route_path(
101 101 'repo_files:default_path',
102 102 repo_name=self.db_repo_name, commit_id=commit_id)
103 103 raise HTTPFound(files_url)
104 104
105 105 def forbid_non_head(self, is_head, f_path, commit_id='tip', json_mode=False):
106 106 _ = self.request.translate
107 107
108 108 if not is_head:
109 109 message = _('Cannot modify file. '
110 110 'Given commit `{}` is not head of a branch.').format(commit_id)
111 111 h.flash(message, category='warning')
112 112
113 113 if json_mode:
114 114 return message
115 115
116 116 files_url = h.route_path(
117 117 'repo_files', repo_name=self.db_repo_name, commit_id=commit_id,
118 118 f_path=f_path)
119 119 raise HTTPFound(files_url)
120 120
121 121 def check_branch_permission(self, branch_name, commit_id='tip', json_mode=False):
122 122 _ = self.request.translate
123 123
124 124 rule, branch_perm = self._rhodecode_user.get_rule_and_branch_permission(
125 125 self.db_repo_name, branch_name)
126 126 if branch_perm and branch_perm not in ['branch.push', 'branch.push_force']:
127 127 message = _('Branch `{}` changes forbidden by rule {}.').format(
128 128 h.escape(branch_name), h.escape(rule))
129 129 h.flash(message, 'warning')
130 130
131 131 if json_mode:
132 132 return message
133 133
134 134 files_url = h.route_path(
135 135 'repo_files:default_path', repo_name=self.db_repo_name, commit_id=commit_id)
136 136
137 137 raise HTTPFound(files_url)
138 138
139 139 def _get_commit_and_path(self):
140 140 default_commit_id = self.db_repo.landing_ref_name
141 141 default_f_path = '/'
142 142
143 143 commit_id = self.request.matchdict.get(
144 144 'commit_id', default_commit_id)
145 145 f_path = self._get_f_path(self.request.matchdict, default_f_path)
146 146 return commit_id, f_path
147 147
148 148 def _get_default_encoding(self, c):
149 149 enc_list = getattr(c, 'default_encodings', [])
150 150 return enc_list[0] if enc_list else 'UTF-8'
151 151
152 152 def _get_commit_or_redirect(self, commit_id, redirect_after=True):
153 153 """
154 154 This is a safe way to get commit. If an error occurs it redirects to
155 155 tip with proper message
156 156
157 157 :param commit_id: id of commit to fetch
158 158 :param redirect_after: toggle redirection
159 159 """
160 160 _ = self.request.translate
161 161
162 162 try:
163 163 return self.rhodecode_vcs_repo.get_commit(commit_id)
164 164 except EmptyRepositoryError:
165 165 if not redirect_after:
166 166 return None
167 167
168 168 _url = h.route_path(
169 169 'repo_files_add_file',
170 170 repo_name=self.db_repo_name, commit_id=0, f_path='')
171 171
172 172 if h.HasRepoPermissionAny(
173 173 'repository.write', 'repository.admin')(self.db_repo_name):
174 174 add_new = h.link_to(
175 175 _('Click here to add a new file.'), _url, class_="alert-link")
176 176 else:
177 177 add_new = ""
178 178
179 179 h.flash(h.literal(
180 180 _('There are no files yet. %s') % add_new), category='warning')
181 181 raise HTTPFound(
182 182 h.route_path('repo_summary', repo_name=self.db_repo_name))
183 183
184 184 except (CommitDoesNotExistError, LookupError) as e:
185 185 msg = _('No such commit exists for this repository. Commit: {}').format(commit_id)
186 186 h.flash(msg, category='error')
187 187 raise HTTPNotFound()
188 188 except RepositoryError as e:
189 189 h.flash(safe_str(h.escape(e)), category='error')
190 190 raise HTTPNotFound()
191 191
192 192 def _get_filenode_or_redirect(self, commit_obj, path):
193 193 """
194 194 Returns file_node, if error occurs or given path is directory,
195 195 it'll redirect to top level path
196 196 """
197 197 _ = self.request.translate
198 198
199 199 try:
200 200 file_node = commit_obj.get_node(path)
201 201 if file_node.is_dir():
202 202 raise RepositoryError('The given path is a directory')
203 203 except CommitDoesNotExistError:
204 204 log.exception('No such commit exists for this repository')
205 205 h.flash(_('No such commit exists for this repository'), category='error')
206 206 raise HTTPNotFound()
207 207 except RepositoryError as e:
208 208 log.warning('Repository error while fetching filenode `%s`. Err:%s', path, e)
209 209 h.flash(safe_str(h.escape(e)), category='error')
210 210 raise HTTPNotFound()
211 211
212 212 return file_node
213 213
214 def _is_valid_head(self, commit_id, repo):
214 def _is_valid_head(self, commit_id, repo, landing_ref):
215 215 branch_name = sha_commit_id = ''
216 216 is_head = False
217 217 log.debug('Checking if commit_id `%s` is a head for %s.', commit_id, repo)
218 218
219 219 for _branch_name, branch_commit_id in repo.branches.items():
220 220 # simple case we pass in branch name, it's a HEAD
221 221 if commit_id == _branch_name:
222 222 is_head = True
223 223 branch_name = _branch_name
224 224 sha_commit_id = branch_commit_id
225 225 break
226 226 # case when we pass in full sha commit_id, which is a head
227 227 elif commit_id == branch_commit_id:
228 228 is_head = True
229 229 branch_name = _branch_name
230 230 sha_commit_id = branch_commit_id
231 231 break
232 232
233 233 if h.is_svn(repo) and not repo.is_empty():
234 234 # Note: Subversion only has one head.
235 235 if commit_id == repo.get_commit(commit_idx=-1).raw_id:
236 236 is_head = True
237 237 return branch_name, sha_commit_id, is_head
238 238
239 239 # checked branches, means we only need to try to get the branch/commit_sha
240 if not repo.is_empty():
240 if repo.is_empty():
241 is_head = True
242 branch_name = landing_ref
243 sha_commit_id = EmptyCommit().raw_id
244 else:
241 245 commit = repo.get_commit(commit_id=commit_id)
242 246 if commit:
243 247 branch_name = commit.branch
244 248 sha_commit_id = commit.raw_id
245 249
246 250 return branch_name, sha_commit_id, is_head
247 251
248 252 def _get_tree_at_commit(self, c, commit_id, f_path, full_load=False, at_rev=None):
249 253
250 254 repo_id = self.db_repo.repo_id
251 255 force_recache = self.get_recache_flag()
252 256
253 257 cache_seconds = safe_int(
254 258 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
255 259 cache_on = not force_recache and cache_seconds > 0
256 260 log.debug(
257 261 'Computing FILE TREE for repo_id %s commit_id `%s` and path `%s`'
258 262 'with caching: %s[TTL: %ss]' % (
259 263 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
260 264
261 265 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
262 266 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
263 267
264 268 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid, condition=cache_on)
265 269 def compute_file_tree(ver, _name_hash, _repo_id, _commit_id, _f_path, _full_load, _at_rev):
266 270 log.debug('Generating cached file tree at ver:%s for repo_id: %s, %s, %s',
267 271 ver, _repo_id, _commit_id, _f_path)
268 272
269 273 c.full_load = _full_load
270 274 return render(
271 275 'rhodecode:templates/files/files_browser_tree.mako',
272 276 self._get_template_context(c), self.request, _at_rev)
273 277
274 278 return compute_file_tree(
275 279 rc_cache.FILE_TREE_CACHE_VER, self.db_repo.repo_name_hash,
276 280 self.db_repo.repo_id, commit_id, f_path, full_load, at_rev)
277 281
278 282 def _get_archive_spec(self, fname):
279 283 log.debug('Detecting archive spec for: `%s`', fname)
280 284
281 285 fileformat = None
282 286 ext = None
283 287 content_type = None
284 288 for a_type, content_type, extension in settings.ARCHIVE_SPECS:
285 289
286 290 if fname.endswith(extension):
287 291 fileformat = a_type
288 292 log.debug('archive is of type: %s', fileformat)
289 293 ext = extension
290 294 break
291 295
292 296 if not fileformat:
293 297 raise ValueError()
294 298
295 299 # left over part of whole fname is the commit
296 300 commit_id = fname[:-len(ext)]
297 301
298 302 return commit_id, ext, fileformat, content_type
299 303
300 304 def create_pure_path(self, *parts):
301 305 # Split paths and sanitize them, removing any ../ etc
302 306 sanitized_path = [
303 307 x for x in pathlib2.PurePath(*parts).parts
304 308 if x not in ['.', '..']]
305 309
306 310 pure_path = pathlib2.PurePath(*sanitized_path)
307 311 return pure_path
308 312
309 313 def _is_lf_enabled(self, target_repo):
310 314 lf_enabled = False
311 315
312 316 lf_key_for_vcs_map = {
313 317 'hg': 'extensions_largefiles',
314 318 'git': 'vcs_git_lfs_enabled'
315 319 }
316 320
317 321 lf_key_for_vcs = lf_key_for_vcs_map.get(target_repo.repo_type)
318 322
319 323 if lf_key_for_vcs:
320 324 lf_enabled = self._get_repo_setting(target_repo, lf_key_for_vcs)
321 325
322 326 return lf_enabled
323 327
324 328 @LoginRequired()
325 329 @HasRepoPermissionAnyDecorator(
326 330 'repository.read', 'repository.write', 'repository.admin')
327 331 @view_config(
328 332 route_name='repo_archivefile', request_method='GET',
329 333 renderer=None)
330 334 def repo_archivefile(self):
331 335 # archive cache config
332 336 from rhodecode import CONFIG
333 337 _ = self.request.translate
334 338 self.load_default_context()
335 339 default_at_path = '/'
336 340 fname = self.request.matchdict['fname']
337 341 subrepos = self.request.GET.get('subrepos') == 'true'
338 342 at_path = self.request.GET.get('at_path') or default_at_path
339 343
340 344 if not self.db_repo.enable_downloads:
341 345 return Response(_('Downloads disabled'))
342 346
343 347 try:
344 348 commit_id, ext, fileformat, content_type = \
345 349 self._get_archive_spec(fname)
346 350 except ValueError:
347 351 return Response(_('Unknown archive type for: `{}`').format(
348 352 h.escape(fname)))
349 353
350 354 try:
351 355 commit = self.rhodecode_vcs_repo.get_commit(commit_id)
352 356 except CommitDoesNotExistError:
353 357 return Response(_('Unknown commit_id {}').format(
354 358 h.escape(commit_id)))
355 359 except EmptyRepositoryError:
356 360 return Response(_('Empty repository'))
357 361
358 362 try:
359 363 at_path = commit.get_node(at_path).path or default_at_path
360 364 except Exception:
361 365 return Response(_('No node at path {} for this repository').format(at_path))
362 366
363 367 path_sha = sha1(at_path)[:8]
364 368
365 369 # original backward compat name of archive
366 370 clean_name = safe_str(self.db_repo_name.replace('/', '_'))
367 371 short_sha = safe_str(commit.short_id)
368 372
369 373 if at_path == default_at_path:
370 374 archive_name = '{}-{}{}{}'.format(
371 375 clean_name,
372 376 '-sub' if subrepos else '',
373 377 short_sha,
374 378 ext)
375 379 # custom path and new name
376 380 else:
377 381 archive_name = '{}-{}{}-{}{}'.format(
378 382 clean_name,
379 383 '-sub' if subrepos else '',
380 384 short_sha,
381 385 path_sha,
382 386 ext)
383 387
384 388 use_cached_archive = False
385 389 archive_cache_enabled = CONFIG.get(
386 390 'archive_cache_dir') and not self.request.GET.get('no_cache')
387 391 cached_archive_path = None
388 392
389 393 if archive_cache_enabled:
390 394 # check if we it's ok to write
391 395 if not os.path.isdir(CONFIG['archive_cache_dir']):
392 396 os.makedirs(CONFIG['archive_cache_dir'])
393 397 cached_archive_path = os.path.join(
394 398 CONFIG['archive_cache_dir'], archive_name)
395 399 if os.path.isfile(cached_archive_path):
396 400 log.debug('Found cached archive in %s', cached_archive_path)
397 401 fd, archive = None, cached_archive_path
398 402 use_cached_archive = True
399 403 else:
400 404 log.debug('Archive %s is not yet cached', archive_name)
401 405
402 406 if not use_cached_archive:
403 407 # generate new archive
404 408 fd, archive = tempfile.mkstemp()
405 409 log.debug('Creating new temp archive in %s', archive)
406 410 try:
407 411 commit.archive_repo(archive, kind=fileformat, subrepos=subrepos,
408 412 archive_at_path=at_path)
409 413 except ImproperArchiveTypeError:
410 414 return _('Unknown archive type')
411 415 if archive_cache_enabled:
412 416 # if we generated the archive and we have cache enabled
413 417 # let's use this for future
414 418 log.debug('Storing new archive in %s', cached_archive_path)
415 419 shutil.move(archive, cached_archive_path)
416 420 archive = cached_archive_path
417 421
418 422 # store download action
419 423 audit_logger.store_web(
420 424 'repo.archive.download', action_data={
421 425 'user_agent': self.request.user_agent,
422 426 'archive_name': archive_name,
423 427 'archive_spec': fname,
424 428 'archive_cached': use_cached_archive},
425 429 user=self._rhodecode_user,
426 430 repo=self.db_repo,
427 431 commit=True
428 432 )
429 433
430 434 def get_chunked_archive(archive_path):
431 435 with open(archive_path, 'rb') as stream:
432 436 while True:
433 437 data = stream.read(16 * 1024)
434 438 if not data:
435 439 if fd: # fd means we used temporary file
436 440 os.close(fd)
437 441 if not archive_cache_enabled:
438 442 log.debug('Destroying temp archive %s', archive_path)
439 443 os.remove(archive_path)
440 444 break
441 445 yield data
442 446
443 447 response = Response(app_iter=get_chunked_archive(archive))
444 448 response.content_disposition = str(
445 449 'attachment; filename=%s' % archive_name)
446 450 response.content_type = str(content_type)
447 451
448 452 return response
449 453
450 454 def _get_file_node(self, commit_id, f_path):
451 455 if commit_id not in ['', None, 'None', '0' * 12, '0' * 40]:
452 456 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
453 457 try:
454 458 node = commit.get_node(f_path)
455 459 if node.is_dir():
456 460 raise NodeError('%s path is a %s not a file'
457 461 % (node, type(node)))
458 462 except NodeDoesNotExistError:
459 463 commit = EmptyCommit(
460 464 commit_id=commit_id,
461 465 idx=commit.idx,
462 466 repo=commit.repository,
463 467 alias=commit.repository.alias,
464 468 message=commit.message,
465 469 author=commit.author,
466 470 date=commit.date)
467 471 node = FileNode(f_path, '', commit=commit)
468 472 else:
469 473 commit = EmptyCommit(
470 474 repo=self.rhodecode_vcs_repo,
471 475 alias=self.rhodecode_vcs_repo.alias)
472 476 node = FileNode(f_path, '', commit=commit)
473 477 return node
474 478
475 479 @LoginRequired()
476 480 @HasRepoPermissionAnyDecorator(
477 481 'repository.read', 'repository.write', 'repository.admin')
478 482 @view_config(
479 483 route_name='repo_files_diff', request_method='GET',
480 484 renderer=None)
481 485 def repo_files_diff(self):
482 486 c = self.load_default_context()
483 487 f_path = self._get_f_path(self.request.matchdict)
484 488 diff1 = self.request.GET.get('diff1', '')
485 489 diff2 = self.request.GET.get('diff2', '')
486 490
487 491 path1, diff1 = parse_path_ref(diff1, default_path=f_path)
488 492
489 493 ignore_whitespace = str2bool(self.request.GET.get('ignorews'))
490 494 line_context = self.request.GET.get('context', 3)
491 495
492 496 if not any((diff1, diff2)):
493 497 h.flash(
494 498 'Need query parameter "diff1" or "diff2" to generate a diff.',
495 499 category='error')
496 500 raise HTTPBadRequest()
497 501
498 502 c.action = self.request.GET.get('diff')
499 503 if c.action not in ['download', 'raw']:
500 504 compare_url = h.route_path(
501 505 'repo_compare',
502 506 repo_name=self.db_repo_name,
503 507 source_ref_type='rev',
504 508 source_ref=diff1,
505 509 target_repo=self.db_repo_name,
506 510 target_ref_type='rev',
507 511 target_ref=diff2,
508 512 _query=dict(f_path=f_path))
509 513 # redirect to new view if we render diff
510 514 raise HTTPFound(compare_url)
511 515
512 516 try:
513 517 node1 = self._get_file_node(diff1, path1)
514 518 node2 = self._get_file_node(diff2, f_path)
515 519 except (RepositoryError, NodeError):
516 520 log.exception("Exception while trying to get node from repository")
517 521 raise HTTPFound(
518 522 h.route_path('repo_files', repo_name=self.db_repo_name,
519 523 commit_id='tip', f_path=f_path))
520 524
521 525 if all(isinstance(node.commit, EmptyCommit)
522 526 for node in (node1, node2)):
523 527 raise HTTPNotFound()
524 528
525 529 c.commit_1 = node1.commit
526 530 c.commit_2 = node2.commit
527 531
528 532 if c.action == 'download':
529 533 _diff = diffs.get_gitdiff(node1, node2,
530 534 ignore_whitespace=ignore_whitespace,
531 535 context=line_context)
532 536 diff = diffs.DiffProcessor(_diff, format='gitdiff')
533 537
534 538 response = Response(self.path_filter.get_raw_patch(diff))
535 539 response.content_type = 'text/plain'
536 540 response.content_disposition = (
537 541 'attachment; filename=%s_%s_vs_%s.diff' % (f_path, diff1, diff2)
538 542 )
539 543 charset = self._get_default_encoding(c)
540 544 if charset:
541 545 response.charset = charset
542 546 return response
543 547
544 548 elif c.action == 'raw':
545 549 _diff = diffs.get_gitdiff(node1, node2,
546 550 ignore_whitespace=ignore_whitespace,
547 551 context=line_context)
548 552 diff = diffs.DiffProcessor(_diff, format='gitdiff')
549 553
550 554 response = Response(self.path_filter.get_raw_patch(diff))
551 555 response.content_type = 'text/plain'
552 556 charset = self._get_default_encoding(c)
553 557 if charset:
554 558 response.charset = charset
555 559 return response
556 560
557 561 # in case we ever end up here
558 562 raise HTTPNotFound()
559 563
560 564 @LoginRequired()
561 565 @HasRepoPermissionAnyDecorator(
562 566 'repository.read', 'repository.write', 'repository.admin')
563 567 @view_config(
564 568 route_name='repo_files_diff_2way_redirect', request_method='GET',
565 569 renderer=None)
566 570 def repo_files_diff_2way_redirect(self):
567 571 """
568 572 Kept only to make OLD links work
569 573 """
570 574 f_path = self._get_f_path_unchecked(self.request.matchdict)
571 575 diff1 = self.request.GET.get('diff1', '')
572 576 diff2 = self.request.GET.get('diff2', '')
573 577
574 578 if not any((diff1, diff2)):
575 579 h.flash(
576 580 'Need query parameter "diff1" or "diff2" to generate a diff.',
577 581 category='error')
578 582 raise HTTPBadRequest()
579 583
580 584 compare_url = h.route_path(
581 585 'repo_compare',
582 586 repo_name=self.db_repo_name,
583 587 source_ref_type='rev',
584 588 source_ref=diff1,
585 589 target_ref_type='rev',
586 590 target_ref=diff2,
587 591 _query=dict(f_path=f_path, diffmode='sideside',
588 592 target_repo=self.db_repo_name,))
589 593 raise HTTPFound(compare_url)
590 594
591 595 @LoginRequired()
592 596 @view_config(
593 597 route_name='repo_files:default_commit', request_method='GET',
594 598 renderer=None)
595 599 def repo_files_default(self):
596 600 c = self.load_default_context()
597 601 ref_name = c.rhodecode_db_repo.landing_ref_name
598 602 landing_url = h.repo_files_by_ref_url(
599 603 c.rhodecode_db_repo.repo_name,
600 604 c.rhodecode_db_repo.repo_type,
601 605 f_path='',
602 606 ref_name=ref_name,
603 607 commit_id='tip',
604 608 query=dict(at=ref_name)
605 609 )
606 610
607 611 raise HTTPFound(landing_url)
608 612
609 613 @LoginRequired()
610 614 @HasRepoPermissionAnyDecorator(
611 615 'repository.read', 'repository.write', 'repository.admin')
612 616 @view_config(
613 617 route_name='repo_files', request_method='GET',
614 618 renderer=None)
615 619 @view_config(
616 620 route_name='repo_files:default_path', request_method='GET',
617 621 renderer=None)
618 622 @view_config(
619 623 route_name='repo_files:rendered', request_method='GET',
620 624 renderer=None)
621 625 @view_config(
622 626 route_name='repo_files:annotated', request_method='GET',
623 627 renderer=None)
624 628 def repo_files(self):
625 629 c = self.load_default_context()
626 630
627 631 view_name = getattr(self.request.matched_route, 'name', None)
628 632
629 633 c.annotate = view_name == 'repo_files:annotated'
630 634 # default is false, but .rst/.md files later are auto rendered, we can
631 635 # overwrite auto rendering by setting this GET flag
632 636 c.renderer = view_name == 'repo_files:rendered' or \
633 637 not self.request.GET.get('no-render', False)
634 638
635 639 commit_id, f_path = self._get_commit_and_path()
636 640
637 641 c.commit = self._get_commit_or_redirect(commit_id)
638 642 c.branch = self.request.GET.get('branch', None)
639 643 c.f_path = f_path
640 644 at_rev = self.request.GET.get('at')
641 645
642 646 # prev link
643 647 try:
644 648 prev_commit = c.commit.prev(c.branch)
645 649 c.prev_commit = prev_commit
646 650 c.url_prev = h.route_path(
647 651 'repo_files', repo_name=self.db_repo_name,
648 652 commit_id=prev_commit.raw_id, f_path=f_path)
649 653 if c.branch:
650 654 c.url_prev += '?branch=%s' % c.branch
651 655 except (CommitDoesNotExistError, VCSError):
652 656 c.url_prev = '#'
653 657 c.prev_commit = EmptyCommit()
654 658
655 659 # next link
656 660 try:
657 661 next_commit = c.commit.next(c.branch)
658 662 c.next_commit = next_commit
659 663 c.url_next = h.route_path(
660 664 'repo_files', repo_name=self.db_repo_name,
661 665 commit_id=next_commit.raw_id, f_path=f_path)
662 666 if c.branch:
663 667 c.url_next += '?branch=%s' % c.branch
664 668 except (CommitDoesNotExistError, VCSError):
665 669 c.url_next = '#'
666 670 c.next_commit = EmptyCommit()
667 671
668 672 # files or dirs
669 673 try:
670 674 c.file = c.commit.get_node(f_path)
671 675 c.file_author = True
672 676 c.file_tree = ''
673 677
674 678 # load file content
675 679 if c.file.is_file():
676 680 c.lf_node = {}
677 681
678 682 has_lf_enabled = self._is_lf_enabled(self.db_repo)
679 683 if has_lf_enabled:
680 684 c.lf_node = c.file.get_largefile_node()
681 685
682 686 c.file_source_page = 'true'
683 687 c.file_last_commit = c.file.last_commit
684 688
685 689 c.file_size_too_big = c.file.size > c.visual.cut_off_limit_file
686 690
687 691 if not (c.file_size_too_big or c.file.is_binary):
688 692 if c.annotate: # annotation has precedence over renderer
689 693 c.annotated_lines = filenode_as_annotated_lines_tokens(
690 694 c.file
691 695 )
692 696 else:
693 697 c.renderer = (
694 698 c.renderer and h.renderer_from_filename(c.file.path)
695 699 )
696 700 if not c.renderer:
697 701 c.lines = filenode_as_lines_tokens(c.file)
698 702
699 _branch_name, _sha_commit_id, is_head = self._is_valid_head(
700 commit_id, self.rhodecode_vcs_repo)
703 _branch_name, _sha_commit_id, is_head = \
704 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
705 landing_ref=self.db_repo.landing_ref_name)
701 706 c.on_branch_head = is_head
702 707
703 708 branch = c.commit.branch if (
704 709 c.commit.branch and '/' not in c.commit.branch) else None
705 710 c.branch_or_raw_id = branch or c.commit.raw_id
706 711 c.branch_name = c.commit.branch or h.short_id(c.commit.raw_id)
707 712
708 713 author = c.file_last_commit.author
709 714 c.authors = [[
710 715 h.email(author),
711 716 h.person(author, 'username_or_name_or_email'),
712 717 1
713 718 ]]
714 719
715 720 else: # load tree content at path
716 721 c.file_source_page = 'false'
717 722 c.authors = []
718 723 # this loads a simple tree without metadata to speed things up
719 724 # later via ajax we call repo_nodetree_full and fetch whole
720 725 c.file_tree = self._get_tree_at_commit(c, c.commit.raw_id, f_path, at_rev=at_rev)
721 726
722 727 c.readme_data, c.readme_file = \
723 728 self._get_readme_data(self.db_repo, c.visual.default_renderer,
724 729 c.commit.raw_id, f_path)
725 730
726 731 except RepositoryError as e:
727 732 h.flash(safe_str(h.escape(e)), category='error')
728 733 raise HTTPNotFound()
729 734
730 735 if self.request.environ.get('HTTP_X_PJAX'):
731 736 html = render('rhodecode:templates/files/files_pjax.mako',
732 737 self._get_template_context(c), self.request)
733 738 else:
734 739 html = render('rhodecode:templates/files/files.mako',
735 740 self._get_template_context(c), self.request)
736 741 return Response(html)
737 742
738 743 @HasRepoPermissionAnyDecorator(
739 744 'repository.read', 'repository.write', 'repository.admin')
740 745 @view_config(
741 746 route_name='repo_files:annotated_previous', request_method='GET',
742 747 renderer=None)
743 748 def repo_files_annotated_previous(self):
744 749 self.load_default_context()
745 750
746 751 commit_id, f_path = self._get_commit_and_path()
747 752 commit = self._get_commit_or_redirect(commit_id)
748 753 prev_commit_id = commit.raw_id
749 754 line_anchor = self.request.GET.get('line_anchor')
750 755 is_file = False
751 756 try:
752 757 _file = commit.get_node(f_path)
753 758 is_file = _file.is_file()
754 759 except (NodeDoesNotExistError, CommitDoesNotExistError, VCSError):
755 760 pass
756 761
757 762 if is_file:
758 763 history = commit.get_path_history(f_path)
759 764 prev_commit_id = history[1].raw_id \
760 765 if len(history) > 1 else prev_commit_id
761 766 prev_url = h.route_path(
762 767 'repo_files:annotated', repo_name=self.db_repo_name,
763 768 commit_id=prev_commit_id, f_path=f_path,
764 769 _anchor='L{}'.format(line_anchor))
765 770
766 771 raise HTTPFound(prev_url)
767 772
768 773 @LoginRequired()
769 774 @HasRepoPermissionAnyDecorator(
770 775 'repository.read', 'repository.write', 'repository.admin')
771 776 @view_config(
772 777 route_name='repo_nodetree_full', request_method='GET',
773 778 renderer=None, xhr=True)
774 779 @view_config(
775 780 route_name='repo_nodetree_full:default_path', request_method='GET',
776 781 renderer=None, xhr=True)
777 782 def repo_nodetree_full(self):
778 783 """
779 784 Returns rendered html of file tree that contains commit date,
780 785 author, commit_id for the specified combination of
781 786 repo, commit_id and file path
782 787 """
783 788 c = self.load_default_context()
784 789
785 790 commit_id, f_path = self._get_commit_and_path()
786 791 commit = self._get_commit_or_redirect(commit_id)
787 792 try:
788 793 dir_node = commit.get_node(f_path)
789 794 except RepositoryError as e:
790 795 return Response('error: {}'.format(h.escape(safe_str(e))))
791 796
792 797 if dir_node.is_file():
793 798 return Response('')
794 799
795 800 c.file = dir_node
796 801 c.commit = commit
797 802 at_rev = self.request.GET.get('at')
798 803
799 804 html = self._get_tree_at_commit(
800 805 c, commit.raw_id, dir_node.path, full_load=True, at_rev=at_rev)
801 806
802 807 return Response(html)
803 808
804 809 def _get_attachement_headers(self, f_path):
805 810 f_name = safe_str(f_path.split(Repository.NAME_SEP)[-1])
806 811 safe_path = f_name.replace('"', '\\"')
807 812 encoded_path = urllib.quote(f_name)
808 813
809 814 return "attachment; " \
810 815 "filename=\"{}\"; " \
811 816 "filename*=UTF-8\'\'{}".format(safe_path, encoded_path)
812 817
813 818 @LoginRequired()
814 819 @HasRepoPermissionAnyDecorator(
815 820 'repository.read', 'repository.write', 'repository.admin')
816 821 @view_config(
817 822 route_name='repo_file_raw', request_method='GET',
818 823 renderer=None)
819 824 def repo_file_raw(self):
820 825 """
821 826 Action for show as raw, some mimetypes are "rendered",
822 827 those include images, icons.
823 828 """
824 829 c = self.load_default_context()
825 830
826 831 commit_id, f_path = self._get_commit_and_path()
827 832 commit = self._get_commit_or_redirect(commit_id)
828 833 file_node = self._get_filenode_or_redirect(commit, f_path)
829 834
830 835 raw_mimetype_mapping = {
831 836 # map original mimetype to a mimetype used for "show as raw"
832 837 # you can also provide a content-disposition to override the
833 838 # default "attachment" disposition.
834 839 # orig_type: (new_type, new_dispo)
835 840
836 841 # show images inline:
837 842 # Do not re-add SVG: it is unsafe and permits XSS attacks. One can
838 843 # for example render an SVG with javascript inside or even render
839 844 # HTML.
840 845 'image/x-icon': ('image/x-icon', 'inline'),
841 846 'image/png': ('image/png', 'inline'),
842 847 'image/gif': ('image/gif', 'inline'),
843 848 'image/jpeg': ('image/jpeg', 'inline'),
844 849 'application/pdf': ('application/pdf', 'inline'),
845 850 }
846 851
847 852 mimetype = file_node.mimetype
848 853 try:
849 854 mimetype, disposition = raw_mimetype_mapping[mimetype]
850 855 except KeyError:
851 856 # we don't know anything special about this, handle it safely
852 857 if file_node.is_binary:
853 858 # do same as download raw for binary files
854 859 mimetype, disposition = 'application/octet-stream', 'attachment'
855 860 else:
856 861 # do not just use the original mimetype, but force text/plain,
857 862 # otherwise it would serve text/html and that might be unsafe.
858 863 # Note: underlying vcs library fakes text/plain mimetype if the
859 864 # mimetype can not be determined and it thinks it is not
860 865 # binary.This might lead to erroneous text display in some
861 866 # cases, but helps in other cases, like with text files
862 867 # without extension.
863 868 mimetype, disposition = 'text/plain', 'inline'
864 869
865 870 if disposition == 'attachment':
866 871 disposition = self._get_attachement_headers(f_path)
867 872
868 873 stream_content = file_node.stream_bytes()
869 874
870 875 response = Response(app_iter=stream_content)
871 876 response.content_disposition = disposition
872 877 response.content_type = mimetype
873 878
874 879 charset = self._get_default_encoding(c)
875 880 if charset:
876 881 response.charset = charset
877 882
878 883 return response
879 884
880 885 @LoginRequired()
881 886 @HasRepoPermissionAnyDecorator(
882 887 'repository.read', 'repository.write', 'repository.admin')
883 888 @view_config(
884 889 route_name='repo_file_download', request_method='GET',
885 890 renderer=None)
886 891 @view_config(
887 892 route_name='repo_file_download:legacy', request_method='GET',
888 893 renderer=None)
889 894 def repo_file_download(self):
890 895 c = self.load_default_context()
891 896
892 897 commit_id, f_path = self._get_commit_and_path()
893 898 commit = self._get_commit_or_redirect(commit_id)
894 899 file_node = self._get_filenode_or_redirect(commit, f_path)
895 900
896 901 if self.request.GET.get('lf'):
897 902 # only if lf get flag is passed, we download this file
898 903 # as LFS/Largefile
899 904 lf_node = file_node.get_largefile_node()
900 905 if lf_node:
901 906 # overwrite our pointer with the REAL large-file
902 907 file_node = lf_node
903 908
904 909 disposition = self._get_attachement_headers(f_path)
905 910
906 911 stream_content = file_node.stream_bytes()
907 912
908 913 response = Response(app_iter=stream_content)
909 914 response.content_disposition = disposition
910 915 response.content_type = file_node.mimetype
911 916
912 917 charset = self._get_default_encoding(c)
913 918 if charset:
914 919 response.charset = charset
915 920
916 921 return response
917 922
918 923 def _get_nodelist_at_commit(self, repo_name, repo_id, commit_id, f_path):
919 924
920 925 cache_seconds = safe_int(
921 926 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
922 927 cache_on = cache_seconds > 0
923 928 log.debug(
924 929 'Computing FILE SEARCH for repo_id %s commit_id `%s` and path `%s`'
925 930 'with caching: %s[TTL: %ss]' % (
926 931 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
927 932
928 933 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
929 934 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
930 935
931 936 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid, condition=cache_on)
932 937 def compute_file_search(_name_hash, _repo_id, _commit_id, _f_path):
933 938 log.debug('Generating cached nodelist for repo_id:%s, %s, %s',
934 939 _repo_id, commit_id, f_path)
935 940 try:
936 941 _d, _f = ScmModel().get_quick_filter_nodes(repo_name, _commit_id, _f_path)
937 942 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
938 943 log.exception(safe_str(e))
939 944 h.flash(safe_str(h.escape(e)), category='error')
940 945 raise HTTPFound(h.route_path(
941 946 'repo_files', repo_name=self.db_repo_name,
942 947 commit_id='tip', f_path='/'))
943 948
944 949 return _d + _f
945 950
946 951 result = compute_file_search(self.db_repo.repo_name_hash, self.db_repo.repo_id,
947 952 commit_id, f_path)
948 953 return filter(lambda n: self.path_filter.path_access_allowed(n['name']), result)
949 954
950 955 @LoginRequired()
951 956 @HasRepoPermissionAnyDecorator(
952 957 'repository.read', 'repository.write', 'repository.admin')
953 958 @view_config(
954 959 route_name='repo_files_nodelist', request_method='GET',
955 960 renderer='json_ext', xhr=True)
956 961 def repo_nodelist(self):
957 962 self.load_default_context()
958 963
959 964 commit_id, f_path = self._get_commit_and_path()
960 965 commit = self._get_commit_or_redirect(commit_id)
961 966
962 967 metadata = self._get_nodelist_at_commit(
963 968 self.db_repo_name, self.db_repo.repo_id, commit.raw_id, f_path)
964 969 return {'nodes': metadata}
965 970
966 971 def _create_references(self, branches_or_tags, symbolic_reference, f_path, ref_type):
967 972 items = []
968 973 for name, commit_id in branches_or_tags.items():
969 974 sym_ref = symbolic_reference(commit_id, name, f_path, ref_type)
970 975 items.append((sym_ref, name, ref_type))
971 976 return items
972 977
973 978 def _symbolic_reference(self, commit_id, name, f_path, ref_type):
974 979 return commit_id
975 980
976 981 def _symbolic_reference_svn(self, commit_id, name, f_path, ref_type):
977 982 return commit_id
978 983
979 984 # NOTE(dan): old code we used in "diff" mode compare
980 985 new_f_path = vcspath.join(name, f_path)
981 986 return u'%s@%s' % (new_f_path, commit_id)
982 987
983 988 def _get_node_history(self, commit_obj, f_path, commits=None):
984 989 """
985 990 get commit history for given node
986 991
987 992 :param commit_obj: commit to calculate history
988 993 :param f_path: path for node to calculate history for
989 994 :param commits: if passed don't calculate history and take
990 995 commits defined in this list
991 996 """
992 997 _ = self.request.translate
993 998
994 999 # calculate history based on tip
995 1000 tip = self.rhodecode_vcs_repo.get_commit()
996 1001 if commits is None:
997 1002 pre_load = ["author", "branch"]
998 1003 try:
999 1004 commits = tip.get_path_history(f_path, pre_load=pre_load)
1000 1005 except (NodeDoesNotExistError, CommitError):
1001 1006 # this node is not present at tip!
1002 1007 commits = commit_obj.get_path_history(f_path, pre_load=pre_load)
1003 1008
1004 1009 history = []
1005 1010 commits_group = ([], _("Changesets"))
1006 1011 for commit in commits:
1007 1012 branch = ' (%s)' % commit.branch if commit.branch else ''
1008 1013 n_desc = 'r%s:%s%s' % (commit.idx, commit.short_id, branch)
1009 1014 commits_group[0].append((commit.raw_id, n_desc, 'sha'))
1010 1015 history.append(commits_group)
1011 1016
1012 1017 symbolic_reference = self._symbolic_reference
1013 1018
1014 1019 if self.rhodecode_vcs_repo.alias == 'svn':
1015 1020 adjusted_f_path = RepoFilesView.adjust_file_path_for_svn(
1016 1021 f_path, self.rhodecode_vcs_repo)
1017 1022 if adjusted_f_path != f_path:
1018 1023 log.debug(
1019 1024 'Recognized svn tag or branch in file "%s", using svn '
1020 1025 'specific symbolic references', f_path)
1021 1026 f_path = adjusted_f_path
1022 1027 symbolic_reference = self._symbolic_reference_svn
1023 1028
1024 1029 branches = self._create_references(
1025 1030 self.rhodecode_vcs_repo.branches, symbolic_reference, f_path, 'branch')
1026 1031 branches_group = (branches, _("Branches"))
1027 1032
1028 1033 tags = self._create_references(
1029 1034 self.rhodecode_vcs_repo.tags, symbolic_reference, f_path, 'tag')
1030 1035 tags_group = (tags, _("Tags"))
1031 1036
1032 1037 history.append(branches_group)
1033 1038 history.append(tags_group)
1034 1039
1035 1040 return history, commits
1036 1041
1037 1042 @LoginRequired()
1038 1043 @HasRepoPermissionAnyDecorator(
1039 1044 'repository.read', 'repository.write', 'repository.admin')
1040 1045 @view_config(
1041 1046 route_name='repo_file_history', request_method='GET',
1042 1047 renderer='json_ext')
1043 1048 def repo_file_history(self):
1044 1049 self.load_default_context()
1045 1050
1046 1051 commit_id, f_path = self._get_commit_and_path()
1047 1052 commit = self._get_commit_or_redirect(commit_id)
1048 1053 file_node = self._get_filenode_or_redirect(commit, f_path)
1049 1054
1050 1055 if file_node.is_file():
1051 1056 file_history, _hist = self._get_node_history(commit, f_path)
1052 1057
1053 1058 res = []
1054 1059 for section_items, section in file_history:
1055 1060 items = []
1056 1061 for obj_id, obj_text, obj_type in section_items:
1057 1062 at_rev = ''
1058 1063 if obj_type in ['branch', 'bookmark', 'tag']:
1059 1064 at_rev = obj_text
1060 1065 entry = {
1061 1066 'id': obj_id,
1062 1067 'text': obj_text,
1063 1068 'type': obj_type,
1064 1069 'at_rev': at_rev
1065 1070 }
1066 1071
1067 1072 items.append(entry)
1068 1073
1069 1074 res.append({
1070 1075 'text': section,
1071 1076 'children': items
1072 1077 })
1073 1078
1074 1079 data = {
1075 1080 'more': False,
1076 1081 'results': res
1077 1082 }
1078 1083 return data
1079 1084
1080 1085 log.warning('Cannot fetch history for directory')
1081 1086 raise HTTPBadRequest()
1082 1087
1083 1088 @LoginRequired()
1084 1089 @HasRepoPermissionAnyDecorator(
1085 1090 'repository.read', 'repository.write', 'repository.admin')
1086 1091 @view_config(
1087 1092 route_name='repo_file_authors', request_method='GET',
1088 1093 renderer='rhodecode:templates/files/file_authors_box.mako')
1089 1094 def repo_file_authors(self):
1090 1095 c = self.load_default_context()
1091 1096
1092 1097 commit_id, f_path = self._get_commit_and_path()
1093 1098 commit = self._get_commit_or_redirect(commit_id)
1094 1099 file_node = self._get_filenode_or_redirect(commit, f_path)
1095 1100
1096 1101 if not file_node.is_file():
1097 1102 raise HTTPBadRequest()
1098 1103
1099 1104 c.file_last_commit = file_node.last_commit
1100 1105 if self.request.GET.get('annotate') == '1':
1101 1106 # use _hist from annotation if annotation mode is on
1102 1107 commit_ids = set(x[1] for x in file_node.annotate)
1103 1108 _hist = (
1104 1109 self.rhodecode_vcs_repo.get_commit(commit_id)
1105 1110 for commit_id in commit_ids)
1106 1111 else:
1107 1112 _f_history, _hist = self._get_node_history(commit, f_path)
1108 1113 c.file_author = False
1109 1114
1110 1115 unique = collections.OrderedDict()
1111 1116 for commit in _hist:
1112 1117 author = commit.author
1113 1118 if author not in unique:
1114 1119 unique[commit.author] = [
1115 1120 h.email(author),
1116 1121 h.person(author, 'username_or_name_or_email'),
1117 1122 1 # counter
1118 1123 ]
1119 1124
1120 1125 else:
1121 1126 # increase counter
1122 1127 unique[commit.author][2] += 1
1123 1128
1124 1129 c.authors = [val for val in unique.values()]
1125 1130
1126 1131 return self._get_template_context(c)
1127 1132
1128 1133 @LoginRequired()
1129 1134 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1130 1135 @view_config(
1131 1136 route_name='repo_files_check_head', request_method='POST',
1132 1137 renderer='json_ext', xhr=True)
1133 1138 def repo_files_check_head(self):
1134 1139 self.load_default_context()
1135 1140
1136 1141 commit_id, f_path = self._get_commit_and_path()
1137 1142 _branch_name, _sha_commit_id, is_head = \
1138 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1143 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1144 landing_ref=self.db_repo.landing_ref_name)
1139 1145
1140 1146 new_path = self.request.POST.get('path')
1141 1147 operation = self.request.POST.get('operation')
1142 1148 path_exist = ''
1143 1149
1144 1150 if new_path and operation in ['create', 'upload']:
1145 1151 new_f_path = os.path.join(f_path.lstrip('/'), new_path)
1146 1152 try:
1147 1153 commit_obj = self.rhodecode_vcs_repo.get_commit(commit_id)
1148 1154 # NOTE(dan): construct whole path without leading /
1149 1155 file_node = commit_obj.get_node(new_f_path)
1150 1156 if file_node is not None:
1151 1157 path_exist = new_f_path
1152 1158 except EmptyRepositoryError:
1153 1159 pass
1154 1160 except Exception:
1155 1161 pass
1156 1162
1157 1163 return {
1158 1164 'branch': _branch_name,
1159 1165 'sha': _sha_commit_id,
1160 1166 'is_head': is_head,
1161 1167 'path_exists': path_exist
1162 1168 }
1163 1169
1164 1170 @LoginRequired()
1165 1171 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1166 1172 @view_config(
1167 1173 route_name='repo_files_remove_file', request_method='GET',
1168 1174 renderer='rhodecode:templates/files/files_delete.mako')
1169 1175 def repo_files_remove_file(self):
1170 1176 _ = self.request.translate
1171 1177 c = self.load_default_context()
1172 1178 commit_id, f_path = self._get_commit_and_path()
1173 1179
1174 1180 self._ensure_not_locked()
1175 1181 _branch_name, _sha_commit_id, is_head = \
1176 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1182 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1183 landing_ref=self.db_repo.landing_ref_name)
1177 1184
1178 1185 self.forbid_non_head(is_head, f_path)
1179 1186 self.check_branch_permission(_branch_name)
1180 1187
1181 1188 c.commit = self._get_commit_or_redirect(commit_id)
1182 1189 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1183 1190
1184 1191 c.default_message = _(
1185 1192 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1186 1193 c.f_path = f_path
1187 1194
1188 1195 return self._get_template_context(c)
1189 1196
1190 1197 @LoginRequired()
1191 1198 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1192 1199 @CSRFRequired()
1193 1200 @view_config(
1194 1201 route_name='repo_files_delete_file', request_method='POST',
1195 1202 renderer=None)
1196 1203 def repo_files_delete_file(self):
1197 1204 _ = self.request.translate
1198 1205
1199 1206 c = self.load_default_context()
1200 1207 commit_id, f_path = self._get_commit_and_path()
1201 1208
1202 1209 self._ensure_not_locked()
1203 1210 _branch_name, _sha_commit_id, is_head = \
1204 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1211 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1212 landing_ref=self.db_repo.landing_ref_name)
1205 1213
1206 1214 self.forbid_non_head(is_head, f_path)
1207 1215 self.check_branch_permission(_branch_name)
1208 1216
1209 1217 c.commit = self._get_commit_or_redirect(commit_id)
1210 1218 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1211 1219
1212 1220 c.default_message = _(
1213 1221 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1214 1222 c.f_path = f_path
1215 1223 node_path = f_path
1216 1224 author = self._rhodecode_db_user.full_contact
1217 1225 message = self.request.POST.get('message') or c.default_message
1218 1226 try:
1219 1227 nodes = {
1220 1228 node_path: {
1221 1229 'content': ''
1222 1230 }
1223 1231 }
1224 1232 ScmModel().delete_nodes(
1225 1233 user=self._rhodecode_db_user.user_id, repo=self.db_repo,
1226 1234 message=message,
1227 1235 nodes=nodes,
1228 1236 parent_commit=c.commit,
1229 1237 author=author,
1230 1238 )
1231 1239
1232 1240 h.flash(
1233 1241 _('Successfully deleted file `{}`').format(
1234 1242 h.escape(f_path)), category='success')
1235 1243 except Exception:
1236 1244 log.exception('Error during commit operation')
1237 1245 h.flash(_('Error occurred during commit'), category='error')
1238 1246 raise HTTPFound(
1239 1247 h.route_path('repo_commit', repo_name=self.db_repo_name,
1240 1248 commit_id='tip'))
1241 1249
1242 1250 @LoginRequired()
1243 1251 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1244 1252 @view_config(
1245 1253 route_name='repo_files_edit_file', request_method='GET',
1246 1254 renderer='rhodecode:templates/files/files_edit.mako')
1247 1255 def repo_files_edit_file(self):
1248 1256 _ = self.request.translate
1249 1257 c = self.load_default_context()
1250 1258 commit_id, f_path = self._get_commit_and_path()
1251 1259
1252 1260 self._ensure_not_locked()
1253 1261 _branch_name, _sha_commit_id, is_head = \
1254 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1262 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1263 landing_ref=self.db_repo.landing_ref_name)
1255 1264
1256 1265 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1257 1266 self.check_branch_permission(_branch_name, commit_id=commit_id)
1258 1267
1259 1268 c.commit = self._get_commit_or_redirect(commit_id)
1260 1269 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1261 1270
1262 1271 if c.file.is_binary:
1263 1272 files_url = h.route_path(
1264 1273 'repo_files',
1265 1274 repo_name=self.db_repo_name,
1266 1275 commit_id=c.commit.raw_id, f_path=f_path)
1267 1276 raise HTTPFound(files_url)
1268 1277
1269 1278 c.default_message = _('Edited file {} via RhodeCode Enterprise').format(f_path)
1270 1279 c.f_path = f_path
1271 1280
1272 1281 return self._get_template_context(c)
1273 1282
1274 1283 @LoginRequired()
1275 1284 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1276 1285 @CSRFRequired()
1277 1286 @view_config(
1278 1287 route_name='repo_files_update_file', request_method='POST',
1279 1288 renderer=None)
1280 1289 def repo_files_update_file(self):
1281 1290 _ = self.request.translate
1282 1291 c = self.load_default_context()
1283 1292 commit_id, f_path = self._get_commit_and_path()
1284 1293
1285 1294 self._ensure_not_locked()
1286 1295
1287 1296 c.commit = self._get_commit_or_redirect(commit_id)
1288 1297 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1289 1298
1290 1299 if c.file.is_binary:
1291 1300 raise HTTPFound(h.route_path('repo_files', repo_name=self.db_repo_name,
1292 1301 commit_id=c.commit.raw_id, f_path=f_path))
1293 1302
1294 1303 _branch_name, _sha_commit_id, is_head = \
1295 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1304 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1305 landing_ref=self.db_repo.landing_ref_name)
1296 1306
1297 1307 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1298 1308 self.check_branch_permission(_branch_name, commit_id=commit_id)
1299 1309
1300 1310 c.default_message = _('Edited file {} via RhodeCode Enterprise').format(f_path)
1301 1311 c.f_path = f_path
1302 1312
1303 1313 old_content = c.file.content
1304 1314 sl = old_content.splitlines(1)
1305 1315 first_line = sl[0] if sl else ''
1306 1316
1307 1317 r_post = self.request.POST
1308 1318 # line endings: 0 - Unix, 1 - Mac, 2 - DOS
1309 1319 line_ending_mode = detect_mode(first_line, 0)
1310 1320 content = convert_line_endings(r_post.get('content', ''), line_ending_mode)
1311 1321
1312 1322 message = r_post.get('message') or c.default_message
1313 1323 org_node_path = c.file.unicode_path
1314 1324 filename = r_post['filename']
1315 1325
1316 1326 root_path = c.file.dir_path
1317 1327 pure_path = self.create_pure_path(root_path, filename)
1318 1328 node_path = safe_unicode(bytes(pure_path))
1319 1329
1320 1330 default_redirect_url = h.route_path('repo_commit', repo_name=self.db_repo_name,
1321 1331 commit_id=commit_id)
1322 1332 if content == old_content and node_path == org_node_path:
1323 1333 h.flash(_('No changes detected on {}').format(h.escape(org_node_path)),
1324 1334 category='warning')
1325 1335 raise HTTPFound(default_redirect_url)
1326 1336
1327 1337 try:
1328 1338 mapping = {
1329 1339 org_node_path: {
1330 1340 'org_filename': org_node_path,
1331 1341 'filename': node_path,
1332 1342 'content': content,
1333 1343 'lexer': '',
1334 1344 'op': 'mod',
1335 1345 'mode': c.file.mode
1336 1346 }
1337 1347 }
1338 1348
1339 1349 commit = ScmModel().update_nodes(
1340 1350 user=self._rhodecode_db_user.user_id,
1341 1351 repo=self.db_repo,
1342 1352 message=message,
1343 1353 nodes=mapping,
1344 1354 parent_commit=c.commit,
1345 1355 )
1346 1356
1347 1357 h.flash(_('Successfully committed changes to file `{}`').format(
1348 1358 h.escape(f_path)), category='success')
1349 1359 default_redirect_url = h.route_path(
1350 1360 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1351 1361
1352 1362 except Exception:
1353 1363 log.exception('Error occurred during commit')
1354 1364 h.flash(_('Error occurred during commit'), category='error')
1355 1365
1356 1366 raise HTTPFound(default_redirect_url)
1357 1367
1358 1368 @LoginRequired()
1359 1369 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1360 1370 @view_config(
1361 1371 route_name='repo_files_add_file', request_method='GET',
1362 1372 renderer='rhodecode:templates/files/files_add.mako')
1363 1373 @view_config(
1364 1374 route_name='repo_files_upload_file', request_method='GET',
1365 1375 renderer='rhodecode:templates/files/files_upload.mako')
1366 1376 def repo_files_add_file(self):
1367 1377 _ = self.request.translate
1368 1378 c = self.load_default_context()
1369 1379 commit_id, f_path = self._get_commit_and_path()
1370 1380
1371 1381 self._ensure_not_locked()
1372 1382
1373 1383 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1374 1384 if c.commit is None:
1375 1385 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1376 1386
1377 1387 if self.rhodecode_vcs_repo.is_empty():
1378 1388 # for empty repository we cannot check for current branch, we rely on
1379 1389 # c.commit.branch instead
1380 1390 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1381 1391 else:
1382 1392 _branch_name, _sha_commit_id, is_head = \
1383 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1393 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1394 landing_ref=self.db_repo.landing_ref_name)
1384 1395
1385 1396 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1386 1397 self.check_branch_permission(_branch_name, commit_id=commit_id)
1387 1398
1388 1399 c.default_message = (_('Added file via RhodeCode Enterprise'))
1389 1400 c.f_path = f_path.lstrip('/') # ensure not relative path
1390 1401
1391 1402 return self._get_template_context(c)
1392 1403
1393 1404 @LoginRequired()
1394 1405 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1395 1406 @CSRFRequired()
1396 1407 @view_config(
1397 1408 route_name='repo_files_create_file', request_method='POST',
1398 1409 renderer=None)
1399 1410 def repo_files_create_file(self):
1400 1411 _ = self.request.translate
1401 1412 c = self.load_default_context()
1402 1413 commit_id, f_path = self._get_commit_and_path()
1403 1414
1404 1415 self._ensure_not_locked()
1405 1416
1406 1417 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1407 1418 if c.commit is None:
1408 1419 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1409 1420
1410 1421 # calculate redirect URL
1411 1422 if self.rhodecode_vcs_repo.is_empty():
1412 1423 default_redirect_url = h.route_path(
1413 1424 'repo_summary', repo_name=self.db_repo_name)
1414 1425 else:
1415 1426 default_redirect_url = h.route_path(
1416 1427 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1417 1428
1418 1429 if self.rhodecode_vcs_repo.is_empty():
1419 1430 # for empty repository we cannot check for current branch, we rely on
1420 1431 # c.commit.branch instead
1421 1432 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1422 1433 else:
1423 1434 _branch_name, _sha_commit_id, is_head = \
1424 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1435 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1436 landing_ref=self.db_repo.landing_ref_name)
1425 1437
1426 1438 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1427 1439 self.check_branch_permission(_branch_name, commit_id=commit_id)
1428 1440
1429 1441 c.default_message = (_('Added file via RhodeCode Enterprise'))
1430 1442 c.f_path = f_path
1431 1443
1432 1444 r_post = self.request.POST
1433 1445 message = r_post.get('message') or c.default_message
1434 1446 filename = r_post.get('filename')
1435 1447 unix_mode = 0
1436 1448 content = convert_line_endings(r_post.get('content', ''), unix_mode)
1437 1449
1438 1450 if not filename:
1439 1451 # If there's no commit, redirect to repo summary
1440 1452 if type(c.commit) is EmptyCommit:
1441 1453 redirect_url = h.route_path(
1442 1454 'repo_summary', repo_name=self.db_repo_name)
1443 1455 else:
1444 1456 redirect_url = default_redirect_url
1445 1457 h.flash(_('No filename specified'), category='warning')
1446 1458 raise HTTPFound(redirect_url)
1447 1459
1448 1460 root_path = f_path
1449 1461 pure_path = self.create_pure_path(root_path, filename)
1450 1462 node_path = safe_unicode(bytes(pure_path).lstrip('/'))
1451 1463
1452 1464 author = self._rhodecode_db_user.full_contact
1453 1465 nodes = {
1454 1466 node_path: {
1455 1467 'content': content
1456 1468 }
1457 1469 }
1458 1470
1459 1471 try:
1460 1472
1461 1473 commit = ScmModel().create_nodes(
1462 1474 user=self._rhodecode_db_user.user_id,
1463 1475 repo=self.db_repo,
1464 1476 message=message,
1465 1477 nodes=nodes,
1466 1478 parent_commit=c.commit,
1467 1479 author=author,
1468 1480 )
1469 1481
1470 1482 h.flash(_('Successfully committed new file `{}`').format(
1471 1483 h.escape(node_path)), category='success')
1472 1484
1473 1485 default_redirect_url = h.route_path(
1474 1486 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1475 1487
1476 1488 except NonRelativePathError:
1477 1489 log.exception('Non Relative path found')
1478 1490 h.flash(_('The location specified must be a relative path and must not '
1479 1491 'contain .. in the path'), category='warning')
1480 1492 raise HTTPFound(default_redirect_url)
1481 1493 except (NodeError, NodeAlreadyExistsError) as e:
1482 1494 h.flash(_(h.escape(e)), category='error')
1483 1495 except Exception:
1484 1496 log.exception('Error occurred during commit')
1485 1497 h.flash(_('Error occurred during commit'), category='error')
1486 1498
1487 1499 raise HTTPFound(default_redirect_url)
1488 1500
1489 1501 @LoginRequired()
1490 1502 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1491 1503 @CSRFRequired()
1492 1504 @view_config(
1493 1505 route_name='repo_files_upload_file', request_method='POST',
1494 1506 renderer='json_ext')
1495 1507 def repo_files_upload_file(self):
1496 1508 _ = self.request.translate
1497 1509 c = self.load_default_context()
1498 1510 commit_id, f_path = self._get_commit_and_path()
1499 1511
1500 1512 self._ensure_not_locked()
1501 1513
1502 1514 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1503 1515 if c.commit is None:
1504 1516 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1505 1517
1506 1518 # calculate redirect URL
1507 1519 if self.rhodecode_vcs_repo.is_empty():
1508 1520 default_redirect_url = h.route_path(
1509 1521 'repo_summary', repo_name=self.db_repo_name)
1510 1522 else:
1511 1523 default_redirect_url = h.route_path(
1512 1524 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1513 1525
1514 1526 if self.rhodecode_vcs_repo.is_empty():
1515 1527 # for empty repository we cannot check for current branch, we rely on
1516 1528 # c.commit.branch instead
1517 1529 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1518 1530 else:
1519 1531 _branch_name, _sha_commit_id, is_head = \
1520 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1532 self._is_valid_head(commit_id, self.rhodecode_vcs_repo,
1533 landing_ref=self.db_repo.landing_ref_name)
1521 1534
1522 1535 error = self.forbid_non_head(is_head, f_path, json_mode=True)
1523 1536 if error:
1524 1537 return {
1525 1538 'error': error,
1526 1539 'redirect_url': default_redirect_url
1527 1540 }
1528 1541 error = self.check_branch_permission(_branch_name, json_mode=True)
1529 1542 if error:
1530 1543 return {
1531 1544 'error': error,
1532 1545 'redirect_url': default_redirect_url
1533 1546 }
1534 1547
1535 1548 c.default_message = (_('Uploaded file via RhodeCode Enterprise'))
1536 1549 c.f_path = f_path
1537 1550
1538 1551 r_post = self.request.POST
1539 1552
1540 1553 message = c.default_message
1541 1554 user_message = r_post.getall('message')
1542 1555 if isinstance(user_message, list) and user_message:
1543 1556 # we take the first from duplicated results if it's not empty
1544 1557 message = user_message[0] if user_message[0] else message
1545 1558
1546 1559 nodes = {}
1547 1560
1548 1561 for file_obj in r_post.getall('files_upload') or []:
1549 1562 content = file_obj.file
1550 1563 filename = file_obj.filename
1551 1564
1552 1565 root_path = f_path
1553 1566 pure_path = self.create_pure_path(root_path, filename)
1554 1567 node_path = safe_unicode(bytes(pure_path).lstrip('/'))
1555 1568
1556 1569 nodes[node_path] = {
1557 1570 'content': content
1558 1571 }
1559 1572
1560 1573 if not nodes:
1561 1574 error = 'missing files'
1562 1575 return {
1563 1576 'error': error,
1564 1577 'redirect_url': default_redirect_url
1565 1578 }
1566 1579
1567 1580 author = self._rhodecode_db_user.full_contact
1568 1581
1569 1582 try:
1570 1583 commit = ScmModel().create_nodes(
1571 1584 user=self._rhodecode_db_user.user_id,
1572 1585 repo=self.db_repo,
1573 1586 message=message,
1574 1587 nodes=nodes,
1575 1588 parent_commit=c.commit,
1576 1589 author=author,
1577 1590 )
1578 1591 if len(nodes) == 1:
1579 1592 flash_message = _('Successfully committed {} new files').format(len(nodes))
1580 1593 else:
1581 1594 flash_message = _('Successfully committed 1 new file')
1582 1595
1583 1596 h.flash(flash_message, category='success')
1584 1597
1585 1598 default_redirect_url = h.route_path(
1586 1599 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1587 1600
1588 1601 except NonRelativePathError:
1589 1602 log.exception('Non Relative path found')
1590 1603 error = _('The location specified must be a relative path and must not '
1591 1604 'contain .. in the path')
1592 1605 h.flash(error, category='warning')
1593 1606
1594 1607 return {
1595 1608 'error': error,
1596 1609 'redirect_url': default_redirect_url
1597 1610 }
1598 1611 except (NodeError, NodeAlreadyExistsError) as e:
1599 1612 error = h.escape(e)
1600 1613 h.flash(error, category='error')
1601 1614
1602 1615 return {
1603 1616 'error': error,
1604 1617 'redirect_url': default_redirect_url
1605 1618 }
1606 1619 except Exception:
1607 1620 log.exception('Error occurred during commit')
1608 1621 error = _('Error occurred during commit')
1609 1622 h.flash(error, category='error')
1610 1623 return {
1611 1624 'error': error,
1612 1625 'redirect_url': default_redirect_url
1613 1626 }
1614 1627
1615 1628 return {
1616 1629 'error': None,
1617 1630 'redirect_url': default_redirect_url
1618 1631 }
@@ -1,140 +1,140 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22
23 23 from pyramid.httpexceptions import HTTPFound
24 24 from pyramid.view import view_config
25 25
26 26 from rhodecode.apps._base import RepoAppView
27 27 from rhodecode.lib import helpers as h
28 28 from rhodecode.lib import audit_logger
29 29 from rhodecode.lib.auth import (
30 30 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
31 31 from rhodecode.lib.utils2 import str2bool
32 32 from rhodecode.model.db import User
33 33 from rhodecode.model.forms import RepoPermsForm
34 34 from rhodecode.model.meta import Session
35 35 from rhodecode.model.permission import PermissionModel
36 36 from rhodecode.model.repo import RepoModel
37 37
38 38 log = logging.getLogger(__name__)
39 39
40 40
41 41 class RepoSettingsPermissionsView(RepoAppView):
42 42
43 43 def load_default_context(self):
44 44 c = self._get_local_tmpl_context()
45 45 return c
46 46
47 47 @LoginRequired()
48 48 @HasRepoPermissionAnyDecorator('repository.admin')
49 49 @view_config(
50 50 route_name='edit_repo_perms', request_method='GET',
51 51 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
52 52 def edit_permissions(self):
53 53 _ = self.request.translate
54 54 c = self.load_default_context()
55 55 c.active = 'permissions'
56 56 if self.request.GET.get('branch_permissions'):
57 h.flash(_('Explicitly add user or user group with write+ '
57 h.flash(_('Explicitly add user or user group with write or higher '
58 58 'permission to modify their branch permissions.'),
59 59 category='notice')
60 60 return self._get_template_context(c)
61 61
62 62 @LoginRequired()
63 63 @HasRepoPermissionAnyDecorator('repository.admin')
64 64 @CSRFRequired()
65 65 @view_config(
66 66 route_name='edit_repo_perms', request_method='POST',
67 67 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
68 68 def edit_permissions_update(self):
69 69 _ = self.request.translate
70 70 c = self.load_default_context()
71 71 c.active = 'permissions'
72 72 data = self.request.POST
73 73 # store private flag outside of HTML to verify if we can modify
74 74 # default user permissions, prevents submission of FAKE post data
75 75 # into the form for private repos
76 76 data['repo_private'] = self.db_repo.private
77 77 form = RepoPermsForm(self.request.translate)().to_python(data)
78 78 changes = RepoModel().update_permissions(
79 79 self.db_repo_name, form['perm_additions'], form['perm_updates'],
80 80 form['perm_deletions'])
81 81
82 82 action_data = {
83 83 'added': changes['added'],
84 84 'updated': changes['updated'],
85 85 'deleted': changes['deleted'],
86 86 }
87 87 audit_logger.store_web(
88 88 'repo.edit.permissions', action_data=action_data,
89 89 user=self._rhodecode_user, repo=self.db_repo)
90 90
91 91 Session().commit()
92 92 h.flash(_('Repository access permissions updated'), category='success')
93 93
94 94 affected_user_ids = None
95 95 if changes.get('default_user_changed', False):
96 96 # if we change the default user, we need to flush everyone permissions
97 97 affected_user_ids = User.get_all_user_ids()
98 98 PermissionModel().flush_user_permission_caches(
99 99 changes, affected_user_ids=affected_user_ids)
100 100
101 101 raise HTTPFound(
102 102 h.route_path('edit_repo_perms', repo_name=self.db_repo_name))
103 103
104 104 @LoginRequired()
105 105 @HasRepoPermissionAnyDecorator('repository.admin')
106 106 @CSRFRequired()
107 107 @view_config(
108 108 route_name='edit_repo_perms_set_private', request_method='POST',
109 109 renderer='json_ext')
110 110 def edit_permissions_set_private_repo(self):
111 111 _ = self.request.translate
112 112 self.load_default_context()
113 113
114 114 private_flag = str2bool(self.request.POST.get('private'))
115 115
116 116 try:
117 117 repo = RepoModel().get(self.db_repo.repo_id)
118 118 repo.private = private_flag
119 119 Session().add(repo)
120 120 RepoModel().grant_user_permission(
121 121 repo=self.db_repo, user=User.DEFAULT_USER, perm='repository.none'
122 122 )
123 123
124 124 Session().commit()
125 125
126 126 h.flash(_('Repository `{}` private mode set successfully').format(self.db_repo_name),
127 127 category='success')
128 128 # NOTE(dan): we change repo private mode we need to notify all USERS
129 129 affected_user_ids = User.get_all_user_ids()
130 130 PermissionModel().trigger_permission_flush(affected_user_ids)
131 131
132 132 except Exception:
133 133 log.exception("Exception during update of repository")
134 134 h.flash(_('Error occurred during update of repository {}').format(
135 135 self.db_repo_name), category='error')
136 136
137 137 return {
138 138 'redirect_url': h.route_path('edit_repo_perms', repo_name=self.db_repo_name),
139 139 'private': private_flag
140 140 }
@@ -1,1631 +1,1637 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import collections
23 23
24 24 import formencode
25 25 import formencode.htmlfill
26 26 import peppercorn
27 27 from pyramid.httpexceptions import (
28 28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest, HTTPConflict)
29 29 from pyramid.view import view_config
30 30 from pyramid.renderers import render
31 31
32 32 from rhodecode.apps._base import RepoAppView, DataGridAppView
33 33
34 34 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
35 35 from rhodecode.lib.base import vcs_operation_context
36 36 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
37 37 from rhodecode.lib.exceptions import CommentVersionMismatch
38 38 from rhodecode.lib.ext_json import json
39 39 from rhodecode.lib.auth import (
40 40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
41 41 NotAnonymous, CSRFRequired)
42 42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
43 43 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
44 44 from rhodecode.lib.vcs.exceptions import (
45 45 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
46 46 from rhodecode.model.changeset_status import ChangesetStatusModel
47 47 from rhodecode.model.comment import CommentsModel
48 48 from rhodecode.model.db import (
49 49 func, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository)
50 50 from rhodecode.model.forms import PullRequestForm
51 51 from rhodecode.model.meta import Session
52 52 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
53 53 from rhodecode.model.scm import ScmModel
54 54
55 55 log = logging.getLogger(__name__)
56 56
57 57
58 58 class RepoPullRequestsView(RepoAppView, DataGridAppView):
59 59
60 60 def load_default_context(self):
61 61 c = self._get_local_tmpl_context(include_app_defaults=True)
62 62 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
63 63 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
64 64 # backward compat., we use for OLD PRs a plain renderer
65 65 c.renderer = 'plain'
66 66 return c
67 67
68 68 def _get_pull_requests_list(
69 69 self, repo_name, source, filter_type, opened_by, statuses):
70 70
71 71 draw, start, limit = self._extract_chunk(self.request)
72 72 search_q, order_by, order_dir = self._extract_ordering(self.request)
73 73 _render = self.request.get_partial_renderer(
74 74 'rhodecode:templates/data_table/_dt_elements.mako')
75 75
76 76 # pagination
77 77
78 78 if filter_type == 'awaiting_review':
79 79 pull_requests = PullRequestModel().get_awaiting_review(
80 80 repo_name, search_q=search_q, source=source, opened_by=opened_by,
81 81 statuses=statuses, offset=start, length=limit,
82 82 order_by=order_by, order_dir=order_dir)
83 83 pull_requests_total_count = PullRequestModel().count_awaiting_review(
84 84 repo_name, search_q=search_q, source=source, statuses=statuses,
85 85 opened_by=opened_by)
86 86 elif filter_type == 'awaiting_my_review':
87 87 pull_requests = PullRequestModel().get_awaiting_my_review(
88 88 repo_name, search_q=search_q, source=source, opened_by=opened_by,
89 89 user_id=self._rhodecode_user.user_id, statuses=statuses,
90 90 offset=start, length=limit, order_by=order_by,
91 91 order_dir=order_dir)
92 92 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
93 93 repo_name, search_q=search_q, source=source, user_id=self._rhodecode_user.user_id,
94 94 statuses=statuses, opened_by=opened_by)
95 95 else:
96 96 pull_requests = PullRequestModel().get_all(
97 97 repo_name, search_q=search_q, source=source, opened_by=opened_by,
98 98 statuses=statuses, offset=start, length=limit,
99 99 order_by=order_by, order_dir=order_dir)
100 100 pull_requests_total_count = PullRequestModel().count_all(
101 101 repo_name, search_q=search_q, source=source, statuses=statuses,
102 102 opened_by=opened_by)
103 103
104 104 data = []
105 105 comments_model = CommentsModel()
106 106 for pr in pull_requests:
107 107 comments = comments_model.get_all_comments(
108 108 self.db_repo.repo_id, pull_request=pr)
109 109
110 110 data.append({
111 111 'name': _render('pullrequest_name',
112 112 pr.pull_request_id, pr.pull_request_state,
113 113 pr.work_in_progress, pr.target_repo.repo_name),
114 114 'name_raw': pr.pull_request_id,
115 115 'status': _render('pullrequest_status',
116 116 pr.calculated_review_status()),
117 117 'title': _render('pullrequest_title', pr.title, pr.description),
118 118 'description': h.escape(pr.description),
119 119 'updated_on': _render('pullrequest_updated_on',
120 120 h.datetime_to_time(pr.updated_on)),
121 121 'updated_on_raw': h.datetime_to_time(pr.updated_on),
122 122 'created_on': _render('pullrequest_updated_on',
123 123 h.datetime_to_time(pr.created_on)),
124 124 'created_on_raw': h.datetime_to_time(pr.created_on),
125 125 'state': pr.pull_request_state,
126 126 'author': _render('pullrequest_author',
127 127 pr.author.full_contact, ),
128 128 'author_raw': pr.author.full_name,
129 129 'comments': _render('pullrequest_comments', len(comments)),
130 130 'comments_raw': len(comments),
131 131 'closed': pr.is_closed(),
132 132 })
133 133
134 134 data = ({
135 135 'draw': draw,
136 136 'data': data,
137 137 'recordsTotal': pull_requests_total_count,
138 138 'recordsFiltered': pull_requests_total_count,
139 139 })
140 140 return data
141 141
142 142 @LoginRequired()
143 143 @HasRepoPermissionAnyDecorator(
144 144 'repository.read', 'repository.write', 'repository.admin')
145 145 @view_config(
146 146 route_name='pullrequest_show_all', request_method='GET',
147 147 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
148 148 def pull_request_list(self):
149 149 c = self.load_default_context()
150 150
151 151 req_get = self.request.GET
152 152 c.source = str2bool(req_get.get('source'))
153 153 c.closed = str2bool(req_get.get('closed'))
154 154 c.my = str2bool(req_get.get('my'))
155 155 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
156 156 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
157 157
158 158 c.active = 'open'
159 159 if c.my:
160 160 c.active = 'my'
161 161 if c.closed:
162 162 c.active = 'closed'
163 163 if c.awaiting_review and not c.source:
164 164 c.active = 'awaiting'
165 165 if c.source and not c.awaiting_review:
166 166 c.active = 'source'
167 167 if c.awaiting_my_review:
168 168 c.active = 'awaiting_my'
169 169
170 170 return self._get_template_context(c)
171 171
172 172 @LoginRequired()
173 173 @HasRepoPermissionAnyDecorator(
174 174 'repository.read', 'repository.write', 'repository.admin')
175 175 @view_config(
176 176 route_name='pullrequest_show_all_data', request_method='GET',
177 177 renderer='json_ext', xhr=True)
178 178 def pull_request_list_data(self):
179 179 self.load_default_context()
180 180
181 181 # additional filters
182 182 req_get = self.request.GET
183 183 source = str2bool(req_get.get('source'))
184 184 closed = str2bool(req_get.get('closed'))
185 185 my = str2bool(req_get.get('my'))
186 186 awaiting_review = str2bool(req_get.get('awaiting_review'))
187 187 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
188 188
189 189 filter_type = 'awaiting_review' if awaiting_review \
190 190 else 'awaiting_my_review' if awaiting_my_review \
191 191 else None
192 192
193 193 opened_by = None
194 194 if my:
195 195 opened_by = [self._rhodecode_user.user_id]
196 196
197 197 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
198 198 if closed:
199 199 statuses = [PullRequest.STATUS_CLOSED]
200 200
201 201 data = self._get_pull_requests_list(
202 202 repo_name=self.db_repo_name, source=source,
203 203 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
204 204
205 205 return data
206 206
207 207 def _is_diff_cache_enabled(self, target_repo):
208 208 caching_enabled = self._get_general_setting(
209 209 target_repo, 'rhodecode_diff_cache')
210 210 log.debug('Diff caching enabled: %s', caching_enabled)
211 211 return caching_enabled
212 212
213 213 def _get_diffset(self, source_repo_name, source_repo,
214 214 ancestor_commit,
215 215 source_ref_id, target_ref_id,
216 216 target_commit, source_commit, diff_limit, file_limit,
217 217 fulldiff, hide_whitespace_changes, diff_context, use_ancestor=True):
218 218
219 219 if use_ancestor:
220 220 # we might want to not use it for versions
221 221 target_ref_id = ancestor_commit.raw_id
222 222
223 223 vcs_diff = PullRequestModel().get_diff(
224 224 source_repo, source_ref_id, target_ref_id,
225 225 hide_whitespace_changes, diff_context)
226 226
227 227 diff_processor = diffs.DiffProcessor(
228 228 vcs_diff, format='newdiff', diff_limit=diff_limit,
229 229 file_limit=file_limit, show_full_diff=fulldiff)
230 230
231 231 _parsed = diff_processor.prepare()
232 232
233 233 diffset = codeblocks.DiffSet(
234 234 repo_name=self.db_repo_name,
235 235 source_repo_name=source_repo_name,
236 236 source_node_getter=codeblocks.diffset_node_getter(target_commit),
237 237 target_node_getter=codeblocks.diffset_node_getter(source_commit),
238 238 )
239 239 diffset = self.path_filter.render_patchset_filtered(
240 240 diffset, _parsed, target_commit.raw_id, source_commit.raw_id)
241 241
242 242 return diffset
243 243
244 244 def _get_range_diffset(self, source_scm, source_repo,
245 245 commit1, commit2, diff_limit, file_limit,
246 246 fulldiff, hide_whitespace_changes, diff_context):
247 247 vcs_diff = source_scm.get_diff(
248 248 commit1, commit2,
249 249 ignore_whitespace=hide_whitespace_changes,
250 250 context=diff_context)
251 251
252 252 diff_processor = diffs.DiffProcessor(
253 253 vcs_diff, format='newdiff', diff_limit=diff_limit,
254 254 file_limit=file_limit, show_full_diff=fulldiff)
255 255
256 256 _parsed = diff_processor.prepare()
257 257
258 258 diffset = codeblocks.DiffSet(
259 259 repo_name=source_repo.repo_name,
260 260 source_node_getter=codeblocks.diffset_node_getter(commit1),
261 261 target_node_getter=codeblocks.diffset_node_getter(commit2))
262 262
263 263 diffset = self.path_filter.render_patchset_filtered(
264 264 diffset, _parsed, commit1.raw_id, commit2.raw_id)
265 265
266 266 return diffset
267 267
268 268 @LoginRequired()
269 269 @HasRepoPermissionAnyDecorator(
270 270 'repository.read', 'repository.write', 'repository.admin')
271 271 @view_config(
272 272 route_name='pullrequest_show', request_method='GET',
273 273 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
274 274 def pull_request_show(self):
275 275 _ = self.request.translate
276 276 c = self.load_default_context()
277 277
278 278 pull_request = PullRequest.get_or_404(
279 279 self.request.matchdict['pull_request_id'])
280 280 pull_request_id = pull_request.pull_request_id
281 281
282 282 c.state_progressing = pull_request.is_state_changing()
283 283
284 284 _new_state = {
285 285 'created': PullRequest.STATE_CREATED,
286 286 }.get(self.request.GET.get('force_state'))
287 287
288 288 if c.is_super_admin and _new_state:
289 289 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
290 290 h.flash(
291 291 _('Pull Request state was force changed to `{}`').format(_new_state),
292 292 category='success')
293 293 Session().commit()
294 294
295 295 raise HTTPFound(h.route_path(
296 296 'pullrequest_show', repo_name=self.db_repo_name,
297 297 pull_request_id=pull_request_id))
298 298
299 299 version = self.request.GET.get('version')
300 300 from_version = self.request.GET.get('from_version') or version
301 301 merge_checks = self.request.GET.get('merge_checks')
302 302 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
303 303
304 304 # fetch global flags of ignore ws or context lines
305 305 diff_context = diffs.get_diff_context(self.request)
306 306 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
307 307
308 308 force_refresh = str2bool(self.request.GET.get('force_refresh'))
309 309
310 310 (pull_request_latest,
311 311 pull_request_at_ver,
312 312 pull_request_display_obj,
313 313 at_version) = PullRequestModel().get_pr_version(
314 314 pull_request_id, version=version)
315 315 pr_closed = pull_request_latest.is_closed()
316 316
317 317 if pr_closed and (version or from_version):
318 318 # not allow to browse versions
319 319 raise HTTPFound(h.route_path(
320 320 'pullrequest_show', repo_name=self.db_repo_name,
321 321 pull_request_id=pull_request_id))
322 322
323 323 versions = pull_request_display_obj.versions()
324 324 # used to store per-commit range diffs
325 325 c.changes = collections.OrderedDict()
326 326 c.range_diff_on = self.request.GET.get('range-diff') == "1"
327 327
328 328 c.at_version = at_version
329 329 c.at_version_num = (at_version
330 330 if at_version and at_version != 'latest'
331 331 else None)
332 332 c.at_version_pos = ChangesetComment.get_index_from_version(
333 333 c.at_version_num, versions)
334 334
335 335 (prev_pull_request_latest,
336 336 prev_pull_request_at_ver,
337 337 prev_pull_request_display_obj,
338 338 prev_at_version) = PullRequestModel().get_pr_version(
339 339 pull_request_id, version=from_version)
340 340
341 341 c.from_version = prev_at_version
342 342 c.from_version_num = (prev_at_version
343 343 if prev_at_version and prev_at_version != 'latest'
344 344 else None)
345 345 c.from_version_pos = ChangesetComment.get_index_from_version(
346 346 c.from_version_num, versions)
347 347
348 348 # define if we're in COMPARE mode or VIEW at version mode
349 349 compare = at_version != prev_at_version
350 350
351 351 # pull_requests repo_name we opened it against
352 352 # ie. target_repo must match
353 353 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
354 354 raise HTTPNotFound()
355 355
356 356 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
357 357 pull_request_at_ver)
358 358
359 359 c.pull_request = pull_request_display_obj
360 360 c.renderer = pull_request_at_ver.description_renderer or c.renderer
361 361 c.pull_request_latest = pull_request_latest
362 362
363 363 if compare or (at_version and not at_version == 'latest'):
364 364 c.allowed_to_change_status = False
365 365 c.allowed_to_update = False
366 366 c.allowed_to_merge = False
367 367 c.allowed_to_delete = False
368 368 c.allowed_to_comment = False
369 369 c.allowed_to_close = False
370 370 else:
371 371 can_change_status = PullRequestModel().check_user_change_status(
372 372 pull_request_at_ver, self._rhodecode_user)
373 373 c.allowed_to_change_status = can_change_status and not pr_closed
374 374
375 375 c.allowed_to_update = PullRequestModel().check_user_update(
376 376 pull_request_latest, self._rhodecode_user) and not pr_closed
377 377 c.allowed_to_merge = PullRequestModel().check_user_merge(
378 378 pull_request_latest, self._rhodecode_user) and not pr_closed
379 379 c.allowed_to_delete = PullRequestModel().check_user_delete(
380 380 pull_request_latest, self._rhodecode_user) and not pr_closed
381 381 c.allowed_to_comment = not pr_closed
382 382 c.allowed_to_close = c.allowed_to_merge and not pr_closed
383 383
384 384 c.forbid_adding_reviewers = False
385 385 c.forbid_author_to_review = False
386 386 c.forbid_commit_author_to_review = False
387 387
388 388 if pull_request_latest.reviewer_data and \
389 389 'rules' in pull_request_latest.reviewer_data:
390 390 rules = pull_request_latest.reviewer_data['rules'] or {}
391 391 try:
392 392 c.forbid_adding_reviewers = rules.get(
393 393 'forbid_adding_reviewers')
394 394 c.forbid_author_to_review = rules.get(
395 395 'forbid_author_to_review')
396 396 c.forbid_commit_author_to_review = rules.get(
397 397 'forbid_commit_author_to_review')
398 398 except Exception:
399 399 pass
400 400
401 401 # check merge capabilities
402 402 _merge_check = MergeCheck.validate(
403 403 pull_request_latest, auth_user=self._rhodecode_user,
404 404 translator=self.request.translate,
405 405 force_shadow_repo_refresh=force_refresh)
406 406
407 407 c.pr_merge_errors = _merge_check.error_details
408 408 c.pr_merge_possible = not _merge_check.failed
409 409 c.pr_merge_message = _merge_check.merge_msg
410 410 c.pr_merge_source_commit = _merge_check.source_commit
411 411 c.pr_merge_target_commit = _merge_check.target_commit
412 412
413 413 c.pr_merge_info = MergeCheck.get_merge_conditions(
414 414 pull_request_latest, translator=self.request.translate)
415 415
416 416 c.pull_request_review_status = _merge_check.review_status
417 417 if merge_checks:
418 418 self.request.override_renderer = \
419 419 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
420 420 return self._get_template_context(c)
421 421
422 422 comments_model = CommentsModel()
423 423
424 424 # reviewers and statuses
425 425 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
426 426 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
427 427
428 428 # GENERAL COMMENTS with versions #
429 429 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
430 430 q = q.order_by(ChangesetComment.comment_id.asc())
431 431 general_comments = q
432 432
433 433 # pick comments we want to render at current version
434 434 c.comment_versions = comments_model.aggregate_comments(
435 435 general_comments, versions, c.at_version_num)
436 436 c.comments = c.comment_versions[c.at_version_num]['until']
437 437
438 438 # INLINE COMMENTS with versions #
439 439 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
440 440 q = q.order_by(ChangesetComment.comment_id.asc())
441 441 inline_comments = q
442 442
443 443 c.inline_versions = comments_model.aggregate_comments(
444 444 inline_comments, versions, c.at_version_num, inline=True)
445 445
446 446 # TODOs
447 447 c.unresolved_comments = CommentsModel() \
448 448 .get_pull_request_unresolved_todos(pull_request)
449 449 c.resolved_comments = CommentsModel() \
450 450 .get_pull_request_resolved_todos(pull_request)
451 451
452 452 # inject latest version
453 453 latest_ver = PullRequest.get_pr_display_object(
454 454 pull_request_latest, pull_request_latest)
455 455
456 456 c.versions = versions + [latest_ver]
457 457
458 458 # if we use version, then do not show later comments
459 459 # than current version
460 460 display_inline_comments = collections.defaultdict(
461 461 lambda: collections.defaultdict(list))
462 462 for co in inline_comments:
463 463 if c.at_version_num:
464 464 # pick comments that are at least UPTO given version, so we
465 465 # don't render comments for higher version
466 466 should_render = co.pull_request_version_id and \
467 467 co.pull_request_version_id <= c.at_version_num
468 468 else:
469 469 # showing all, for 'latest'
470 470 should_render = True
471 471
472 472 if should_render:
473 473 display_inline_comments[co.f_path][co.line_no].append(co)
474 474
475 475 # load diff data into template context, if we use compare mode then
476 476 # diff is calculated based on changes between versions of PR
477 477
478 478 source_repo = pull_request_at_ver.source_repo
479 479 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
480 480
481 481 target_repo = pull_request_at_ver.target_repo
482 482 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
483 483
484 484 if compare:
485 485 # in compare switch the diff base to latest commit from prev version
486 486 target_ref_id = prev_pull_request_display_obj.revisions[0]
487 487
488 488 # despite opening commits for bookmarks/branches/tags, we always
489 489 # convert this to rev to prevent changes after bookmark or branch change
490 490 c.source_ref_type = 'rev'
491 491 c.source_ref = source_ref_id
492 492
493 493 c.target_ref_type = 'rev'
494 494 c.target_ref = target_ref_id
495 495
496 496 c.source_repo = source_repo
497 497 c.target_repo = target_repo
498 498
499 499 c.commit_ranges = []
500 500 source_commit = EmptyCommit()
501 501 target_commit = EmptyCommit()
502 502 c.missing_requirements = False
503 503
504 504 source_scm = source_repo.scm_instance()
505 505 target_scm = target_repo.scm_instance()
506 506
507 507 shadow_scm = None
508 508 try:
509 509 shadow_scm = pull_request_latest.get_shadow_repo()
510 510 except Exception:
511 511 log.debug('Failed to get shadow repo', exc_info=True)
512 512 # try first the existing source_repo, and then shadow
513 513 # repo if we can obtain one
514 514 commits_source_repo = source_scm
515 515 if shadow_scm:
516 516 commits_source_repo = shadow_scm
517 517
518 518 c.commits_source_repo = commits_source_repo
519 519 c.ancestor = None # set it to None, to hide it from PR view
520 520
521 521 # empty version means latest, so we keep this to prevent
522 522 # double caching
523 523 version_normalized = version or 'latest'
524 524 from_version_normalized = from_version or 'latest'
525 525
526 526 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
527 527 cache_file_path = diff_cache_exist(
528 528 cache_path, 'pull_request', pull_request_id, version_normalized,
529 529 from_version_normalized, source_ref_id, target_ref_id,
530 530 hide_whitespace_changes, diff_context, c.fulldiff)
531 531
532 532 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
533 533 force_recache = self.get_recache_flag()
534 534
535 535 cached_diff = None
536 536 if caching_enabled:
537 537 cached_diff = load_cached_diff(cache_file_path)
538 538
539 539 has_proper_commit_cache = (
540 540 cached_diff and cached_diff.get('commits')
541 541 and len(cached_diff.get('commits', [])) == 5
542 542 and cached_diff.get('commits')[0]
543 543 and cached_diff.get('commits')[3])
544 544
545 545 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
546 546 diff_commit_cache = \
547 547 (ancestor_commit, commit_cache, missing_requirements,
548 548 source_commit, target_commit) = cached_diff['commits']
549 549 else:
550 550 # NOTE(marcink): we reach potentially unreachable errors when a PR has
551 551 # merge errors resulting in potentially hidden commits in the shadow repo.
552 552 maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \
553 553 and _merge_check.merge_response
554 554 maybe_unreachable = maybe_unreachable \
555 555 and _merge_check.merge_response.metadata.get('unresolved_files')
556 556 log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation")
557 557 diff_commit_cache = \
558 558 (ancestor_commit, commit_cache, missing_requirements,
559 559 source_commit, target_commit) = self.get_commits(
560 560 commits_source_repo,
561 561 pull_request_at_ver,
562 562 source_commit,
563 563 source_ref_id,
564 564 source_scm,
565 565 target_commit,
566 566 target_ref_id,
567 567 target_scm,
568 568 maybe_unreachable=maybe_unreachable)
569 569
570 570 # register our commit range
571 571 for comm in commit_cache.values():
572 572 c.commit_ranges.append(comm)
573 573
574 574 c.missing_requirements = missing_requirements
575 575 c.ancestor_commit = ancestor_commit
576 576 c.statuses = source_repo.statuses(
577 577 [x.raw_id for x in c.commit_ranges])
578 578
579 579 # auto collapse if we have more than limit
580 580 collapse_limit = diffs.DiffProcessor._collapse_commits_over
581 581 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
582 582 c.compare_mode = compare
583 583
584 584 # diff_limit is the old behavior, will cut off the whole diff
585 585 # if the limit is applied otherwise will just hide the
586 586 # big files from the front-end
587 587 diff_limit = c.visual.cut_off_limit_diff
588 588 file_limit = c.visual.cut_off_limit_file
589 589
590 590 c.missing_commits = False
591 591 if (c.missing_requirements
592 592 or isinstance(source_commit, EmptyCommit)
593 593 or source_commit == target_commit):
594 594
595 595 c.missing_commits = True
596 596 else:
597 597 c.inline_comments = display_inline_comments
598 598
599 599 use_ancestor = True
600 600 if from_version_normalized != version_normalized:
601 601 use_ancestor = False
602 602
603 603 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
604 604 if not force_recache and has_proper_diff_cache:
605 605 c.diffset = cached_diff['diff']
606 606 else:
607 c.diffset = self._get_diffset(
608 c.source_repo.repo_name, commits_source_repo,
609 c.ancestor_commit,
610 source_ref_id, target_ref_id,
611 target_commit, source_commit,
612 diff_limit, file_limit, c.fulldiff,
613 hide_whitespace_changes, diff_context,
614 use_ancestor=use_ancestor
615 )
616
617 # save cached diff
618 if caching_enabled:
619 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
620
621 c.limited_diff = c.diffset.limited_diff
622
623 # calculate removed files that are bound to comments
624 comment_deleted_files = [
625 fname for fname in display_inline_comments
626 if fname not in c.diffset.file_stats]
627
628 c.deleted_files_comments = collections.defaultdict(dict)
629 for fname, per_line_comments in display_inline_comments.items():
630 if fname in comment_deleted_files:
631 c.deleted_files_comments[fname]['stats'] = 0
632 c.deleted_files_comments[fname]['comments'] = list()
633 for lno, comments in per_line_comments.items():
634 c.deleted_files_comments[fname]['comments'].extend(comments)
635
636 # maybe calculate the range diff
637 if c.range_diff_on:
638 # TODO(marcink): set whitespace/context
639 context_lcl = 3
640 ign_whitespace_lcl = False
641
642 for commit in c.commit_ranges:
643 commit2 = commit
644 commit1 = commit.first_parent
645
646 range_diff_cache_file_path = diff_cache_exist(
647 cache_path, 'diff', commit.raw_id,
648 ign_whitespace_lcl, context_lcl, c.fulldiff)
649
650 cached_diff = None
651 if caching_enabled:
652 cached_diff = load_cached_diff(range_diff_cache_file_path)
653
654 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
655 if not force_recache and has_proper_diff_cache:
656 diffset = cached_diff['diff']
657 else:
658 diffset = self._get_range_diffset(
659 commits_source_repo, source_repo,
660 commit1, commit2, diff_limit, file_limit,
661 c.fulldiff, ign_whitespace_lcl, context_lcl
662 )
607 try:
608 c.diffset = self._get_diffset(
609 c.source_repo.repo_name, commits_source_repo,
610 c.ancestor_commit,
611 source_ref_id, target_ref_id,
612 target_commit, source_commit,
613 diff_limit, file_limit, c.fulldiff,
614 hide_whitespace_changes, diff_context,
615 use_ancestor=use_ancestor
616 )
663 617
664 618 # save cached diff
665 619 if caching_enabled:
666 cache_diff(range_diff_cache_file_path, diffset, None)
620 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
621 except CommitDoesNotExistError:
622 log.exception('Failed to generate diffset')
623 c.missing_commits = True
624
625 if not c.missing_commits:
626
627 c.limited_diff = c.diffset.limited_diff
628
629 # calculate removed files that are bound to comments
630 comment_deleted_files = [
631 fname for fname in display_inline_comments
632 if fname not in c.diffset.file_stats]
633
634 c.deleted_files_comments = collections.defaultdict(dict)
635 for fname, per_line_comments in display_inline_comments.items():
636 if fname in comment_deleted_files:
637 c.deleted_files_comments[fname]['stats'] = 0
638 c.deleted_files_comments[fname]['comments'] = list()
639 for lno, comments in per_line_comments.items():
640 c.deleted_files_comments[fname]['comments'].extend(comments)
641
642 # maybe calculate the range diff
643 if c.range_diff_on:
644 # TODO(marcink): set whitespace/context
645 context_lcl = 3
646 ign_whitespace_lcl = False
667 647
668 c.changes[commit.raw_id] = diffset
648 for commit in c.commit_ranges:
649 commit2 = commit
650 commit1 = commit.first_parent
651
652 range_diff_cache_file_path = diff_cache_exist(
653 cache_path, 'diff', commit.raw_id,
654 ign_whitespace_lcl, context_lcl, c.fulldiff)
655
656 cached_diff = None
657 if caching_enabled:
658 cached_diff = load_cached_diff(range_diff_cache_file_path)
659
660 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
661 if not force_recache and has_proper_diff_cache:
662 diffset = cached_diff['diff']
663 else:
664 diffset = self._get_range_diffset(
665 commits_source_repo, source_repo,
666 commit1, commit2, diff_limit, file_limit,
667 c.fulldiff, ign_whitespace_lcl, context_lcl
668 )
669
670 # save cached diff
671 if caching_enabled:
672 cache_diff(range_diff_cache_file_path, diffset, None)
673
674 c.changes[commit.raw_id] = diffset
669 675
670 676 # this is a hack to properly display links, when creating PR, the
671 677 # compare view and others uses different notation, and
672 678 # compare_commits.mako renders links based on the target_repo.
673 679 # We need to swap that here to generate it properly on the html side
674 680 c.target_repo = c.source_repo
675 681
676 682 c.commit_statuses = ChangesetStatus.STATUSES
677 683
678 684 c.show_version_changes = not pr_closed
679 685 if c.show_version_changes:
680 686 cur_obj = pull_request_at_ver
681 687 prev_obj = prev_pull_request_at_ver
682 688
683 689 old_commit_ids = prev_obj.revisions
684 690 new_commit_ids = cur_obj.revisions
685 691 commit_changes = PullRequestModel()._calculate_commit_id_changes(
686 692 old_commit_ids, new_commit_ids)
687 693 c.commit_changes_summary = commit_changes
688 694
689 695 # calculate the diff for commits between versions
690 696 c.commit_changes = []
691 697
692 698 def mark(cs, fw):
693 699 return list(h.itertools.izip_longest([], cs, fillvalue=fw))
694 700
695 701 for c_type, raw_id in mark(commit_changes.added, 'a') \
696 702 + mark(commit_changes.removed, 'r') \
697 703 + mark(commit_changes.common, 'c'):
698 704
699 705 if raw_id in commit_cache:
700 706 commit = commit_cache[raw_id]
701 707 else:
702 708 try:
703 709 commit = commits_source_repo.get_commit(raw_id)
704 710 except CommitDoesNotExistError:
705 711 # in case we fail extracting still use "dummy" commit
706 712 # for display in commit diff
707 713 commit = h.AttributeDict(
708 714 {'raw_id': raw_id,
709 715 'message': 'EMPTY or MISSING COMMIT'})
710 716 c.commit_changes.append([c_type, commit])
711 717
712 718 # current user review statuses for each version
713 719 c.review_versions = {}
714 720 if self._rhodecode_user.user_id in allowed_reviewers:
715 721 for co in general_comments:
716 722 if co.author.user_id == self._rhodecode_user.user_id:
717 723 status = co.status_change
718 724 if status:
719 725 _ver_pr = status[0].comment.pull_request_version_id
720 726 c.review_versions[_ver_pr] = status[0]
721 727
722 728 return self._get_template_context(c)
723 729
724 730 def get_commits(
725 731 self, commits_source_repo, pull_request_at_ver, source_commit,
726 732 source_ref_id, source_scm, target_commit, target_ref_id, target_scm,
727 733 maybe_unreachable=False):
728 734
729 735 commit_cache = collections.OrderedDict()
730 736 missing_requirements = False
731 737
732 738 try:
733 739 pre_load = ["author", "date", "message", "branch", "parents"]
734 740
735 741 pull_request_commits = pull_request_at_ver.revisions
736 742 log.debug('Loading %s commits from %s',
737 743 len(pull_request_commits), commits_source_repo)
738 744
739 745 for rev in pull_request_commits:
740 746 comm = commits_source_repo.get_commit(commit_id=rev, pre_load=pre_load,
741 747 maybe_unreachable=maybe_unreachable)
742 748 commit_cache[comm.raw_id] = comm
743 749
744 750 # Order here matters, we first need to get target, and then
745 751 # the source
746 752 target_commit = commits_source_repo.get_commit(
747 753 commit_id=safe_str(target_ref_id))
748 754
749 755 source_commit = commits_source_repo.get_commit(
750 756 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
751 757 except CommitDoesNotExistError:
752 758 log.warning('Failed to get commit from `{}` repo'.format(
753 759 commits_source_repo), exc_info=True)
754 760 except RepositoryRequirementError:
755 761 log.warning('Failed to get all required data from repo', exc_info=True)
756 762 missing_requirements = True
757 763
758 764 pr_ancestor_id = pull_request_at_ver.common_ancestor_id
759 765
760 766 try:
761 767 ancestor_commit = source_scm.get_commit(pr_ancestor_id)
762 768 except Exception:
763 769 ancestor_commit = None
764 770
765 771 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
766 772
767 773 def assure_not_empty_repo(self):
768 774 _ = self.request.translate
769 775
770 776 try:
771 777 self.db_repo.scm_instance().get_commit()
772 778 except EmptyRepositoryError:
773 779 h.flash(h.literal(_('There are no commits yet')),
774 780 category='warning')
775 781 raise HTTPFound(
776 782 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
777 783
778 784 @LoginRequired()
779 785 @NotAnonymous()
780 786 @HasRepoPermissionAnyDecorator(
781 787 'repository.read', 'repository.write', 'repository.admin')
782 788 @view_config(
783 789 route_name='pullrequest_new', request_method='GET',
784 790 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
785 791 def pull_request_new(self):
786 792 _ = self.request.translate
787 793 c = self.load_default_context()
788 794
789 795 self.assure_not_empty_repo()
790 796 source_repo = self.db_repo
791 797
792 798 commit_id = self.request.GET.get('commit')
793 799 branch_ref = self.request.GET.get('branch')
794 800 bookmark_ref = self.request.GET.get('bookmark')
795 801
796 802 try:
797 803 source_repo_data = PullRequestModel().generate_repo_data(
798 804 source_repo, commit_id=commit_id,
799 805 branch=branch_ref, bookmark=bookmark_ref,
800 806 translator=self.request.translate)
801 807 except CommitDoesNotExistError as e:
802 808 log.exception(e)
803 809 h.flash(_('Commit does not exist'), 'error')
804 810 raise HTTPFound(
805 811 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
806 812
807 813 default_target_repo = source_repo
808 814
809 815 if source_repo.parent and c.has_origin_repo_read_perm:
810 816 parent_vcs_obj = source_repo.parent.scm_instance()
811 817 if parent_vcs_obj and not parent_vcs_obj.is_empty():
812 818 # change default if we have a parent repo
813 819 default_target_repo = source_repo.parent
814 820
815 821 target_repo_data = PullRequestModel().generate_repo_data(
816 822 default_target_repo, translator=self.request.translate)
817 823
818 824 selected_source_ref = source_repo_data['refs']['selected_ref']
819 825 title_source_ref = ''
820 826 if selected_source_ref:
821 827 title_source_ref = selected_source_ref.split(':', 2)[1]
822 828 c.default_title = PullRequestModel().generate_pullrequest_title(
823 829 source=source_repo.repo_name,
824 830 source_ref=title_source_ref,
825 831 target=default_target_repo.repo_name
826 832 )
827 833
828 834 c.default_repo_data = {
829 835 'source_repo_name': source_repo.repo_name,
830 836 'source_refs_json': json.dumps(source_repo_data),
831 837 'target_repo_name': default_target_repo.repo_name,
832 838 'target_refs_json': json.dumps(target_repo_data),
833 839 }
834 840 c.default_source_ref = selected_source_ref
835 841
836 842 return self._get_template_context(c)
837 843
838 844 @LoginRequired()
839 845 @NotAnonymous()
840 846 @HasRepoPermissionAnyDecorator(
841 847 'repository.read', 'repository.write', 'repository.admin')
842 848 @view_config(
843 849 route_name='pullrequest_repo_refs', request_method='GET',
844 850 renderer='json_ext', xhr=True)
845 851 def pull_request_repo_refs(self):
846 852 self.load_default_context()
847 853 target_repo_name = self.request.matchdict['target_repo_name']
848 854 repo = Repository.get_by_repo_name(target_repo_name)
849 855 if not repo:
850 856 raise HTTPNotFound()
851 857
852 858 target_perm = HasRepoPermissionAny(
853 859 'repository.read', 'repository.write', 'repository.admin')(
854 860 target_repo_name)
855 861 if not target_perm:
856 862 raise HTTPNotFound()
857 863
858 864 return PullRequestModel().generate_repo_data(
859 865 repo, translator=self.request.translate)
860 866
861 867 @LoginRequired()
862 868 @NotAnonymous()
863 869 @HasRepoPermissionAnyDecorator(
864 870 'repository.read', 'repository.write', 'repository.admin')
865 871 @view_config(
866 872 route_name='pullrequest_repo_targets', request_method='GET',
867 873 renderer='json_ext', xhr=True)
868 874 def pullrequest_repo_targets(self):
869 875 _ = self.request.translate
870 876 filter_query = self.request.GET.get('query')
871 877
872 878 # get the parents
873 879 parent_target_repos = []
874 880 if self.db_repo.parent:
875 881 parents_query = Repository.query() \
876 882 .order_by(func.length(Repository.repo_name)) \
877 883 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
878 884
879 885 if filter_query:
880 886 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
881 887 parents_query = parents_query.filter(
882 888 Repository.repo_name.ilike(ilike_expression))
883 889 parents = parents_query.limit(20).all()
884 890
885 891 for parent in parents:
886 892 parent_vcs_obj = parent.scm_instance()
887 893 if parent_vcs_obj and not parent_vcs_obj.is_empty():
888 894 parent_target_repos.append(parent)
889 895
890 896 # get other forks, and repo itself
891 897 query = Repository.query() \
892 898 .order_by(func.length(Repository.repo_name)) \
893 899 .filter(
894 900 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
895 901 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
896 902 ) \
897 903 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
898 904
899 905 if filter_query:
900 906 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
901 907 query = query.filter(Repository.repo_name.ilike(ilike_expression))
902 908
903 909 limit = max(20 - len(parent_target_repos), 5) # not less then 5
904 910 target_repos = query.limit(limit).all()
905 911
906 912 all_target_repos = target_repos + parent_target_repos
907 913
908 914 repos = []
909 915 # This checks permissions to the repositories
910 916 for obj in ScmModel().get_repos(all_target_repos):
911 917 repos.append({
912 918 'id': obj['name'],
913 919 'text': obj['name'],
914 920 'type': 'repo',
915 921 'repo_id': obj['dbrepo']['repo_id'],
916 922 'repo_type': obj['dbrepo']['repo_type'],
917 923 'private': obj['dbrepo']['private'],
918 924
919 925 })
920 926
921 927 data = {
922 928 'more': False,
923 929 'results': [{
924 930 'text': _('Repositories'),
925 931 'children': repos
926 932 }] if repos else []
927 933 }
928 934 return data
929 935
930 936 @LoginRequired()
931 937 @NotAnonymous()
932 938 @HasRepoPermissionAnyDecorator(
933 939 'repository.read', 'repository.write', 'repository.admin')
934 940 @CSRFRequired()
935 941 @view_config(
936 942 route_name='pullrequest_create', request_method='POST',
937 943 renderer=None)
938 944 def pull_request_create(self):
939 945 _ = self.request.translate
940 946 self.assure_not_empty_repo()
941 947 self.load_default_context()
942 948
943 949 controls = peppercorn.parse(self.request.POST.items())
944 950
945 951 try:
946 952 form = PullRequestForm(
947 953 self.request.translate, self.db_repo.repo_id)()
948 954 _form = form.to_python(controls)
949 955 except formencode.Invalid as errors:
950 956 if errors.error_dict.get('revisions'):
951 957 msg = 'Revisions: %s' % errors.error_dict['revisions']
952 958 elif errors.error_dict.get('pullrequest_title'):
953 959 msg = errors.error_dict.get('pullrequest_title')
954 960 else:
955 961 msg = _('Error creating pull request: {}').format(errors)
956 962 log.exception(msg)
957 963 h.flash(msg, 'error')
958 964
959 965 # would rather just go back to form ...
960 966 raise HTTPFound(
961 967 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
962 968
963 969 source_repo = _form['source_repo']
964 970 source_ref = _form['source_ref']
965 971 target_repo = _form['target_repo']
966 972 target_ref = _form['target_ref']
967 973 commit_ids = _form['revisions'][::-1]
968 974 common_ancestor_id = _form['common_ancestor']
969 975
970 976 # find the ancestor for this pr
971 977 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
972 978 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
973 979
974 980 if not (source_db_repo or target_db_repo):
975 981 h.flash(_('source_repo or target repo not found'), category='error')
976 982 raise HTTPFound(
977 983 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
978 984
979 985 # re-check permissions again here
980 986 # source_repo we must have read permissions
981 987
982 988 source_perm = HasRepoPermissionAny(
983 989 'repository.read', 'repository.write', 'repository.admin')(
984 990 source_db_repo.repo_name)
985 991 if not source_perm:
986 992 msg = _('Not Enough permissions to source repo `{}`.'.format(
987 993 source_db_repo.repo_name))
988 994 h.flash(msg, category='error')
989 995 # copy the args back to redirect
990 996 org_query = self.request.GET.mixed()
991 997 raise HTTPFound(
992 998 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
993 999 _query=org_query))
994 1000
995 1001 # target repo we must have read permissions, and also later on
996 1002 # we want to check branch permissions here
997 1003 target_perm = HasRepoPermissionAny(
998 1004 'repository.read', 'repository.write', 'repository.admin')(
999 1005 target_db_repo.repo_name)
1000 1006 if not target_perm:
1001 1007 msg = _('Not Enough permissions to target repo `{}`.'.format(
1002 1008 target_db_repo.repo_name))
1003 1009 h.flash(msg, category='error')
1004 1010 # copy the args back to redirect
1005 1011 org_query = self.request.GET.mixed()
1006 1012 raise HTTPFound(
1007 1013 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1008 1014 _query=org_query))
1009 1015
1010 1016 source_scm = source_db_repo.scm_instance()
1011 1017 target_scm = target_db_repo.scm_instance()
1012 1018
1013 1019 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
1014 1020 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
1015 1021
1016 1022 ancestor = source_scm.get_common_ancestor(
1017 1023 source_commit.raw_id, target_commit.raw_id, target_scm)
1018 1024
1019 1025 # recalculate target ref based on ancestor
1020 1026 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
1021 1027 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
1022 1028
1023 1029 get_default_reviewers_data, validate_default_reviewers = \
1024 1030 PullRequestModel().get_reviewer_functions()
1025 1031
1026 1032 # recalculate reviewers logic, to make sure we can validate this
1027 1033 reviewer_rules = get_default_reviewers_data(
1028 1034 self._rhodecode_db_user, source_db_repo,
1029 1035 source_commit, target_db_repo, target_commit)
1030 1036
1031 1037 given_reviewers = _form['review_members']
1032 1038 reviewers = validate_default_reviewers(
1033 1039 given_reviewers, reviewer_rules)
1034 1040
1035 1041 pullrequest_title = _form['pullrequest_title']
1036 1042 title_source_ref = source_ref.split(':', 2)[1]
1037 1043 if not pullrequest_title:
1038 1044 pullrequest_title = PullRequestModel().generate_pullrequest_title(
1039 1045 source=source_repo,
1040 1046 source_ref=title_source_ref,
1041 1047 target=target_repo
1042 1048 )
1043 1049
1044 1050 description = _form['pullrequest_desc']
1045 1051 description_renderer = _form['description_renderer']
1046 1052
1047 1053 try:
1048 1054 pull_request = PullRequestModel().create(
1049 1055 created_by=self._rhodecode_user.user_id,
1050 1056 source_repo=source_repo,
1051 1057 source_ref=source_ref,
1052 1058 target_repo=target_repo,
1053 1059 target_ref=target_ref,
1054 1060 revisions=commit_ids,
1055 1061 common_ancestor_id=common_ancestor_id,
1056 1062 reviewers=reviewers,
1057 1063 title=pullrequest_title,
1058 1064 description=description,
1059 1065 description_renderer=description_renderer,
1060 1066 reviewer_data=reviewer_rules,
1061 1067 auth_user=self._rhodecode_user
1062 1068 )
1063 1069 Session().commit()
1064 1070
1065 1071 h.flash(_('Successfully opened new pull request'),
1066 1072 category='success')
1067 1073 except Exception:
1068 1074 msg = _('Error occurred during creation of this pull request.')
1069 1075 log.exception(msg)
1070 1076 h.flash(msg, category='error')
1071 1077
1072 1078 # copy the args back to redirect
1073 1079 org_query = self.request.GET.mixed()
1074 1080 raise HTTPFound(
1075 1081 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1076 1082 _query=org_query))
1077 1083
1078 1084 raise HTTPFound(
1079 1085 h.route_path('pullrequest_show', repo_name=target_repo,
1080 1086 pull_request_id=pull_request.pull_request_id))
1081 1087
1082 1088 @LoginRequired()
1083 1089 @NotAnonymous()
1084 1090 @HasRepoPermissionAnyDecorator(
1085 1091 'repository.read', 'repository.write', 'repository.admin')
1086 1092 @CSRFRequired()
1087 1093 @view_config(
1088 1094 route_name='pullrequest_update', request_method='POST',
1089 1095 renderer='json_ext')
1090 1096 def pull_request_update(self):
1091 1097 pull_request = PullRequest.get_or_404(
1092 1098 self.request.matchdict['pull_request_id'])
1093 1099 _ = self.request.translate
1094 1100
1095 1101 self.load_default_context()
1096 1102 redirect_url = None
1097 1103
1098 1104 if pull_request.is_closed():
1099 1105 log.debug('update: forbidden because pull request is closed')
1100 1106 msg = _(u'Cannot update closed pull requests.')
1101 1107 h.flash(msg, category='error')
1102 1108 return {'response': True,
1103 1109 'redirect_url': redirect_url}
1104 1110
1105 1111 is_state_changing = pull_request.is_state_changing()
1106 1112
1107 1113 # only owner or admin can update it
1108 1114 allowed_to_update = PullRequestModel().check_user_update(
1109 1115 pull_request, self._rhodecode_user)
1110 1116 if allowed_to_update:
1111 1117 controls = peppercorn.parse(self.request.POST.items())
1112 1118 force_refresh = str2bool(self.request.POST.get('force_refresh'))
1113 1119
1114 1120 if 'review_members' in controls:
1115 1121 self._update_reviewers(
1116 1122 pull_request, controls['review_members'],
1117 1123 pull_request.reviewer_data)
1118 1124 elif str2bool(self.request.POST.get('update_commits', 'false')):
1119 1125 if is_state_changing:
1120 1126 log.debug('commits update: forbidden because pull request is in state %s',
1121 1127 pull_request.pull_request_state)
1122 1128 msg = _(u'Cannot update pull requests commits in state other than `{}`. '
1123 1129 u'Current state is: `{}`').format(
1124 1130 PullRequest.STATE_CREATED, pull_request.pull_request_state)
1125 1131 h.flash(msg, category='error')
1126 1132 return {'response': True,
1127 1133 'redirect_url': redirect_url}
1128 1134
1129 1135 self._update_commits(pull_request)
1130 1136 if force_refresh:
1131 1137 redirect_url = h.route_path(
1132 1138 'pullrequest_show', repo_name=self.db_repo_name,
1133 1139 pull_request_id=pull_request.pull_request_id,
1134 1140 _query={"force_refresh": 1})
1135 1141 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1136 1142 self._edit_pull_request(pull_request)
1137 1143 else:
1138 1144 raise HTTPBadRequest()
1139 1145
1140 1146 return {'response': True,
1141 1147 'redirect_url': redirect_url}
1142 1148 raise HTTPForbidden()
1143 1149
1144 1150 def _edit_pull_request(self, pull_request):
1145 1151 _ = self.request.translate
1146 1152
1147 1153 try:
1148 1154 PullRequestModel().edit(
1149 1155 pull_request,
1150 1156 self.request.POST.get('title'),
1151 1157 self.request.POST.get('description'),
1152 1158 self.request.POST.get('description_renderer'),
1153 1159 self._rhodecode_user)
1154 1160 except ValueError:
1155 1161 msg = _(u'Cannot update closed pull requests.')
1156 1162 h.flash(msg, category='error')
1157 1163 return
1158 1164 else:
1159 1165 Session().commit()
1160 1166
1161 1167 msg = _(u'Pull request title & description updated.')
1162 1168 h.flash(msg, category='success')
1163 1169 return
1164 1170
1165 1171 def _update_commits(self, pull_request):
1166 1172 _ = self.request.translate
1167 1173
1168 1174 with pull_request.set_state(PullRequest.STATE_UPDATING):
1169 1175 resp = PullRequestModel().update_commits(
1170 1176 pull_request, self._rhodecode_db_user)
1171 1177
1172 1178 if resp.executed:
1173 1179
1174 1180 if resp.target_changed and resp.source_changed:
1175 1181 changed = 'target and source repositories'
1176 1182 elif resp.target_changed and not resp.source_changed:
1177 1183 changed = 'target repository'
1178 1184 elif not resp.target_changed and resp.source_changed:
1179 1185 changed = 'source repository'
1180 1186 else:
1181 1187 changed = 'nothing'
1182 1188
1183 1189 msg = _(u'Pull request updated to "{source_commit_id}" with '
1184 1190 u'{count_added} added, {count_removed} removed commits. '
1185 1191 u'Source of changes: {change_source}')
1186 1192 msg = msg.format(
1187 1193 source_commit_id=pull_request.source_ref_parts.commit_id,
1188 1194 count_added=len(resp.changes.added),
1189 1195 count_removed=len(resp.changes.removed),
1190 1196 change_source=changed)
1191 1197 h.flash(msg, category='success')
1192 1198
1193 1199 channel = '/repo${}$/pr/{}'.format(
1194 1200 pull_request.target_repo.repo_name, pull_request.pull_request_id)
1195 1201 message = msg + (
1196 1202 ' - <a onclick="window.location.reload()">'
1197 1203 '<strong>{}</strong></a>'.format(_('Reload page')))
1198 1204 channelstream.post_message(
1199 1205 channel, message, self._rhodecode_user.username,
1200 1206 registry=self.request.registry)
1201 1207 else:
1202 1208 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1203 1209 warning_reasons = [
1204 1210 UpdateFailureReason.NO_CHANGE,
1205 1211 UpdateFailureReason.WRONG_REF_TYPE,
1206 1212 ]
1207 1213 category = 'warning' if resp.reason in warning_reasons else 'error'
1208 1214 h.flash(msg, category=category)
1209 1215
1210 1216 @LoginRequired()
1211 1217 @NotAnonymous()
1212 1218 @HasRepoPermissionAnyDecorator(
1213 1219 'repository.read', 'repository.write', 'repository.admin')
1214 1220 @CSRFRequired()
1215 1221 @view_config(
1216 1222 route_name='pullrequest_merge', request_method='POST',
1217 1223 renderer='json_ext')
1218 1224 def pull_request_merge(self):
1219 1225 """
1220 1226 Merge will perform a server-side merge of the specified
1221 1227 pull request, if the pull request is approved and mergeable.
1222 1228 After successful merging, the pull request is automatically
1223 1229 closed, with a relevant comment.
1224 1230 """
1225 1231 pull_request = PullRequest.get_or_404(
1226 1232 self.request.matchdict['pull_request_id'])
1227 1233 _ = self.request.translate
1228 1234
1229 1235 if pull_request.is_state_changing():
1230 1236 log.debug('show: forbidden because pull request is in state %s',
1231 1237 pull_request.pull_request_state)
1232 1238 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1233 1239 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1234 1240 pull_request.pull_request_state)
1235 1241 h.flash(msg, category='error')
1236 1242 raise HTTPFound(
1237 1243 h.route_path('pullrequest_show',
1238 1244 repo_name=pull_request.target_repo.repo_name,
1239 1245 pull_request_id=pull_request.pull_request_id))
1240 1246
1241 1247 self.load_default_context()
1242 1248
1243 1249 with pull_request.set_state(PullRequest.STATE_UPDATING):
1244 1250 check = MergeCheck.validate(
1245 1251 pull_request, auth_user=self._rhodecode_user,
1246 1252 translator=self.request.translate)
1247 1253 merge_possible = not check.failed
1248 1254
1249 1255 for err_type, error_msg in check.errors:
1250 1256 h.flash(error_msg, category=err_type)
1251 1257
1252 1258 if merge_possible:
1253 1259 log.debug("Pre-conditions checked, trying to merge.")
1254 1260 extras = vcs_operation_context(
1255 1261 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1256 1262 username=self._rhodecode_db_user.username, action='push',
1257 1263 scm=pull_request.target_repo.repo_type)
1258 1264 with pull_request.set_state(PullRequest.STATE_UPDATING):
1259 1265 self._merge_pull_request(
1260 1266 pull_request, self._rhodecode_db_user, extras)
1261 1267 else:
1262 1268 log.debug("Pre-conditions failed, NOT merging.")
1263 1269
1264 1270 raise HTTPFound(
1265 1271 h.route_path('pullrequest_show',
1266 1272 repo_name=pull_request.target_repo.repo_name,
1267 1273 pull_request_id=pull_request.pull_request_id))
1268 1274
1269 1275 def _merge_pull_request(self, pull_request, user, extras):
1270 1276 _ = self.request.translate
1271 1277 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1272 1278
1273 1279 if merge_resp.executed:
1274 1280 log.debug("The merge was successful, closing the pull request.")
1275 1281 PullRequestModel().close_pull_request(
1276 1282 pull_request.pull_request_id, user)
1277 1283 Session().commit()
1278 1284 msg = _('Pull request was successfully merged and closed.')
1279 1285 h.flash(msg, category='success')
1280 1286 else:
1281 1287 log.debug(
1282 1288 "The merge was not successful. Merge response: %s", merge_resp)
1283 1289 msg = merge_resp.merge_status_message
1284 1290 h.flash(msg, category='error')
1285 1291
1286 1292 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
1287 1293 _ = self.request.translate
1288 1294
1289 1295 get_default_reviewers_data, validate_default_reviewers = \
1290 1296 PullRequestModel().get_reviewer_functions()
1291 1297
1292 1298 try:
1293 1299 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1294 1300 except ValueError as e:
1295 1301 log.error('Reviewers Validation: {}'.format(e))
1296 1302 h.flash(e, category='error')
1297 1303 return
1298 1304
1299 1305 old_calculated_status = pull_request.calculated_review_status()
1300 1306 PullRequestModel().update_reviewers(
1301 1307 pull_request, reviewers, self._rhodecode_user)
1302 1308 h.flash(_('Pull request reviewers updated.'), category='success')
1303 1309 Session().commit()
1304 1310
1305 1311 # trigger status changed if change in reviewers changes the status
1306 1312 calculated_status = pull_request.calculated_review_status()
1307 1313 if old_calculated_status != calculated_status:
1308 1314 PullRequestModel().trigger_pull_request_hook(
1309 1315 pull_request, self._rhodecode_user, 'review_status_change',
1310 1316 data={'status': calculated_status})
1311 1317
1312 1318 @LoginRequired()
1313 1319 @NotAnonymous()
1314 1320 @HasRepoPermissionAnyDecorator(
1315 1321 'repository.read', 'repository.write', 'repository.admin')
1316 1322 @CSRFRequired()
1317 1323 @view_config(
1318 1324 route_name='pullrequest_delete', request_method='POST',
1319 1325 renderer='json_ext')
1320 1326 def pull_request_delete(self):
1321 1327 _ = self.request.translate
1322 1328
1323 1329 pull_request = PullRequest.get_or_404(
1324 1330 self.request.matchdict['pull_request_id'])
1325 1331 self.load_default_context()
1326 1332
1327 1333 pr_closed = pull_request.is_closed()
1328 1334 allowed_to_delete = PullRequestModel().check_user_delete(
1329 1335 pull_request, self._rhodecode_user) and not pr_closed
1330 1336
1331 1337 # only owner can delete it !
1332 1338 if allowed_to_delete:
1333 1339 PullRequestModel().delete(pull_request, self._rhodecode_user)
1334 1340 Session().commit()
1335 1341 h.flash(_('Successfully deleted pull request'),
1336 1342 category='success')
1337 1343 raise HTTPFound(h.route_path('pullrequest_show_all',
1338 1344 repo_name=self.db_repo_name))
1339 1345
1340 1346 log.warning('user %s tried to delete pull request without access',
1341 1347 self._rhodecode_user)
1342 1348 raise HTTPNotFound()
1343 1349
1344 1350 @LoginRequired()
1345 1351 @NotAnonymous()
1346 1352 @HasRepoPermissionAnyDecorator(
1347 1353 'repository.read', 'repository.write', 'repository.admin')
1348 1354 @CSRFRequired()
1349 1355 @view_config(
1350 1356 route_name='pullrequest_comment_create', request_method='POST',
1351 1357 renderer='json_ext')
1352 1358 def pull_request_comment_create(self):
1353 1359 _ = self.request.translate
1354 1360
1355 1361 pull_request = PullRequest.get_or_404(
1356 1362 self.request.matchdict['pull_request_id'])
1357 1363 pull_request_id = pull_request.pull_request_id
1358 1364
1359 1365 if pull_request.is_closed():
1360 1366 log.debug('comment: forbidden because pull request is closed')
1361 1367 raise HTTPForbidden()
1362 1368
1363 1369 allowed_to_comment = PullRequestModel().check_user_comment(
1364 1370 pull_request, self._rhodecode_user)
1365 1371 if not allowed_to_comment:
1366 1372 log.debug(
1367 1373 'comment: forbidden because pull request is from forbidden repo')
1368 1374 raise HTTPForbidden()
1369 1375
1370 1376 c = self.load_default_context()
1371 1377
1372 1378 status = self.request.POST.get('changeset_status', None)
1373 1379 text = self.request.POST.get('text')
1374 1380 comment_type = self.request.POST.get('comment_type')
1375 1381 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1376 1382 close_pull_request = self.request.POST.get('close_pull_request')
1377 1383
1378 1384 # the logic here should work like following, if we submit close
1379 1385 # pr comment, use `close_pull_request_with_comment` function
1380 1386 # else handle regular comment logic
1381 1387
1382 1388 if close_pull_request:
1383 1389 # only owner or admin or person with write permissions
1384 1390 allowed_to_close = PullRequestModel().check_user_update(
1385 1391 pull_request, self._rhodecode_user)
1386 1392 if not allowed_to_close:
1387 1393 log.debug('comment: forbidden because not allowed to close '
1388 1394 'pull request %s', pull_request_id)
1389 1395 raise HTTPForbidden()
1390 1396
1391 1397 # This also triggers `review_status_change`
1392 1398 comment, status = PullRequestModel().close_pull_request_with_comment(
1393 1399 pull_request, self._rhodecode_user, self.db_repo, message=text,
1394 1400 auth_user=self._rhodecode_user)
1395 1401 Session().flush()
1396 1402
1397 1403 PullRequestModel().trigger_pull_request_hook(
1398 1404 pull_request, self._rhodecode_user, 'comment',
1399 1405 data={'comment': comment})
1400 1406
1401 1407 else:
1402 1408 # regular comment case, could be inline, or one with status.
1403 1409 # for that one we check also permissions
1404 1410
1405 1411 allowed_to_change_status = PullRequestModel().check_user_change_status(
1406 1412 pull_request, self._rhodecode_user)
1407 1413
1408 1414 if status and allowed_to_change_status:
1409 1415 message = (_('Status change %(transition_icon)s %(status)s')
1410 1416 % {'transition_icon': '>',
1411 1417 'status': ChangesetStatus.get_status_lbl(status)})
1412 1418 text = text or message
1413 1419
1414 1420 comment = CommentsModel().create(
1415 1421 text=text,
1416 1422 repo=self.db_repo.repo_id,
1417 1423 user=self._rhodecode_user.user_id,
1418 1424 pull_request=pull_request,
1419 1425 f_path=self.request.POST.get('f_path'),
1420 1426 line_no=self.request.POST.get('line'),
1421 1427 status_change=(ChangesetStatus.get_status_lbl(status)
1422 1428 if status and allowed_to_change_status else None),
1423 1429 status_change_type=(status
1424 1430 if status and allowed_to_change_status else None),
1425 1431 comment_type=comment_type,
1426 1432 resolves_comment_id=resolves_comment_id,
1427 1433 auth_user=self._rhodecode_user
1428 1434 )
1429 1435
1430 1436 if allowed_to_change_status:
1431 1437 # calculate old status before we change it
1432 1438 old_calculated_status = pull_request.calculated_review_status()
1433 1439
1434 1440 # get status if set !
1435 1441 if status:
1436 1442 ChangesetStatusModel().set_status(
1437 1443 self.db_repo.repo_id,
1438 1444 status,
1439 1445 self._rhodecode_user.user_id,
1440 1446 comment,
1441 1447 pull_request=pull_request
1442 1448 )
1443 1449
1444 1450 Session().flush()
1445 1451 # this is somehow required to get access to some relationship
1446 1452 # loaded on comment
1447 1453 Session().refresh(comment)
1448 1454
1449 1455 PullRequestModel().trigger_pull_request_hook(
1450 1456 pull_request, self._rhodecode_user, 'comment',
1451 1457 data={'comment': comment})
1452 1458
1453 1459 # we now calculate the status of pull request, and based on that
1454 1460 # calculation we set the commits status
1455 1461 calculated_status = pull_request.calculated_review_status()
1456 1462 if old_calculated_status != calculated_status:
1457 1463 PullRequestModel().trigger_pull_request_hook(
1458 1464 pull_request, self._rhodecode_user, 'review_status_change',
1459 1465 data={'status': calculated_status})
1460 1466
1461 1467 Session().commit()
1462 1468
1463 1469 data = {
1464 1470 'target_id': h.safeid(h.safe_unicode(
1465 1471 self.request.POST.get('f_path'))),
1466 1472 }
1467 1473 if comment:
1468 1474 c.co = comment
1469 1475 rendered_comment = render(
1470 1476 'rhodecode:templates/changeset/changeset_comment_block.mako',
1471 1477 self._get_template_context(c), self.request)
1472 1478
1473 1479 data.update(comment.get_dict())
1474 1480 data.update({'rendered_text': rendered_comment})
1475 1481
1476 1482 return data
1477 1483
1478 1484 @LoginRequired()
1479 1485 @NotAnonymous()
1480 1486 @HasRepoPermissionAnyDecorator(
1481 1487 'repository.read', 'repository.write', 'repository.admin')
1482 1488 @CSRFRequired()
1483 1489 @view_config(
1484 1490 route_name='pullrequest_comment_delete', request_method='POST',
1485 1491 renderer='json_ext')
1486 1492 def pull_request_comment_delete(self):
1487 1493 pull_request = PullRequest.get_or_404(
1488 1494 self.request.matchdict['pull_request_id'])
1489 1495
1490 1496 comment = ChangesetComment.get_or_404(
1491 1497 self.request.matchdict['comment_id'])
1492 1498 comment_id = comment.comment_id
1493 1499
1494 1500 if comment.immutable:
1495 1501 # don't allow deleting comments that are immutable
1496 1502 raise HTTPForbidden()
1497 1503
1498 1504 if pull_request.is_closed():
1499 1505 log.debug('comment: forbidden because pull request is closed')
1500 1506 raise HTTPForbidden()
1501 1507
1502 1508 if not comment:
1503 1509 log.debug('Comment with id:%s not found, skipping', comment_id)
1504 1510 # comment already deleted in another call probably
1505 1511 return True
1506 1512
1507 1513 if comment.pull_request.is_closed():
1508 1514 # don't allow deleting comments on closed pull request
1509 1515 raise HTTPForbidden()
1510 1516
1511 1517 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1512 1518 super_admin = h.HasPermissionAny('hg.admin')()
1513 1519 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1514 1520 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1515 1521 comment_repo_admin = is_repo_admin and is_repo_comment
1516 1522
1517 1523 if super_admin or comment_owner or comment_repo_admin:
1518 1524 old_calculated_status = comment.pull_request.calculated_review_status()
1519 1525 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1520 1526 Session().commit()
1521 1527 calculated_status = comment.pull_request.calculated_review_status()
1522 1528 if old_calculated_status != calculated_status:
1523 1529 PullRequestModel().trigger_pull_request_hook(
1524 1530 comment.pull_request, self._rhodecode_user, 'review_status_change',
1525 1531 data={'status': calculated_status})
1526 1532 return True
1527 1533 else:
1528 1534 log.warning('No permissions for user %s to delete comment_id: %s',
1529 1535 self._rhodecode_db_user, comment_id)
1530 1536 raise HTTPNotFound()
1531 1537
1532 1538 @LoginRequired()
1533 1539 @NotAnonymous()
1534 1540 @HasRepoPermissionAnyDecorator(
1535 1541 'repository.read', 'repository.write', 'repository.admin')
1536 1542 @CSRFRequired()
1537 1543 @view_config(
1538 1544 route_name='pullrequest_comment_edit', request_method='POST',
1539 1545 renderer='json_ext')
1540 1546 def pull_request_comment_edit(self):
1541 1547 self.load_default_context()
1542 1548
1543 1549 pull_request = PullRequest.get_or_404(
1544 1550 self.request.matchdict['pull_request_id']
1545 1551 )
1546 1552 comment = ChangesetComment.get_or_404(
1547 1553 self.request.matchdict['comment_id']
1548 1554 )
1549 1555 comment_id = comment.comment_id
1550 1556
1551 1557 if comment.immutable:
1552 1558 # don't allow deleting comments that are immutable
1553 1559 raise HTTPForbidden()
1554 1560
1555 1561 if pull_request.is_closed():
1556 1562 log.debug('comment: forbidden because pull request is closed')
1557 1563 raise HTTPForbidden()
1558 1564
1559 1565 if not comment:
1560 1566 log.debug('Comment with id:%s not found, skipping', comment_id)
1561 1567 # comment already deleted in another call probably
1562 1568 return True
1563 1569
1564 1570 if comment.pull_request.is_closed():
1565 1571 # don't allow deleting comments on closed pull request
1566 1572 raise HTTPForbidden()
1567 1573
1568 1574 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1569 1575 super_admin = h.HasPermissionAny('hg.admin')()
1570 1576 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1571 1577 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1572 1578 comment_repo_admin = is_repo_admin and is_repo_comment
1573 1579
1574 1580 if super_admin or comment_owner or comment_repo_admin:
1575 1581 text = self.request.POST.get('text')
1576 1582 version = self.request.POST.get('version')
1577 1583 if text == comment.text:
1578 1584 log.warning(
1579 1585 'Comment(PR): '
1580 1586 'Trying to create new version '
1581 1587 'with the same comment body {}'.format(
1582 1588 comment_id,
1583 1589 )
1584 1590 )
1585 1591 raise HTTPNotFound()
1586 1592
1587 1593 if version.isdigit():
1588 1594 version = int(version)
1589 1595 else:
1590 1596 log.warning(
1591 1597 'Comment(PR): Wrong version type {} {} '
1592 1598 'for comment {}'.format(
1593 1599 version,
1594 1600 type(version),
1595 1601 comment_id,
1596 1602 )
1597 1603 )
1598 1604 raise HTTPNotFound()
1599 1605
1600 1606 try:
1601 1607 comment_history = CommentsModel().edit(
1602 1608 comment_id=comment_id,
1603 1609 text=text,
1604 1610 auth_user=self._rhodecode_user,
1605 1611 version=version,
1606 1612 )
1607 1613 except CommentVersionMismatch:
1608 1614 raise HTTPConflict()
1609 1615
1610 1616 if not comment_history:
1611 1617 raise HTTPNotFound()
1612 1618
1613 1619 Session().commit()
1614 1620
1615 1621 PullRequestModel().trigger_pull_request_hook(
1616 1622 pull_request, self._rhodecode_user, 'comment_edit',
1617 1623 data={'comment': comment})
1618 1624
1619 1625 return {
1620 1626 'comment_history_id': comment_history.comment_history_id,
1621 1627 'comment_id': comment.comment_id,
1622 1628 'comment_version': comment_history.version,
1623 1629 'comment_author_username': comment_history.author.username,
1624 1630 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
1625 1631 'comment_created_on': h.age_component(comment_history.created_on,
1626 1632 time_is_local=True),
1627 1633 }
1628 1634 else:
1629 1635 log.warning('No permissions for user %s to edit comment_id: %s',
1630 1636 self._rhodecode_db_user, comment_id)
1631 1637 raise HTTPNotFound()
@@ -1,162 +1,162 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import os
22 22 import sys
23 23 import json
24 24 import logging
25 25
26 26 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
27 27 from rhodecode.lib.vcs.conf import settings as vcs_settings
28 28 from rhodecode.model.scm import ScmModel
29 29
30 30 log = logging.getLogger(__name__)
31 31
32 32
33 33 class VcsServer(object):
34 34 _path = None # set executable path for hg/git/svn binary
35 35 backend = None # set in child classes
36 36 tunnel = None # subprocess handling tunnel
37 37 write_perms = ['repository.admin', 'repository.write']
38 38 read_perms = ['repository.read', 'repository.admin', 'repository.write']
39 39
40 40 def __init__(self, user, user_permissions, config, env):
41 41 self.user = user
42 42 self.user_permissions = user_permissions
43 43 self.config = config
44 44 self.env = env
45 45 self.stdin = sys.stdin
46 46
47 47 self.repo_name = None
48 48 self.repo_mode = None
49 49 self.store = ''
50 50 self.ini_path = ''
51 51
52 52 def _invalidate_cache(self, repo_name):
53 53 """
54 54 Set's cache for this repository for invalidation on next access
55 55
56 56 :param repo_name: full repo name, also a cache key
57 57 """
58 58 ScmModel().mark_for_invalidation(repo_name)
59 59
60 60 def has_write_perm(self):
61 61 permission = self.user_permissions.get(self.repo_name)
62 62 if permission in ['repository.write', 'repository.admin']:
63 63 return True
64 64
65 65 return False
66 66
67 67 def _check_permissions(self, action):
68 68 permission = self.user_permissions.get(self.repo_name)
69 69 log.debug('permission for %s on %s are: %s',
70 70 self.user, self.repo_name, permission)
71 71
72 72 if not permission:
73 73 log.error('user `%s` permissions to repo:%s are empty. Forbidding access.',
74 74 self.user, self.repo_name)
75 75 return -2
76 76
77 77 if action == 'pull':
78 78 if permission in self.read_perms:
79 79 log.info(
80 80 'READ Permissions for User "%s" detected to repo "%s"!',
81 81 self.user, self.repo_name)
82 82 return 0
83 83 else:
84 84 if permission in self.write_perms:
85 85 log.info(
86 'WRITE+ Permissions for User "%s" detected to repo "%s"!',
86 'WRITE, or Higher Permissions for User "%s" detected to repo "%s"!',
87 87 self.user, self.repo_name)
88 88 return 0
89 89
90 90 log.error('Cannot properly fetch or verify user `%s` permissions. '
91 91 'Permissions: %s, vcs action: %s',
92 92 self.user, permission, action)
93 93 return -2
94 94
95 95 def update_environment(self, action, extras=None):
96 96
97 97 scm_data = {
98 98 'ip': os.environ['SSH_CLIENT'].split()[0],
99 99 'username': self.user.username,
100 100 'user_id': self.user.user_id,
101 101 'action': action,
102 102 'repository': self.repo_name,
103 103 'scm': self.backend,
104 104 'config': self.ini_path,
105 105 'repo_store': self.store,
106 106 'make_lock': None,
107 107 'locked_by': [None, None],
108 108 'server_url': None,
109 109 'user_agent': 'ssh-user-agent',
110 110 'hooks': ['push', 'pull'],
111 111 'hooks_module': 'rhodecode.lib.hooks_daemon',
112 112 'is_shadow_repo': False,
113 113 'detect_force_push': False,
114 114 'check_branch_perms': False,
115 115
116 116 'SSH': True,
117 117 'SSH_PERMISSIONS': self.user_permissions.get(self.repo_name),
118 118 }
119 119 if extras:
120 120 scm_data.update(extras)
121 121 os.putenv("RC_SCM_DATA", json.dumps(scm_data))
122 122
123 123 def get_root_store(self):
124 124 root_store = self.store
125 125 if not root_store.endswith('/'):
126 126 # always append trailing slash
127 127 root_store = root_store + '/'
128 128 return root_store
129 129
130 130 def _handle_tunnel(self, extras):
131 131 # pre-auth
132 132 action = 'pull'
133 133 exit_code = self._check_permissions(action)
134 134 if exit_code:
135 135 return exit_code, False
136 136
137 137 req = self.env['request']
138 138 server_url = req.host_url + req.script_name
139 139 extras['server_url'] = server_url
140 140
141 141 log.debug('Using %s binaries from path %s', self.backend, self._path)
142 142 exit_code = self.tunnel.run(extras)
143 143
144 144 return exit_code, action == "push"
145 145
146 146 def run(self, tunnel_extras=None):
147 147 tunnel_extras = tunnel_extras or {}
148 148 extras = {}
149 149 extras.update(tunnel_extras)
150 150
151 151 callback_daemon, extras = prepare_callback_daemon(
152 152 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
153 153 host=vcs_settings.HOOKS_HOST,
154 154 use_direct_calls=False)
155 155
156 156 with callback_daemon:
157 157 try:
158 158 return self._handle_tunnel(extras)
159 159 finally:
160 160 log.debug('Running cleanup with cache invalidation')
161 161 if self.repo_name:
162 162 self._invalidate_cache(self.repo_name)
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 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