##// END OF EJS Templates
security: escape flash messaged VCS errors to prevent XSS atacks.
ergo -
r1838:b8e3feed default
parent child Browse files
Show More
@@ -1,260 +1,257 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 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 """
22 22 changelog controller for rhodecode
23 23 """
24 24
25 25 import logging
26 26
27 27 from pylons import request, url, session, tmpl_context as c
28 28 from pylons.controllers.util import redirect
29 29 from pylons.i18n.translation import _
30 30 from webob.exc import HTTPNotFound, HTTPBadRequest
31 31
32 32 import rhodecode.lib.helpers as h
33 33 from rhodecode.lib.auth import (
34 34 LoginRequired, HasRepoPermissionAnyDecorator, XHRRequired)
35 35 from rhodecode.lib.base import BaseRepoController, render
36 36 from rhodecode.lib.ext_json import json
37 37 from rhodecode.lib.graphmod import _colored, _dagwalker
38 38 from rhodecode.lib.helpers import RepoPage
39 39 from rhodecode.lib.utils2 import safe_int, safe_str
40 40 from rhodecode.lib.vcs.exceptions import (
41 41 RepositoryError, CommitDoesNotExistError,
42 42 CommitError, NodeDoesNotExistError, EmptyRepositoryError)
43 43
44 44 log = logging.getLogger(__name__)
45 45
46 46 DEFAULT_CHANGELOG_SIZE = 20
47 47
48 48
49 49 class ChangelogController(BaseRepoController):
50 50
51 51 def __before__(self):
52 52 super(ChangelogController, self).__before__()
53 53 c.affected_files_cut_off = 60
54 54
55 55 def __get_commit_or_redirect(
56 56 self, commit_id, repo, redirect_after=True, partial=False):
57 57 """
58 58 This is a safe way to get a commit. If an error occurs it
59 59 redirects to a commit with a proper message. If partial is set
60 60 then it does not do redirect raise and throws an exception instead.
61 61
62 62 :param commit_id: commit to fetch
63 63 :param repo: repo instance
64 64 """
65 65 try:
66 66 return c.rhodecode_repo.get_commit(commit_id)
67 67 except EmptyRepositoryError:
68 68 if not redirect_after:
69 69 return None
70 h.flash(h.literal(_('There are no commits yet')),
71 category='warning')
70 h.flash(_('There are no commits yet'), category='warning')
72 71 redirect(url('changelog_home', repo_name=repo.repo_name))
73 72 except RepositoryError as e:
74 msg = safe_str(e)
75 log.exception(msg)
76 h.flash(msg, category='warning')
73 log.exception(safe_str(e))
74 h.flash(safe_str(h.escape(e)), category='warning')
77 75 if not partial:
78 76 redirect(h.url('changelog_home', repo_name=repo.repo_name))
79 77 raise HTTPBadRequest()
80 78
81 79 def _graph(self, repo, commits, prev_data=None, next_data=None):
82 80 """
83 81 Generates a DAG graph for repo
84 82
85 83 :param repo: repo instance
86 84 :param commits: list of commits
87 85 """
88 86 if not commits:
89 87 return json.dumps([])
90 88
91 89 def serialize(commit, parents=True):
92 90 data = dict(
93 91 raw_id=commit.raw_id,
94 92 idx=commit.idx,
95 93 branch=commit.branch,
96 94 )
97 95 if parents:
98 96 data['parents'] = [
99 97 serialize(x, parents=False) for x in commit.parents]
100 98 return data
101 99
102 100 prev_data = prev_data or []
103 101 next_data = next_data or []
104 102
105 103 current = [serialize(x) for x in commits]
106 104 commits = prev_data + current + next_data
107 105
108 106 dag = _dagwalker(repo, commits)
109 107
110 108 data = [[commit_id, vtx, edges, branch]
111 109 for commit_id, vtx, edges, branch in _colored(dag)]
112 110 return json.dumps(data), json.dumps(current)
113 111
114 112 def _check_if_valid_branch(self, branch_name, repo_name, f_path):
115 113 if branch_name not in c.rhodecode_repo.branches_all:
116 h.flash('Branch {} is not found.'.format(branch_name),
114 h.flash('Branch {} is not found.'.format(h.escape(branch_name)),
117 115 category='warning')
118 116 redirect(url('changelog_file_home', repo_name=repo_name,
119 117 revision=branch_name, f_path=f_path or ''))
120 118
121 119 def _load_changelog_data(self, collection, page, chunk_size, branch_name=None, dynamic=False):
122 120 c.total_cs = len(collection)
123 121 c.showing_commits = min(chunk_size, c.total_cs)
124 122 c.pagination = RepoPage(collection, page=page, item_count=c.total_cs,
125 123 items_per_page=chunk_size, branch=branch_name)
126 124
127 125 c.next_page = c.pagination.next_page
128 126 c.prev_page = c.pagination.previous_page
129 127
130 128 if dynamic:
131 129 if request.GET.get('chunk') != 'next':
132 130 c.next_page = None
133 131 if request.GET.get('chunk') != 'prev':
134 132 c.prev_page = None
135 133
136 134 page_commit_ids = [x.raw_id for x in c.pagination]
137 135 c.comments = c.rhodecode_db_repo.get_comments(page_commit_ids)
138 136 c.statuses = c.rhodecode_db_repo.statuses(page_commit_ids)
139 137
140 138 @LoginRequired()
141 139 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
142 140 'repository.admin')
143 141 def index(self, repo_name, revision=None, f_path=None):
144 142 commit_id = revision
145 143 chunk_size = 20
146 144
147 145 c.branch_name = branch_name = request.GET.get('branch', None)
148 146 c.book_name = book_name = request.GET.get('bookmark', None)
149 147 hist_limit = safe_int(request.GET.get('limit')) or None
150 148
151 149 p = safe_int(request.GET.get('page', 1), 1)
152 150
153 151 c.selected_name = branch_name or book_name
154 152 if not commit_id and branch_name:
155 153 self._check_if_valid_branch(branch_name, repo_name, f_path)
156 154
157 155 c.changelog_for_path = f_path
158 156 pre_load = ['author', 'branch', 'date', 'message', 'parents']
159 157 commit_ids = []
160 158
161 159 try:
162 160 if f_path:
163 161 log.debug('generating changelog for path %s', f_path)
164 162 # get the history for the file !
165 163 base_commit = c.rhodecode_repo.get_commit(revision)
166 164 try:
167 165 collection = base_commit.get_file_history(
168 166 f_path, limit=hist_limit, pre_load=pre_load)
169 167 if (collection
170 168 and request.environ.get('HTTP_X_PARTIAL_XHR')):
171 169 # for ajax call we remove first one since we're looking
172 170 # at it right now in the context of a file commit
173 171 collection.pop(0)
174 172 except (NodeDoesNotExistError, CommitError):
175 173 # this node is not present at tip!
176 174 try:
177 175 commit = self.__get_commit_or_redirect(
178 176 commit_id, repo_name)
179 177 collection = commit.get_file_history(f_path)
180 178 except RepositoryError as e:
181 179 h.flash(safe_str(e), category='warning')
182 180 redirect(h.url('changelog_home', repo_name=repo_name))
183 181 collection = list(reversed(collection))
184 182 else:
185 183 collection = c.rhodecode_repo.get_commits(
186 184 branch_name=branch_name, pre_load=pre_load)
187 185
188 186 self._load_changelog_data(
189 187 collection, p, chunk_size, c.branch_name, dynamic=f_path)
190 188
191 189 except EmptyRepositoryError as e:
192 h.flash(safe_str(e), category='warning')
190 h.flash(safe_str(h.escape(e)), category='warning')
193 191 return redirect(h.route_path('repo_summary', repo_name=repo_name))
194 192 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
195 msg = safe_str(e)
196 log.exception(msg)
197 h.flash(msg, category='error')
193 log.exception(safe_str(e))
194 h.flash(safe_str(h.escape(e)), category='error')
198 195 return redirect(url('changelog_home', repo_name=repo_name))
199 196
200 197 if (request.environ.get('HTTP_X_PARTIAL_XHR')
201 198 or request.environ.get('HTTP_X_PJAX')):
202 199 # loading from ajax, we don't want the first result, it's popped
203 200 return render('changelog/changelog_file_history.mako')
204 201
205 202 if not f_path:
206 203 commit_ids = c.pagination
207 204
208 205 c.graph_data, c.graph_commits = self._graph(
209 206 c.rhodecode_repo, commit_ids)
210 207
211 208 return render('changelog/changelog.mako')
212 209
213 210 @LoginRequired()
214 211 @XHRRequired()
215 212 @HasRepoPermissionAnyDecorator(
216 213 'repository.read', 'repository.write', 'repository.admin')
217 214 def changelog_elements(self, repo_name):
218 215 commit_id = None
219 216 chunk_size = 20
220 217
221 218 def wrap_for_error(err):
222 219 return '<tr><td colspan="9" class="alert alert-error">ERROR: {}</td></tr>'.format(err)
223 220
224 221 c.branch_name = branch_name = request.GET.get('branch', None)
225 222 c.book_name = book_name = request.GET.get('bookmark', None)
226 223
227 224 p = safe_int(request.GET.get('page', 1), 1)
228 225
229 226 c.selected_name = branch_name or book_name
230 227 if not commit_id and branch_name:
231 228 if branch_name not in c.rhodecode_repo.branches_all:
232 229 return wrap_for_error(
233 230 safe_str('Missing branch: {}'.format(branch_name)))
234 231
235 232 pre_load = ['author', 'branch', 'date', 'message', 'parents']
236 233 collection = c.rhodecode_repo.get_commits(
237 234 branch_name=branch_name, pre_load=pre_load)
238 235
239 236 try:
240 237 self._load_changelog_data(collection, p, chunk_size, dynamic=True)
241 238 except EmptyRepositoryError as e:
242 239 return wrap_for_error(safe_str(e))
243 240 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
244 241 log.exception('Failed to fetch commits')
245 242 return wrap_for_error(safe_str(e))
246 243
247 244 prev_data = None
248 245 next_data = None
249 246
250 247 prev_graph = json.loads(request.POST.get('graph', ''))
251 248
252 249 if request.GET.get('chunk') == 'prev':
253 250 next_data = prev_graph
254 251 elif request.GET.get('chunk') == 'next':
255 252 prev_data = prev_graph
256 253
257 254 c.graph_data, c.graph_commits = self._graph(
258 255 c.rhodecode_repo, c.pagination,
259 256 prev_data=prev_data, next_data=next_data)
260 257 return render('changelog/changelog_elements.mako')
@@ -1,282 +1,284 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2017 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 """
22 22 Compare controller for showing differences between two commits/refs/tags etc.
23 23 """
24 24
25 25 import logging
26 26
27 from webob.exc import HTTPBadRequest
27 from webob.exc import HTTPBadRequest, HTTPNotFound
28 28 from pylons import request, tmpl_context as c, url
29 29 from pylons.controllers.util import redirect
30 30 from pylons.i18n.translation import _
31 31
32 32 from rhodecode.controllers.utils import parse_path_ref, get_commit_from_ref_name
33 33 from rhodecode.lib import helpers as h
34 34 from rhodecode.lib import diffs, codeblocks
35 35 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
36 36 from rhodecode.lib.base import BaseRepoController, render
37 37 from rhodecode.lib.utils import safe_str
38 38 from rhodecode.lib.utils2 import safe_unicode, str2bool
39 39 from rhodecode.lib.vcs.exceptions import (
40 40 EmptyRepositoryError, RepositoryError, RepositoryRequirementError,
41 41 NodeDoesNotExistError)
42 42 from rhodecode.model.db import Repository, ChangesetStatus
43 43
44 44 log = logging.getLogger(__name__)
45 45
46 46
47 47 class CompareController(BaseRepoController):
48 48
49 49 def __before__(self):
50 50 super(CompareController, self).__before__()
51 51
52 52 def _get_commit_or_redirect(
53 53 self, ref, ref_type, repo, redirect_after=True, partial=False):
54 54 """
55 55 This is a safe way to get a commit. If an error occurs it
56 56 redirects to a commit with a proper message. If partial is set
57 57 then it does not do redirect raise and throws an exception instead.
58 58 """
59 59 try:
60 60 return get_commit_from_ref_name(repo, safe_str(ref), ref_type)
61 61 except EmptyRepositoryError:
62 62 if not redirect_after:
63 63 return repo.scm_instance().EMPTY_COMMIT
64 64 h.flash(h.literal(_('There are no commits yet')),
65 65 category='warning')
66 66 redirect(h.route_path('repo_summary', repo_name=repo.repo_name))
67 67
68 68 except RepositoryError as e:
69 msg = safe_str(e)
70 log.exception(msg)
71 h.flash(msg, category='warning')
69 log.exception(safe_str(e))
70 h.flash(safe_str(h.escape(e)), category='warning')
72 71 if not partial:
73 72 redirect(h.route_path('repo_summary', repo_name=repo.repo_name))
74 73 raise HTTPBadRequest()
75 74
76 75 @LoginRequired()
77 76 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
78 77 'repository.admin')
79 78 def index(self, repo_name):
80 79 c.compare_home = True
81 80 c.commit_ranges = []
82 81 c.collapse_all_commits = False
83 82 c.diffset = None
84 83 c.limited_diff = False
85 84 source_repo = c.rhodecode_db_repo.repo_name
86 85 target_repo = request.GET.get('target_repo', source_repo)
87 86 c.source_repo = Repository.get_by_repo_name(source_repo)
88 87 c.target_repo = Repository.get_by_repo_name(target_repo)
88
89 if c.source_repo is None or c.target_repo is None:
90 raise HTTPNotFound()
91
89 92 c.source_ref = c.target_ref = _('Select commit')
90 93 c.source_ref_type = ""
91 94 c.target_ref_type = ""
92 95 c.commit_statuses = ChangesetStatus.STATUSES
93 96 c.preview_mode = False
94 97 c.file_path = None
95 98 return render('compare/compare_diff.mako')
96 99
97 100 @LoginRequired()
98 101 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
99 102 'repository.admin')
100 103 def compare(self, repo_name, source_ref_type, source_ref,
101 104 target_ref_type, target_ref):
102 105 # source_ref will be evaluated in source_repo
103 106 source_repo_name = c.rhodecode_db_repo.repo_name
104 107 source_path, source_id = parse_path_ref(source_ref)
105 108
106 109 # target_ref will be evaluated in target_repo
107 110 target_repo_name = request.GET.get('target_repo', source_repo_name)
108 111 target_path, target_id = parse_path_ref(
109 112 target_ref, default_path=request.GET.get('f_path', ''))
110 113
111 114 c.file_path = target_path
112 115 c.commit_statuses = ChangesetStatus.STATUSES
113 116
114 117 # if merge is True
115 118 # Show what changes since the shared ancestor commit of target/source
116 119 # the source would get if it was merged with target. Only commits
117 120 # which are in target but not in source will be shown.
118 121 merge = str2bool(request.GET.get('merge'))
119 122 # if merge is False
120 123 # Show a raw diff of source/target refs even if no ancestor exists
121 124
122 125 # c.fulldiff disables cut_off_limit
123 126 c.fulldiff = str2bool(request.GET.get('fulldiff'))
124 127
125 128 # if partial, returns just compare_commits.html (commits log)
126 129 partial = request.is_xhr
127 130
128 131 # swap url for compare_diff page
129 132 c.swap_url = h.url(
130 133 'compare_url',
131 134 repo_name=target_repo_name,
132 135 source_ref_type=target_ref_type,
133 136 source_ref=target_ref,
134 137 target_repo=source_repo_name,
135 138 target_ref_type=source_ref_type,
136 139 target_ref=source_ref,
137 140 merge=merge and '1' or '',
138 141 f_path=target_path)
139 142
140 143 source_repo = Repository.get_by_repo_name(source_repo_name)
141 144 target_repo = Repository.get_by_repo_name(target_repo_name)
142 145
143 146 if source_repo is None:
144 msg = _('Could not find the original repo: %(repo)s') % {
145 'repo': source_repo}
146
147 log.error(msg)
148 h.flash(msg, category='error')
147 log.error('Could not find the source repo: {}'
148 .format(source_repo_name))
149 h.flash(_('Could not find the source repo: `{}`')
150 .format(h.escape(source_repo_name)), category='error')
149 151 return redirect(url('compare_home', repo_name=c.repo_name))
150 152
151 153 if target_repo is None:
152 msg = _('Could not find the other repo: %(repo)s') % {
153 'repo': target_repo_name}
154 log.error(msg)
155 h.flash(msg, category='error')
154 log.error('Could not find the target repo: {}'
155 .format(source_repo_name))
156 h.flash(_('Could not find the target repo: `{}`')
157 .format(h.escape(target_repo_name)), category='error')
156 158 return redirect(url('compare_home', repo_name=c.repo_name))
157 159
158 160 source_scm = source_repo.scm_instance()
159 161 target_scm = target_repo.scm_instance()
160 162
161 163 source_alias = source_scm.alias
162 164 target_alias = target_scm.alias
163 165 if source_alias != target_alias:
164 166 msg = _('The comparison of two different kinds of remote repos '
165 167 'is not available')
166 168 log.error(msg)
167 169 h.flash(msg, category='error')
168 170 return redirect(url('compare_home', repo_name=c.repo_name))
169 171
170 172 source_commit = self._get_commit_or_redirect(
171 173 ref=source_id, ref_type=source_ref_type, repo=source_repo,
172 174 partial=partial)
173 175 target_commit = self._get_commit_or_redirect(
174 176 ref=target_id, ref_type=target_ref_type, repo=target_repo,
175 177 partial=partial)
176 178
177 179 c.compare_home = False
178 180 c.source_repo = source_repo
179 181 c.target_repo = target_repo
180 182 c.source_ref = source_ref
181 183 c.target_ref = target_ref
182 184 c.source_ref_type = source_ref_type
183 185 c.target_ref_type = target_ref_type
184 186
185 187 pre_load = ["author", "branch", "date", "message"]
186 188 c.ancestor = None
187 189
188 190 if c.file_path:
189 191 if source_commit == target_commit:
190 192 c.commit_ranges = []
191 193 else:
192 194 c.commit_ranges = [target_commit]
193 195 else:
194 196 try:
195 197 c.commit_ranges = source_scm.compare(
196 198 source_commit.raw_id, target_commit.raw_id,
197 199 target_scm, merge, pre_load=pre_load)
198 200 if merge:
199 201 c.ancestor = source_scm.get_common_ancestor(
200 202 source_commit.raw_id, target_commit.raw_id, target_scm)
201 203 except RepositoryRequirementError:
202 204 msg = _('Could not compare repos with different '
203 205 'large file settings')
204 206 log.error(msg)
205 207 if partial:
206 208 return msg
207 209 h.flash(msg, category='error')
208 210 return redirect(url('compare_home', repo_name=c.repo_name))
209 211
210 212 c.statuses = c.rhodecode_db_repo.statuses(
211 213 [x.raw_id for x in c.commit_ranges])
212 214
213 215 # auto collapse if we have more than limit
214 216 collapse_limit = diffs.DiffProcessor._collapse_commits_over
215 217 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
216 218
217 219 if partial: # for PR ajax commits loader
218 220 if not c.ancestor:
219 221 return '' # cannot merge if there is no ancestor
220 222 return render('compare/compare_commits.mako')
221 223
222 224 if c.ancestor:
223 225 # case we want a simple diff without incoming commits,
224 226 # previewing what will be merged.
225 227 # Make the diff on target repo (which is known to have target_ref)
226 228 log.debug('Using ancestor %s as source_ref instead of %s'
227 229 % (c.ancestor, source_ref))
228 230 source_repo = target_repo
229 231 source_commit = target_repo.get_commit(commit_id=c.ancestor)
230 232
231 233 # diff_limit will cut off the whole diff if the limit is applied
232 234 # otherwise it will just hide the big files from the front-end
233 235 diff_limit = self.cut_off_limit_diff
234 236 file_limit = self.cut_off_limit_file
235 237
236 238 log.debug('calculating diff between '
237 239 'source_ref:%s and target_ref:%s for repo `%s`',
238 240 source_commit, target_commit,
239 241 safe_unicode(source_repo.scm_instance().path))
240 242
241 243 if source_commit.repository != target_commit.repository:
242 244 msg = _(
243 245 "Repositories unrelated. "
244 246 "Cannot compare commit %(commit1)s from repository %(repo1)s "
245 247 "with commit %(commit2)s from repository %(repo2)s.") % {
246 248 'commit1': h.show_id(source_commit),
247 249 'repo1': source_repo.repo_name,
248 250 'commit2': h.show_id(target_commit),
249 251 'repo2': target_repo.repo_name,
250 252 }
251 253 h.flash(msg, category='error')
252 254 raise HTTPBadRequest()
253 255
254 256 txtdiff = source_repo.scm_instance().get_diff(
255 257 commit1=source_commit, commit2=target_commit,
256 258 path=target_path, path1=source_path)
257 259
258 260 diff_processor = diffs.DiffProcessor(
259 261 txtdiff, format='newdiff', diff_limit=diff_limit,
260 262 file_limit=file_limit, show_full_diff=c.fulldiff)
261 263 _parsed = diff_processor.prepare()
262 264
263 265 def _node_getter(commit):
264 266 """ Returns a function that returns a node for a commit or None """
265 267 def get_node(fname):
266 268 try:
267 269 return commit.get_node(fname)
268 270 except NodeDoesNotExistError:
269 271 return None
270 272 return get_node
271 273
272 274 c.diffset = codeblocks.DiffSet(
273 275 repo_name=source_repo.repo_name,
274 276 source_node_getter=_node_getter(source_commit),
275 277 target_node_getter=_node_getter(target_commit),
276 278 ).render_patchset(_parsed, source_ref, target_ref)
277 279
278 280 c.preview_mode = merge
279 281 c.source_commit = source_commit
280 282 c.target_commit = target_commit
281 283
282 284 return render('compare/compare_diff.mako')
@@ -1,1110 +1,1109 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 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 """
22 22 Files controller for RhodeCode Enterprise
23 23 """
24 24
25 25 import itertools
26 26 import logging
27 27 import os
28 28 import shutil
29 29 import tempfile
30 30
31 31 from pylons import request, response, tmpl_context as c, url
32 32 from pylons.i18n.translation import _
33 33 from pylons.controllers.util import redirect
34 34 from webob.exc import HTTPNotFound, HTTPBadRequest
35 35
36 36 from rhodecode.controllers.utils import parse_path_ref
37 37 from rhodecode.lib import diffs, helpers as h, caches
38 38 from rhodecode.lib import audit_logger
39 39 from rhodecode.lib.codeblocks import (
40 40 filenode_as_lines_tokens, filenode_as_annotated_lines_tokens)
41 41 from rhodecode.lib.utils import jsonify
42 42 from rhodecode.lib.utils2 import (
43 43 convert_line_endings, detect_mode, safe_str, str2bool)
44 44 from rhodecode.lib.auth import (
45 45 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired, XHRRequired)
46 46 from rhodecode.lib.base import BaseRepoController, render
47 47 from rhodecode.lib.vcs import path as vcspath
48 48 from rhodecode.lib.vcs.backends.base import EmptyCommit
49 49 from rhodecode.lib.vcs.conf import settings
50 50 from rhodecode.lib.vcs.exceptions import (
51 51 RepositoryError, CommitDoesNotExistError, EmptyRepositoryError,
52 52 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError,
53 53 NodeDoesNotExistError, CommitError, NodeError)
54 54 from rhodecode.lib.vcs.nodes import FileNode
55 55
56 56 from rhodecode.model.repo import RepoModel
57 57 from rhodecode.model.scm import ScmModel
58 58 from rhodecode.model.db import Repository
59 59
60 60 from rhodecode.controllers.changeset import (
61 61 _ignorews_url, _context_url, get_line_ctx, get_ignore_ws)
62 62 from rhodecode.lib.exceptions import NonRelativePathError
63 63
64 64 log = logging.getLogger(__name__)
65 65
66 66
67 67 class FilesController(BaseRepoController):
68 68
69 69 def __before__(self):
70 70 super(FilesController, self).__before__()
71 71 c.cut_off_limit = self.cut_off_limit_file
72 72
73 73 def _get_default_encoding(self):
74 74 enc_list = getattr(c, 'default_encodings', [])
75 75 return enc_list[0] if enc_list else 'UTF-8'
76 76
77 77 def __get_commit_or_redirect(self, commit_id, repo_name,
78 78 redirect_after=True):
79 79 """
80 80 This is a safe way to get commit. If an error occurs it redirects to
81 81 tip with proper message
82 82
83 83 :param commit_id: id of commit to fetch
84 84 :param repo_name: repo name to redirect after
85 85 :param redirect_after: toggle redirection
86 86 """
87 87 try:
88 88 return c.rhodecode_repo.get_commit(commit_id)
89 89 except EmptyRepositoryError:
90 90 if not redirect_after:
91 91 return None
92 92 url_ = url('files_add_home',
93 93 repo_name=c.repo_name,
94 94 revision=0, f_path='', anchor='edit')
95 95 if h.HasRepoPermissionAny(
96 96 'repository.write', 'repository.admin')(c.repo_name):
97 97 add_new = h.link_to(
98 98 _('Click here to add a new file.'),
99 99 url_, class_="alert-link")
100 100 else:
101 101 add_new = ""
102 102 h.flash(h.literal(
103 103 _('There are no files yet. %s') % add_new), category='warning')
104 104 redirect(h.route_path('repo_summary', repo_name=repo_name))
105 105 except (CommitDoesNotExistError, LookupError):
106 106 msg = _('No such commit exists for this repository')
107 107 h.flash(msg, category='error')
108 108 raise HTTPNotFound()
109 109 except RepositoryError as e:
110 h.flash(safe_str(e), category='error')
110 h.flash(safe_str(h.escape(e)), category='error')
111 111 raise HTTPNotFound()
112 112
113 113 def __get_filenode_or_redirect(self, repo_name, commit, path):
114 114 """
115 115 Returns file_node, if error occurs or given path is directory,
116 116 it'll redirect to top level path
117 117
118 118 :param repo_name: repo_name
119 119 :param commit: given commit
120 120 :param path: path to lookup
121 121 """
122 122 try:
123 123 file_node = commit.get_node(path)
124 124 if file_node.is_dir():
125 125 raise RepositoryError('The given path is a directory')
126 126 except CommitDoesNotExistError:
127 127 log.exception('No such commit exists for this repository')
128 128 h.flash(_('No such commit exists for this repository'), category='error')
129 129 raise HTTPNotFound()
130 130 except RepositoryError as e:
131 h.flash(safe_str(e), category='error')
131 h.flash(safe_str(h.escape(e)), category='error')
132 132 raise HTTPNotFound()
133 133
134 134 return file_node
135 135
136 136 def __get_tree_cache_manager(self, repo_name, namespace_type):
137 137 _namespace = caches.get_repo_namespace_key(namespace_type, repo_name)
138 138 return caches.get_cache_manager('repo_cache_long', _namespace)
139 139
140 140 def _get_tree_at_commit(self, repo_name, commit_id, f_path,
141 141 full_load=False, force=False):
142 142 def _cached_tree():
143 143 log.debug('Generating cached file tree for %s, %s, %s',
144 144 repo_name, commit_id, f_path)
145 145 c.full_load = full_load
146 146 return render('files/files_browser_tree.mako')
147 147
148 148 cache_manager = self.__get_tree_cache_manager(
149 149 repo_name, caches.FILE_TREE)
150 150
151 151 cache_key = caches.compute_key_from_params(
152 152 repo_name, commit_id, f_path)
153 153
154 154 if force:
155 155 # we want to force recompute of caches
156 156 cache_manager.remove_value(cache_key)
157 157
158 158 return cache_manager.get(cache_key, createfunc=_cached_tree)
159 159
160 160 def _get_nodelist_at_commit(self, repo_name, commit_id, f_path):
161 161 def _cached_nodes():
162 162 log.debug('Generating cached nodelist for %s, %s, %s',
163 163 repo_name, commit_id, f_path)
164 164 _d, _f = ScmModel().get_nodes(
165 165 repo_name, commit_id, f_path, flat=False)
166 166 return _d + _f
167 167
168 168 cache_manager = self.__get_tree_cache_manager(
169 169 repo_name, caches.FILE_SEARCH_TREE_META)
170 170
171 171 cache_key = caches.compute_key_from_params(
172 172 repo_name, commit_id, f_path)
173 173 return cache_manager.get(cache_key, createfunc=_cached_nodes)
174 174
175 175 @LoginRequired()
176 176 @HasRepoPermissionAnyDecorator(
177 177 'repository.read', 'repository.write', 'repository.admin')
178 178 def index(
179 179 self, repo_name, revision, f_path, annotate=False, rendered=False):
180 180 commit_id = revision
181 181
182 182 # redirect to given commit_id from form if given
183 183 get_commit_id = request.GET.get('at_rev', None)
184 184 if get_commit_id:
185 185 self.__get_commit_or_redirect(get_commit_id, repo_name)
186 186
187 187 c.commit = self.__get_commit_or_redirect(commit_id, repo_name)
188 188 c.branch = request.GET.get('branch', None)
189 189 c.f_path = f_path
190 190 c.annotate = annotate
191 191 # default is false, but .rst/.md files later are autorendered, we can
192 192 # overwrite autorendering by setting this GET flag
193 193 c.renderer = rendered or not request.GET.get('no-render', False)
194 194
195 195 # prev link
196 196 try:
197 197 prev_commit = c.commit.prev(c.branch)
198 198 c.prev_commit = prev_commit
199 199 c.url_prev = url('files_home', repo_name=c.repo_name,
200 200 revision=prev_commit.raw_id, f_path=f_path)
201 201 if c.branch:
202 202 c.url_prev += '?branch=%s' % c.branch
203 203 except (CommitDoesNotExistError, VCSError):
204 204 c.url_prev = '#'
205 205 c.prev_commit = EmptyCommit()
206 206
207 207 # next link
208 208 try:
209 209 next_commit = c.commit.next(c.branch)
210 210 c.next_commit = next_commit
211 211 c.url_next = url('files_home', repo_name=c.repo_name,
212 212 revision=next_commit.raw_id, f_path=f_path)
213 213 if c.branch:
214 214 c.url_next += '?branch=%s' % c.branch
215 215 except (CommitDoesNotExistError, VCSError):
216 216 c.url_next = '#'
217 217 c.next_commit = EmptyCommit()
218 218
219 219 # files or dirs
220 220 try:
221 221 c.file = c.commit.get_node(f_path)
222 222 c.file_author = True
223 223 c.file_tree = ''
224 224 if c.file.is_file():
225 225 c.lf_node = c.file.get_largefile_node()
226 226
227 227 c.file_source_page = 'true'
228 228 c.file_last_commit = c.file.last_commit
229 229 if c.file.size < self.cut_off_limit_file:
230 230 if c.annotate: # annotation has precedence over renderer
231 231 c.annotated_lines = filenode_as_annotated_lines_tokens(
232 232 c.file
233 233 )
234 234 else:
235 235 c.renderer = (
236 236 c.renderer and h.renderer_from_filename(c.file.path)
237 237 )
238 238 if not c.renderer:
239 239 c.lines = filenode_as_lines_tokens(c.file)
240 240
241 241 c.on_branch_head = self._is_valid_head(
242 242 commit_id, c.rhodecode_repo)
243 243
244 244 branch = c.commit.branch if (
245 245 c.commit.branch and '/' not in c.commit.branch) else None
246 246 c.branch_or_raw_id = branch or c.commit.raw_id
247 247 c.branch_name = c.commit.branch or h.short_id(c.commit.raw_id)
248 248
249 249 author = c.file_last_commit.author
250 250 c.authors = [(h.email(author),
251 251 h.person(author, 'username_or_name_or_email'))]
252 252 else:
253 253 c.file_source_page = 'false'
254 254 c.authors = []
255 255 c.file_tree = self._get_tree_at_commit(
256 256 repo_name, c.commit.raw_id, f_path)
257 257
258 258 except RepositoryError as e:
259 h.flash(safe_str(e), category='error')
259 h.flash(safe_str(h.escape(e)), category='error')
260 260 raise HTTPNotFound()
261 261
262 262 if request.environ.get('HTTP_X_PJAX'):
263 263 return render('files/files_pjax.mako')
264 264
265 265 return render('files/files.mako')
266 266
267 267 @LoginRequired()
268 268 @HasRepoPermissionAnyDecorator(
269 269 'repository.read', 'repository.write', 'repository.admin')
270 270 def annotate_previous(self, repo_name, revision, f_path):
271 271
272 272 commit_id = revision
273 273 commit = self.__get_commit_or_redirect(commit_id, repo_name)
274 274 prev_commit_id = commit.raw_id
275 275
276 276 f_path = f_path
277 277 is_file = False
278 278 try:
279 279 _file = commit.get_node(f_path)
280 280 is_file = _file.is_file()
281 281 except (NodeDoesNotExistError, CommitDoesNotExistError, VCSError):
282 282 pass
283 283
284 284 if is_file:
285 285 history = commit.get_file_history(f_path)
286 286 prev_commit_id = history[1].raw_id \
287 287 if len(history) > 1 else prev_commit_id
288 288
289 289 return redirect(h.url(
290 290 'files_annotate_home', repo_name=repo_name,
291 291 revision=prev_commit_id, f_path=f_path))
292 292
293 293 @LoginRequired()
294 294 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
295 295 'repository.admin')
296 296 @jsonify
297 297 def history(self, repo_name, revision, f_path):
298 298 commit = self.__get_commit_or_redirect(revision, repo_name)
299 299 f_path = f_path
300 300 _file = commit.get_node(f_path)
301 301 if _file.is_file():
302 302 file_history, _hist = self._get_node_history(commit, f_path)
303 303
304 304 res = []
305 305 for obj in file_history:
306 306 res.append({
307 307 'text': obj[1],
308 308 'children': [{'id': o[0], 'text': o[1]} for o in obj[0]]
309 309 })
310 310
311 311 data = {
312 312 'more': False,
313 313 'results': res
314 314 }
315 315 return data
316 316
317 317 @LoginRequired()
318 318 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
319 319 'repository.admin')
320 320 def authors(self, repo_name, revision, f_path):
321 321 commit = self.__get_commit_or_redirect(revision, repo_name)
322 322 file_node = commit.get_node(f_path)
323 323 if file_node.is_file():
324 324 c.file_last_commit = file_node.last_commit
325 325 if request.GET.get('annotate') == '1':
326 326 # use _hist from annotation if annotation mode is on
327 327 commit_ids = set(x[1] for x in file_node.annotate)
328 328 _hist = (
329 329 c.rhodecode_repo.get_commit(commit_id)
330 330 for commit_id in commit_ids)
331 331 else:
332 332 _f_history, _hist = self._get_node_history(commit, f_path)
333 333 c.file_author = False
334 334 c.authors = []
335 335 for author in set(commit.author for commit in _hist):
336 336 c.authors.append((
337 337 h.email(author),
338 338 h.person(author, 'username_or_name_or_email')))
339 339 return render('files/file_authors_box.mako')
340 340
341 341 @LoginRequired()
342 342 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
343 343 'repository.admin')
344 344 def rawfile(self, repo_name, revision, f_path):
345 345 """
346 346 Action for download as raw
347 347 """
348 348 commit = self.__get_commit_or_redirect(revision, repo_name)
349 349 file_node = self.__get_filenode_or_redirect(repo_name, commit, f_path)
350 350
351 351 if request.GET.get('lf'):
352 352 # only if lf get flag is passed, we download this file
353 353 # as LFS/Largefile
354 354 lf_node = file_node.get_largefile_node()
355 355 if lf_node:
356 356 # overwrite our pointer with the REAL large-file
357 357 file_node = lf_node
358 358
359 359 response.content_disposition = 'attachment; filename=%s' % \
360 360 safe_str(f_path.split(Repository.NAME_SEP)[-1])
361 361
362 362 response.content_type = file_node.mimetype
363 363 charset = self._get_default_encoding()
364 364 if charset:
365 365 response.charset = charset
366 366
367 367 return file_node.content
368 368
369 369 @LoginRequired()
370 370 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
371 371 'repository.admin')
372 372 def raw(self, repo_name, revision, f_path):
373 373 """
374 374 Action for show as raw, some mimetypes are "rendered",
375 375 those include images, icons.
376 376 """
377 377 commit = self.__get_commit_or_redirect(revision, repo_name)
378 378 file_node = self.__get_filenode_or_redirect(repo_name, commit, f_path)
379 379
380 380 raw_mimetype_mapping = {
381 381 # map original mimetype to a mimetype used for "show as raw"
382 382 # you can also provide a content-disposition to override the
383 383 # default "attachment" disposition.
384 384 # orig_type: (new_type, new_dispo)
385 385
386 386 # show images inline:
387 387 # Do not re-add SVG: it is unsafe and permits XSS attacks. One can
388 388 # for example render an SVG with javascript inside or even render
389 389 # HTML.
390 390 'image/x-icon': ('image/x-icon', 'inline'),
391 391 'image/png': ('image/png', 'inline'),
392 392 'image/gif': ('image/gif', 'inline'),
393 393 'image/jpeg': ('image/jpeg', 'inline'),
394 394 'application/pdf': ('application/pdf', 'inline'),
395 395 }
396 396
397 397 mimetype = file_node.mimetype
398 398 try:
399 399 mimetype, dispo = raw_mimetype_mapping[mimetype]
400 400 except KeyError:
401 401 # we don't know anything special about this, handle it safely
402 402 if file_node.is_binary:
403 403 # do same as download raw for binary files
404 404 mimetype, dispo = 'application/octet-stream', 'attachment'
405 405 else:
406 406 # do not just use the original mimetype, but force text/plain,
407 407 # otherwise it would serve text/html and that might be unsafe.
408 408 # Note: underlying vcs library fakes text/plain mimetype if the
409 409 # mimetype can not be determined and it thinks it is not
410 410 # binary.This might lead to erroneous text display in some
411 411 # cases, but helps in other cases, like with text files
412 412 # without extension.
413 413 mimetype, dispo = 'text/plain', 'inline'
414 414
415 415 if dispo == 'attachment':
416 416 dispo = 'attachment; filename=%s' % safe_str(
417 417 f_path.split(os.sep)[-1])
418 418
419 419 response.content_disposition = dispo
420 420 response.content_type = mimetype
421 421 charset = self._get_default_encoding()
422 422 if charset:
423 423 response.charset = charset
424 424 return file_node.content
425 425
426 426 @CSRFRequired()
427 427 @LoginRequired()
428 428 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
429 429 def delete(self, repo_name, revision, f_path):
430 430 commit_id = revision
431 431
432 432 repo = c.rhodecode_db_repo
433 433 if repo.enable_locking and repo.locked[0]:
434 434 h.flash(_('This repository has been locked by %s on %s')
435 435 % (h.person_by_id(repo.locked[0]),
436 436 h.format_date(h.time_to_datetime(repo.locked[1]))),
437 437 'warning')
438 438 return redirect(h.url('files_home',
439 439 repo_name=repo_name, revision='tip'))
440 440
441 441 if not self._is_valid_head(commit_id, repo.scm_instance()):
442 442 h.flash(_('You can only delete files with revision '
443 443 'being a valid branch '), category='warning')
444 444 return redirect(h.url('files_home',
445 445 repo_name=repo_name, revision='tip',
446 446 f_path=f_path))
447 447
448 448 c.commit = self.__get_commit_or_redirect(commit_id, repo_name)
449 449 c.file = self.__get_filenode_or_redirect(repo_name, c.commit, f_path)
450 450
451 451 c.default_message = _(
452 452 'Deleted file {} via RhodeCode Enterprise').format(f_path)
453 453 c.f_path = f_path
454 454 node_path = f_path
455 455 author = c.rhodecode_user.full_contact
456 456 message = request.POST.get('message') or c.default_message
457 457 try:
458 458 nodes = {
459 459 node_path: {
460 460 'content': ''
461 461 }
462 462 }
463 463 self.scm_model.delete_nodes(
464 464 user=c.rhodecode_user.user_id, repo=c.rhodecode_db_repo,
465 465 message=message,
466 466 nodes=nodes,
467 467 parent_commit=c.commit,
468 468 author=author,
469 469 )
470 470
471 471 h.flash(
472 472 _('Successfully deleted file `{}`').format(
473 473 h.escape(f_path)), category='success')
474 474 except Exception:
475 msg = _('Error occurred during commit')
476 log.exception(msg)
477 h.flash(msg, category='error')
475 log.exception('Error during commit operation')
476 h.flash(_('Error occurred during commit'), category='error')
478 477 return redirect(url('changeset_home',
479 478 repo_name=c.repo_name, revision='tip'))
480 479
481 480 @LoginRequired()
482 481 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
483 482 def delete_home(self, repo_name, revision, f_path):
484 483 commit_id = revision
485 484
486 485 repo = c.rhodecode_db_repo
487 486 if repo.enable_locking and repo.locked[0]:
488 487 h.flash(_('This repository has been locked by %s on %s')
489 488 % (h.person_by_id(repo.locked[0]),
490 489 h.format_date(h.time_to_datetime(repo.locked[1]))),
491 490 'warning')
492 491 return redirect(h.url('files_home',
493 492 repo_name=repo_name, revision='tip'))
494 493
495 494 if not self._is_valid_head(commit_id, repo.scm_instance()):
496 495 h.flash(_('You can only delete files with revision '
497 496 'being a valid branch '), category='warning')
498 497 return redirect(h.url('files_home',
499 498 repo_name=repo_name, revision='tip',
500 499 f_path=f_path))
501 500
502 501 c.commit = self.__get_commit_or_redirect(commit_id, repo_name)
503 502 c.file = self.__get_filenode_or_redirect(repo_name, c.commit, f_path)
504 503
505 504 c.default_message = _(
506 505 'Deleted file {} via RhodeCode Enterprise').format(f_path)
507 506 c.f_path = f_path
508 507
509 508 return render('files/files_delete.mako')
510 509
511 510 @CSRFRequired()
512 511 @LoginRequired()
513 512 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
514 513 def edit(self, repo_name, revision, f_path):
515 514 commit_id = revision
516 515
517 516 repo = c.rhodecode_db_repo
518 517 if repo.enable_locking and repo.locked[0]:
519 518 h.flash(_('This repository has been locked by %s on %s')
520 519 % (h.person_by_id(repo.locked[0]),
521 520 h.format_date(h.time_to_datetime(repo.locked[1]))),
522 521 'warning')
523 522 return redirect(h.url('files_home',
524 523 repo_name=repo_name, revision='tip'))
525 524
526 525 if not self._is_valid_head(commit_id, repo.scm_instance()):
527 526 h.flash(_('You can only edit files with revision '
528 527 'being a valid branch '), category='warning')
529 528 return redirect(h.url('files_home',
530 529 repo_name=repo_name, revision='tip',
531 530 f_path=f_path))
532 531
533 532 c.commit = self.__get_commit_or_redirect(commit_id, repo_name)
534 533 c.file = self.__get_filenode_or_redirect(repo_name, c.commit, f_path)
535 534
536 535 if c.file.is_binary:
537 536 return redirect(url('files_home', repo_name=c.repo_name,
538 537 revision=c.commit.raw_id, f_path=f_path))
539 538 c.default_message = _(
540 539 'Edited file {} via RhodeCode Enterprise').format(f_path)
541 540 c.f_path = f_path
542 541 old_content = c.file.content
543 542 sl = old_content.splitlines(1)
544 543 first_line = sl[0] if sl else ''
545 544
546 545 # modes: 0 - Unix, 1 - Mac, 2 - DOS
547 546 mode = detect_mode(first_line, 0)
548 547 content = convert_line_endings(request.POST.get('content', ''), mode)
549 548
550 549 message = request.POST.get('message') or c.default_message
551 550 org_f_path = c.file.unicode_path
552 551 filename = request.POST['filename']
553 552 org_filename = c.file.name
554 553
555 554 if content == old_content and filename == org_filename:
556 555 h.flash(_('No changes'), category='warning')
557 556 return redirect(url('changeset_home', repo_name=c.repo_name,
558 557 revision='tip'))
559 558 try:
560 559 mapping = {
561 560 org_f_path: {
562 561 'org_filename': org_f_path,
563 562 'filename': os.path.join(c.file.dir_path, filename),
564 563 'content': content,
565 564 'lexer': '',
566 565 'op': 'mod',
567 566 }
568 567 }
569 568
570 569 ScmModel().update_nodes(
571 570 user=c.rhodecode_user.user_id,
572 571 repo=c.rhodecode_db_repo,
573 572 message=message,
574 573 nodes=mapping,
575 574 parent_commit=c.commit,
576 575 )
577 576
578 577 h.flash(
579 578 _('Successfully committed changes to file `{}`').format(
580 579 h.escape(f_path)), category='success')
581 580 except Exception:
582 581 log.exception('Error occurred during commit')
583 582 h.flash(_('Error occurred during commit'), category='error')
584 583 return redirect(url('changeset_home',
585 584 repo_name=c.repo_name, revision='tip'))
586 585
587 586 @LoginRequired()
588 587 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
589 588 def edit_home(self, repo_name, revision, f_path):
590 589 commit_id = revision
591 590
592 591 repo = c.rhodecode_db_repo
593 592 if repo.enable_locking and repo.locked[0]:
594 593 h.flash(_('This repository has been locked by %s on %s')
595 594 % (h.person_by_id(repo.locked[0]),
596 595 h.format_date(h.time_to_datetime(repo.locked[1]))),
597 596 'warning')
598 597 return redirect(h.url('files_home',
599 598 repo_name=repo_name, revision='tip'))
600 599
601 600 if not self._is_valid_head(commit_id, repo.scm_instance()):
602 601 h.flash(_('You can only edit files with revision '
603 602 'being a valid branch '), category='warning')
604 603 return redirect(h.url('files_home',
605 604 repo_name=repo_name, revision='tip',
606 605 f_path=f_path))
607 606
608 607 c.commit = self.__get_commit_or_redirect(commit_id, repo_name)
609 608 c.file = self.__get_filenode_or_redirect(repo_name, c.commit, f_path)
610 609
611 610 if c.file.is_binary:
612 611 return redirect(url('files_home', repo_name=c.repo_name,
613 612 revision=c.commit.raw_id, f_path=f_path))
614 613 c.default_message = _(
615 614 'Edited file {} via RhodeCode Enterprise').format(f_path)
616 615 c.f_path = f_path
617 616
618 617 return render('files/files_edit.mako')
619 618
620 619 def _is_valid_head(self, commit_id, repo):
621 620 # check if commit is a branch identifier- basically we cannot
622 621 # create multiple heads via file editing
623 622 valid_heads = repo.branches.keys() + repo.branches.values()
624 623
625 624 if h.is_svn(repo) and not repo.is_empty():
626 625 # Note: Subversion only has one head, we add it here in case there
627 626 # is no branch matched.
628 627 valid_heads.append(repo.get_commit(commit_idx=-1).raw_id)
629 628
630 629 # check if commit is a branch name or branch hash
631 630 return commit_id in valid_heads
632 631
633 632 @CSRFRequired()
634 633 @LoginRequired()
635 634 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
636 635 def add(self, repo_name, revision, f_path):
637 636 repo = Repository.get_by_repo_name(repo_name)
638 637 if repo.enable_locking and repo.locked[0]:
639 638 h.flash(_('This repository has been locked by %s on %s')
640 639 % (h.person_by_id(repo.locked[0]),
641 640 h.format_date(h.time_to_datetime(repo.locked[1]))),
642 641 'warning')
643 642 return redirect(h.url('files_home',
644 643 repo_name=repo_name, revision='tip'))
645 644
646 645 r_post = request.POST
647 646
648 647 c.commit = self.__get_commit_or_redirect(
649 648 revision, repo_name, redirect_after=False)
650 649 if c.commit is None:
651 650 c.commit = EmptyCommit(alias=c.rhodecode_repo.alias)
652 651 c.default_message = (_('Added file via RhodeCode Enterprise'))
653 652 c.f_path = f_path
654 653 unix_mode = 0
655 654 content = convert_line_endings(r_post.get('content', ''), unix_mode)
656 655
657 656 message = r_post.get('message') or c.default_message
658 657 filename = r_post.get('filename')
659 658 location = r_post.get('location', '') # dir location
660 659 file_obj = r_post.get('upload_file', None)
661 660
662 661 if file_obj is not None and hasattr(file_obj, 'filename'):
663 662 filename = r_post.get('filename_upload')
664 663 content = file_obj.file
665 664
666 665 if hasattr(content, 'file'):
667 666 # non posix systems store real file under file attr
668 667 content = content.file
669 668
670 669 # If there's no commit, redirect to repo summary
671 670 if type(c.commit) is EmptyCommit:
672 671 redirect_url = h.route_path('repo_summary', repo_name=c.repo_name)
673 672 else:
674 673 redirect_url = url("changeset_home", repo_name=c.repo_name,
675 674 revision='tip')
676 675
677 676 if not filename:
678 677 h.flash(_('No filename'), category='warning')
679 678 return redirect(redirect_url)
680 679
681 680 # extract the location from filename,
682 681 # allows using foo/bar.txt syntax to create subdirectories
683 682 subdir_loc = filename.rsplit('/', 1)
684 683 if len(subdir_loc) == 2:
685 684 location = os.path.join(location, subdir_loc[0])
686 685
687 686 # strip all crap out of file, just leave the basename
688 687 filename = os.path.basename(filename)
689 688 node_path = os.path.join(location, filename)
690 689 author = c.rhodecode_user.full_contact
691 690
692 691 try:
693 692 nodes = {
694 693 node_path: {
695 694 'content': content
696 695 }
697 696 }
698 697 self.scm_model.create_nodes(
699 698 user=c.rhodecode_user.user_id,
700 699 repo=c.rhodecode_db_repo,
701 700 message=message,
702 701 nodes=nodes,
703 702 parent_commit=c.commit,
704 703 author=author,
705 704 )
706 705
707 706 h.flash(
708 707 _('Successfully committed new file `{}`').format(
709 708 h.escape(node_path)), category='success')
710 709 except NonRelativePathError as e:
711 710 h.flash(_(
712 711 'The location specified must be a relative path and must not '
713 712 'contain .. in the path'), category='warning')
714 713 return redirect(url('changeset_home', repo_name=c.repo_name,
715 714 revision='tip'))
716 715 except (NodeError, NodeAlreadyExistsError) as e:
717 716 h.flash(_(h.escape(e)), category='error')
718 717 except Exception:
719 718 log.exception('Error occurred during commit')
720 719 h.flash(_('Error occurred during commit'), category='error')
721 720 return redirect(url('changeset_home',
722 721 repo_name=c.repo_name, revision='tip'))
723 722
724 723 @LoginRequired()
725 724 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
726 725 def add_home(self, repo_name, revision, f_path):
727 726
728 727 repo = Repository.get_by_repo_name(repo_name)
729 728 if repo.enable_locking and repo.locked[0]:
730 729 h.flash(_('This repository has been locked by %s on %s')
731 730 % (h.person_by_id(repo.locked[0]),
732 731 h.format_date(h.time_to_datetime(repo.locked[1]))),
733 732 'warning')
734 733 return redirect(h.url('files_home',
735 734 repo_name=repo_name, revision='tip'))
736 735
737 736 c.commit = self.__get_commit_or_redirect(
738 737 revision, repo_name, redirect_after=False)
739 738 if c.commit is None:
740 739 c.commit = EmptyCommit(alias=c.rhodecode_repo.alias)
741 740 c.default_message = (_('Added file via RhodeCode Enterprise'))
742 741 c.f_path = f_path
743 742
744 743 return render('files/files_add.mako')
745 744
746 745 @LoginRequired()
747 746 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
748 747 'repository.admin')
749 748 def archivefile(self, repo_name, fname):
750 749 fileformat = None
751 750 commit_id = None
752 751 ext = None
753 752 subrepos = request.GET.get('subrepos') == 'true'
754 753
755 754 for a_type, ext_data in settings.ARCHIVE_SPECS.items():
756 755 archive_spec = fname.split(ext_data[1])
757 756 if len(archive_spec) == 2 and archive_spec[1] == '':
758 757 fileformat = a_type or ext_data[1]
759 758 commit_id = archive_spec[0]
760 759 ext = ext_data[1]
761 760
762 761 dbrepo = RepoModel().get_by_repo_name(repo_name)
763 762 if not dbrepo.enable_downloads:
764 763 return _('Downloads disabled')
765 764
766 765 try:
767 766 commit = c.rhodecode_repo.get_commit(commit_id)
768 767 content_type = settings.ARCHIVE_SPECS[fileformat][0]
769 768 except CommitDoesNotExistError:
770 769 return _('Unknown revision %s') % commit_id
771 770 except EmptyRepositoryError:
772 771 return _('Empty repository')
773 772 except KeyError:
774 773 return _('Unknown archive type')
775 774
776 775 # archive cache
777 776 from rhodecode import CONFIG
778 777
779 778 archive_name = '%s-%s%s%s' % (
780 779 safe_str(repo_name.replace('/', '_')),
781 780 '-sub' if subrepos else '',
782 781 safe_str(commit.short_id), ext)
783 782
784 783 use_cached_archive = False
785 784 archive_cache_enabled = CONFIG.get(
786 785 'archive_cache_dir') and not request.GET.get('no_cache')
787 786
788 787 if archive_cache_enabled:
789 788 # check if we it's ok to write
790 789 if not os.path.isdir(CONFIG['archive_cache_dir']):
791 790 os.makedirs(CONFIG['archive_cache_dir'])
792 791 cached_archive_path = os.path.join(
793 792 CONFIG['archive_cache_dir'], archive_name)
794 793 if os.path.isfile(cached_archive_path):
795 794 log.debug('Found cached archive in %s', cached_archive_path)
796 795 fd, archive = None, cached_archive_path
797 796 use_cached_archive = True
798 797 else:
799 798 log.debug('Archive %s is not yet cached', archive_name)
800 799
801 800 if not use_cached_archive:
802 801 # generate new archive
803 802 fd, archive = tempfile.mkstemp()
804 803 log.debug('Creating new temp archive in %s', archive)
805 804 try:
806 805 commit.archive_repo(archive, kind=fileformat, subrepos=subrepos)
807 806 except ImproperArchiveTypeError:
808 807 return _('Unknown archive type')
809 808 if archive_cache_enabled:
810 809 # if we generated the archive and we have cache enabled
811 810 # let's use this for future
812 811 log.debug('Storing new archive in %s', cached_archive_path)
813 812 shutil.move(archive, cached_archive_path)
814 813 archive = cached_archive_path
815 814
816 815 # store download action
817 816 audit_logger.store_web(
818 817 'repo.archive.download', action_data={
819 818 'user_agent': request.user_agent,
820 819 'archive_name': archive_name,
821 820 'archive_spec': fname,
822 821 'archive_cached': use_cached_archive},
823 822 user=c.rhodecode_user,
824 823 repo=dbrepo,
825 824 commit=True
826 825 )
827 826
828 827 response.content_disposition = str(
829 828 'attachment; filename=%s' % archive_name)
830 829 response.content_type = str(content_type)
831 830
832 831 def get_chunked_archive(archive):
833 832 with open(archive, 'rb') as stream:
834 833 while True:
835 834 data = stream.read(16 * 1024)
836 835 if not data:
837 836 if fd: # fd means we used temporary file
838 837 os.close(fd)
839 838 if not archive_cache_enabled:
840 839 log.debug('Destroying temp archive %s', archive)
841 840 os.remove(archive)
842 841 break
843 842 yield data
844 843
845 844 return get_chunked_archive(archive)
846 845
847 846 @LoginRequired()
848 847 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
849 848 'repository.admin')
850 849 def diff(self, repo_name, f_path):
851 850
852 851 c.action = request.GET.get('diff')
853 852 diff1 = request.GET.get('diff1', '')
854 853 diff2 = request.GET.get('diff2', '')
855 854
856 855 path1, diff1 = parse_path_ref(diff1, default_path=f_path)
857 856
858 857 ignore_whitespace = str2bool(request.GET.get('ignorews'))
859 858 line_context = request.GET.get('context', 3)
860 859
861 860 if not any((diff1, diff2)):
862 861 h.flash(
863 862 'Need query parameter "diff1" or "diff2" to generate a diff.',
864 863 category='error')
865 864 raise HTTPBadRequest()
866 865
867 866 if c.action not in ['download', 'raw']:
868 867 # redirect to new view if we render diff
869 868 return redirect(
870 869 url('compare_url', repo_name=repo_name,
871 870 source_ref_type='rev',
872 871 source_ref=diff1,
873 872 target_repo=c.repo_name,
874 873 target_ref_type='rev',
875 874 target_ref=diff2,
876 875 f_path=f_path))
877 876
878 877 try:
879 878 node1 = self._get_file_node(diff1, path1)
880 879 node2 = self._get_file_node(diff2, f_path)
881 880 except (RepositoryError, NodeError):
882 881 log.exception("Exception while trying to get node from repository")
883 882 return redirect(url(
884 883 'files_home', repo_name=c.repo_name, f_path=f_path))
885 884
886 885 if all(isinstance(node.commit, EmptyCommit)
887 886 for node in (node1, node2)):
888 887 raise HTTPNotFound
889 888
890 889 c.commit_1 = node1.commit
891 890 c.commit_2 = node2.commit
892 891
893 892 if c.action == 'download':
894 893 _diff = diffs.get_gitdiff(node1, node2,
895 894 ignore_whitespace=ignore_whitespace,
896 895 context=line_context)
897 896 diff = diffs.DiffProcessor(_diff, format='gitdiff')
898 897
899 898 diff_name = '%s_vs_%s.diff' % (diff1, diff2)
900 899 response.content_type = 'text/plain'
901 900 response.content_disposition = (
902 901 'attachment; filename=%s' % (diff_name,)
903 902 )
904 903 charset = self._get_default_encoding()
905 904 if charset:
906 905 response.charset = charset
907 906 return diff.as_raw()
908 907
909 908 elif c.action == 'raw':
910 909 _diff = diffs.get_gitdiff(node1, node2,
911 910 ignore_whitespace=ignore_whitespace,
912 911 context=line_context)
913 912 diff = diffs.DiffProcessor(_diff, format='gitdiff')
914 913 response.content_type = 'text/plain'
915 914 charset = self._get_default_encoding()
916 915 if charset:
917 916 response.charset = charset
918 917 return diff.as_raw()
919 918
920 919 else:
921 920 return redirect(
922 921 url('compare_url', repo_name=repo_name,
923 922 source_ref_type='rev',
924 923 source_ref=diff1,
925 924 target_repo=c.repo_name,
926 925 target_ref_type='rev',
927 926 target_ref=diff2,
928 927 f_path=f_path))
929 928
930 929 @LoginRequired()
931 930 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
932 931 'repository.admin')
933 932 def diff_2way(self, repo_name, f_path):
934 933 """
935 934 Kept only to make OLD links work
936 935 """
937 936 diff1 = request.GET.get('diff1', '')
938 937 diff2 = request.GET.get('diff2', '')
939 938
940 939 if not any((diff1, diff2)):
941 940 h.flash(
942 941 'Need query parameter "diff1" or "diff2" to generate a diff.',
943 942 category='error')
944 943 raise HTTPBadRequest()
945 944
946 945 return redirect(
947 946 url('compare_url', repo_name=repo_name,
948 947 source_ref_type='rev',
949 948 source_ref=diff1,
950 949 target_repo=c.repo_name,
951 950 target_ref_type='rev',
952 951 target_ref=diff2,
953 952 f_path=f_path,
954 953 diffmode='sideside'))
955 954
956 955 def _get_file_node(self, commit_id, f_path):
957 956 if commit_id not in ['', None, 'None', '0' * 12, '0' * 40]:
958 957 commit = c.rhodecode_repo.get_commit(commit_id=commit_id)
959 958 try:
960 959 node = commit.get_node(f_path)
961 960 if node.is_dir():
962 961 raise NodeError('%s path is a %s not a file'
963 962 % (node, type(node)))
964 963 except NodeDoesNotExistError:
965 964 commit = EmptyCommit(
966 965 commit_id=commit_id,
967 966 idx=commit.idx,
968 967 repo=commit.repository,
969 968 alias=commit.repository.alias,
970 969 message=commit.message,
971 970 author=commit.author,
972 971 date=commit.date)
973 972 node = FileNode(f_path, '', commit=commit)
974 973 else:
975 974 commit = EmptyCommit(
976 975 repo=c.rhodecode_repo,
977 976 alias=c.rhodecode_repo.alias)
978 977 node = FileNode(f_path, '', commit=commit)
979 978 return node
980 979
981 980 def _get_node_history(self, commit, f_path, commits=None):
982 981 """
983 982 get commit history for given node
984 983
985 984 :param commit: commit to calculate history
986 985 :param f_path: path for node to calculate history for
987 986 :param commits: if passed don't calculate history and take
988 987 commits defined in this list
989 988 """
990 989 # calculate history based on tip
991 990 tip = c.rhodecode_repo.get_commit()
992 991 if commits is None:
993 992 pre_load = ["author", "branch"]
994 993 try:
995 994 commits = tip.get_file_history(f_path, pre_load=pre_load)
996 995 except (NodeDoesNotExistError, CommitError):
997 996 # this node is not present at tip!
998 997 commits = commit.get_file_history(f_path, pre_load=pre_load)
999 998
1000 999 history = []
1001 1000 commits_group = ([], _("Changesets"))
1002 1001 for commit in commits:
1003 1002 branch = ' (%s)' % commit.branch if commit.branch else ''
1004 1003 n_desc = 'r%s:%s%s' % (commit.idx, commit.short_id, branch)
1005 1004 commits_group[0].append((commit.raw_id, n_desc,))
1006 1005 history.append(commits_group)
1007 1006
1008 1007 symbolic_reference = self._symbolic_reference
1009 1008
1010 1009 if c.rhodecode_repo.alias == 'svn':
1011 1010 adjusted_f_path = self._adjust_file_path_for_svn(
1012 1011 f_path, c.rhodecode_repo)
1013 1012 if adjusted_f_path != f_path:
1014 1013 log.debug(
1015 1014 'Recognized svn tag or branch in file "%s", using svn '
1016 1015 'specific symbolic references', f_path)
1017 1016 f_path = adjusted_f_path
1018 1017 symbolic_reference = self._symbolic_reference_svn
1019 1018
1020 1019 branches = self._create_references(
1021 1020 c.rhodecode_repo.branches, symbolic_reference, f_path)
1022 1021 branches_group = (branches, _("Branches"))
1023 1022
1024 1023 tags = self._create_references(
1025 1024 c.rhodecode_repo.tags, symbolic_reference, f_path)
1026 1025 tags_group = (tags, _("Tags"))
1027 1026
1028 1027 history.append(branches_group)
1029 1028 history.append(tags_group)
1030 1029
1031 1030 return history, commits
1032 1031
1033 1032 def _adjust_file_path_for_svn(self, f_path, repo):
1034 1033 """
1035 1034 Computes the relative path of `f_path`.
1036 1035
1037 1036 This is mainly based on prefix matching of the recognized tags and
1038 1037 branches in the underlying repository.
1039 1038 """
1040 1039 tags_and_branches = itertools.chain(
1041 1040 repo.branches.iterkeys(),
1042 1041 repo.tags.iterkeys())
1043 1042 tags_and_branches = sorted(tags_and_branches, key=len, reverse=True)
1044 1043
1045 1044 for name in tags_and_branches:
1046 1045 if f_path.startswith(name + '/'):
1047 1046 f_path = vcspath.relpath(f_path, name)
1048 1047 break
1049 1048 return f_path
1050 1049
1051 1050 def _create_references(
1052 1051 self, branches_or_tags, symbolic_reference, f_path):
1053 1052 items = []
1054 1053 for name, commit_id in branches_or_tags.items():
1055 1054 sym_ref = symbolic_reference(commit_id, name, f_path)
1056 1055 items.append((sym_ref, name))
1057 1056 return items
1058 1057
1059 1058 def _symbolic_reference(self, commit_id, name, f_path):
1060 1059 return commit_id
1061 1060
1062 1061 def _symbolic_reference_svn(self, commit_id, name, f_path):
1063 1062 new_f_path = vcspath.join(name, f_path)
1064 1063 return u'%s@%s' % (new_f_path, commit_id)
1065 1064
1066 1065 @LoginRequired()
1067 1066 @XHRRequired()
1068 1067 @HasRepoPermissionAnyDecorator(
1069 1068 'repository.read', 'repository.write', 'repository.admin')
1070 1069 @jsonify
1071 1070 def nodelist(self, repo_name, revision, f_path):
1072 1071 commit = self.__get_commit_or_redirect(revision, repo_name)
1073 1072
1074 1073 metadata = self._get_nodelist_at_commit(
1075 1074 repo_name, commit.raw_id, f_path)
1076 1075 return {'nodes': metadata}
1077 1076
1078 1077 @LoginRequired()
1079 1078 @XHRRequired()
1080 1079 @HasRepoPermissionAnyDecorator(
1081 1080 'repository.read', 'repository.write', 'repository.admin')
1082 1081 def nodetree_full(self, repo_name, commit_id, f_path):
1083 1082 """
1084 1083 Returns rendered html of file tree that contains commit date,
1085 1084 author, revision for the specified combination of
1086 1085 repo, commit_id and file path
1087 1086
1088 1087 :param repo_name: name of the repository
1089 1088 :param commit_id: commit_id of file tree
1090 1089 :param f_path: file path of the requested directory
1091 1090 """
1092 1091
1093 1092 commit = self.__get_commit_or_redirect(commit_id, repo_name)
1094 1093 try:
1095 1094 dir_node = commit.get_node(f_path)
1096 1095 except RepositoryError as e:
1097 1096 return 'error {}'.format(safe_str(e))
1098 1097
1099 1098 if dir_node.is_file():
1100 1099 return ''
1101 1100
1102 1101 c.file = dir_node
1103 1102 c.commit = commit
1104 1103
1105 1104 # using force=True here, make a little trick. We flush the cache and
1106 1105 # compute it using the same key as without full_load, so the fully
1107 1106 # loaded cached tree is now returned instead of partial
1108 1107 return self._get_tree_at_commit(
1109 1108 repo_name, commit.raw_id, dir_node.path, full_load=True,
1110 1109 force=True)
@@ -1,1588 +1,1588 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2014-2017 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 """
22 22 Base module for all VCS systems
23 23 """
24 24
25 25 import collections
26 26 import datetime
27 27 import itertools
28 28 import logging
29 29 import os
30 30 import time
31 31 import warnings
32 32
33 33 from zope.cachedescriptors.property import Lazy as LazyProperty
34 34
35 35 from rhodecode.lib.utils2 import safe_str, safe_unicode
36 36 from rhodecode.lib.vcs import connection
37 37 from rhodecode.lib.vcs.utils import author_name, author_email
38 38 from rhodecode.lib.vcs.conf import settings
39 39 from rhodecode.lib.vcs.exceptions import (
40 40 CommitError, EmptyRepositoryError, NodeAlreadyAddedError,
41 41 NodeAlreadyChangedError, NodeAlreadyExistsError, NodeAlreadyRemovedError,
42 42 NodeDoesNotExistError, NodeNotChangedError, VCSError,
43 43 ImproperArchiveTypeError, BranchDoesNotExistError, CommitDoesNotExistError,
44 44 RepositoryError)
45 45
46 46
47 47 log = logging.getLogger(__name__)
48 48
49 49
50 50 FILEMODE_DEFAULT = 0100644
51 51 FILEMODE_EXECUTABLE = 0100755
52 52
53 53 Reference = collections.namedtuple('Reference', ('type', 'name', 'commit_id'))
54 54 MergeResponse = collections.namedtuple(
55 55 'MergeResponse',
56 56 ('possible', 'executed', 'merge_ref', 'failure_reason'))
57 57
58 58
59 59 class MergeFailureReason(object):
60 60 """
61 61 Enumeration with all the reasons why the server side merge could fail.
62 62
63 63 DO NOT change the number of the reasons, as they may be stored in the
64 64 database.
65 65
66 66 Changing the name of a reason is acceptable and encouraged to deprecate old
67 67 reasons.
68 68 """
69 69
70 70 # Everything went well.
71 71 NONE = 0
72 72
73 73 # An unexpected exception was raised. Check the logs for more details.
74 74 UNKNOWN = 1
75 75
76 76 # The merge was not successful, there are conflicts.
77 77 MERGE_FAILED = 2
78 78
79 79 # The merge succeeded but we could not push it to the target repository.
80 80 PUSH_FAILED = 3
81 81
82 82 # The specified target is not a head in the target repository.
83 83 TARGET_IS_NOT_HEAD = 4
84 84
85 85 # The source repository contains more branches than the target. Pushing
86 86 # the merge will create additional branches in the target.
87 87 HG_SOURCE_HAS_MORE_BRANCHES = 5
88 88
89 89 # The target reference has multiple heads. That does not allow to correctly
90 90 # identify the target location. This could only happen for mercurial
91 91 # branches.
92 92 HG_TARGET_HAS_MULTIPLE_HEADS = 6
93 93
94 94 # The target repository is locked
95 95 TARGET_IS_LOCKED = 7
96 96
97 97 # Deprecated, use MISSING_TARGET_REF or MISSING_SOURCE_REF instead.
98 98 # A involved commit could not be found.
99 99 _DEPRECATED_MISSING_COMMIT = 8
100 100
101 101 # The target repo reference is missing.
102 102 MISSING_TARGET_REF = 9
103 103
104 104 # The source repo reference is missing.
105 105 MISSING_SOURCE_REF = 10
106 106
107 107 # The merge was not successful, there are conflicts related to sub
108 108 # repositories.
109 109 SUBREPO_MERGE_FAILED = 11
110 110
111 111
112 112 class UpdateFailureReason(object):
113 113 """
114 114 Enumeration with all the reasons why the pull request update could fail.
115 115
116 116 DO NOT change the number of the reasons, as they may be stored in the
117 117 database.
118 118
119 119 Changing the name of a reason is acceptable and encouraged to deprecate old
120 120 reasons.
121 121 """
122 122
123 123 # Everything went well.
124 124 NONE = 0
125 125
126 126 # An unexpected exception was raised. Check the logs for more details.
127 127 UNKNOWN = 1
128 128
129 129 # The pull request is up to date.
130 130 NO_CHANGE = 2
131 131
132 132 # The pull request has a reference type that is not supported for update.
133 133 WRONG_REF_TYPE = 3
134 134
135 135 # Update failed because the target reference is missing.
136 136 MISSING_TARGET_REF = 4
137 137
138 138 # Update failed because the source reference is missing.
139 139 MISSING_SOURCE_REF = 5
140 140
141 141
142 142 class BaseRepository(object):
143 143 """
144 144 Base Repository for final backends
145 145
146 146 .. attribute:: DEFAULT_BRANCH_NAME
147 147
148 148 name of default branch (i.e. "trunk" for svn, "master" for git etc.
149 149
150 150 .. attribute:: commit_ids
151 151
152 152 list of all available commit ids, in ascending order
153 153
154 154 .. attribute:: path
155 155
156 156 absolute path to the repository
157 157
158 158 .. attribute:: bookmarks
159 159
160 160 Mapping from name to :term:`Commit ID` of the bookmark. Empty in case
161 161 there are no bookmarks or the backend implementation does not support
162 162 bookmarks.
163 163
164 164 .. attribute:: tags
165 165
166 166 Mapping from name to :term:`Commit ID` of the tag.
167 167
168 168 """
169 169
170 170 DEFAULT_BRANCH_NAME = None
171 171 DEFAULT_CONTACT = u"Unknown"
172 172 DEFAULT_DESCRIPTION = u"unknown"
173 173 EMPTY_COMMIT_ID = '0' * 40
174 174
175 175 path = None
176 176
177 177 def __init__(self, repo_path, config=None, create=False, **kwargs):
178 178 """
179 179 Initializes repository. Raises RepositoryError if repository could
180 180 not be find at the given ``repo_path`` or directory at ``repo_path``
181 181 exists and ``create`` is set to True.
182 182
183 183 :param repo_path: local path of the repository
184 184 :param config: repository configuration
185 185 :param create=False: if set to True, would try to create repository.
186 186 :param src_url=None: if set, should be proper url from which repository
187 187 would be cloned; requires ``create`` parameter to be set to True -
188 188 raises RepositoryError if src_url is set and create evaluates to
189 189 False
190 190 """
191 191 raise NotImplementedError
192 192
193 193 def __repr__(self):
194 194 return '<%s at %s>' % (self.__class__.__name__, self.path)
195 195
196 196 def __len__(self):
197 197 return self.count()
198 198
199 199 def __eq__(self, other):
200 200 same_instance = isinstance(other, self.__class__)
201 201 return same_instance and other.path == self.path
202 202
203 203 def __ne__(self, other):
204 204 return not self.__eq__(other)
205 205
206 206 @LazyProperty
207 207 def EMPTY_COMMIT(self):
208 208 return EmptyCommit(self.EMPTY_COMMIT_ID)
209 209
210 210 @LazyProperty
211 211 def alias(self):
212 212 for k, v in settings.BACKENDS.items():
213 213 if v.split('.')[-1] == str(self.__class__.__name__):
214 214 return k
215 215
216 216 @LazyProperty
217 217 def name(self):
218 218 return safe_unicode(os.path.basename(self.path))
219 219
220 220 @LazyProperty
221 221 def description(self):
222 222 raise NotImplementedError
223 223
224 224 def refs(self):
225 225 """
226 226 returns a `dict` with branches, bookmarks, tags, and closed_branches
227 227 for this repository
228 228 """
229 229 return dict(
230 230 branches=self.branches,
231 231 branches_closed=self.branches_closed,
232 232 tags=self.tags,
233 233 bookmarks=self.bookmarks
234 234 )
235 235
236 236 @LazyProperty
237 237 def branches(self):
238 238 """
239 239 A `dict` which maps branch names to commit ids.
240 240 """
241 241 raise NotImplementedError
242 242
243 243 @LazyProperty
244 244 def tags(self):
245 245 """
246 246 A `dict` which maps tags names to commit ids.
247 247 """
248 248 raise NotImplementedError
249 249
250 250 @LazyProperty
251 251 def size(self):
252 252 """
253 253 Returns combined size in bytes for all repository files
254 254 """
255 255 tip = self.get_commit()
256 256 return tip.size
257 257
258 258 def size_at_commit(self, commit_id):
259 259 commit = self.get_commit(commit_id)
260 260 return commit.size
261 261
262 262 def is_empty(self):
263 263 return not bool(self.commit_ids)
264 264
265 265 @staticmethod
266 266 def check_url(url, config):
267 267 """
268 268 Function will check given url and try to verify if it's a valid
269 269 link.
270 270 """
271 271 raise NotImplementedError
272 272
273 273 @staticmethod
274 274 def is_valid_repository(path):
275 275 """
276 276 Check if given `path` contains a valid repository of this backend
277 277 """
278 278 raise NotImplementedError
279 279
280 280 # ==========================================================================
281 281 # COMMITS
282 282 # ==========================================================================
283 283
284 284 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
285 285 """
286 286 Returns instance of `BaseCommit` class. If `commit_id` and `commit_idx`
287 287 are both None, most recent commit is returned.
288 288
289 289 :param pre_load: Optional. List of commit attributes to load.
290 290
291 291 :raises ``EmptyRepositoryError``: if there are no commits
292 292 """
293 293 raise NotImplementedError
294 294
295 295 def __iter__(self):
296 296 for commit_id in self.commit_ids:
297 297 yield self.get_commit(commit_id=commit_id)
298 298
299 299 def get_commits(
300 300 self, start_id=None, end_id=None, start_date=None, end_date=None,
301 301 branch_name=None, pre_load=None):
302 302 """
303 303 Returns iterator of `BaseCommit` objects from start to end
304 304 not inclusive. This should behave just like a list, ie. end is not
305 305 inclusive.
306 306
307 307 :param start_id: None or str, must be a valid commit id
308 308 :param end_id: None or str, must be a valid commit id
309 309 :param start_date:
310 310 :param end_date:
311 311 :param branch_name:
312 312 :param pre_load:
313 313 """
314 314 raise NotImplementedError
315 315
316 316 def __getitem__(self, key):
317 317 """
318 318 Allows index based access to the commit objects of this repository.
319 319 """
320 320 pre_load = ["author", "branch", "date", "message", "parents"]
321 321 if isinstance(key, slice):
322 322 return self._get_range(key, pre_load)
323 323 return self.get_commit(commit_idx=key, pre_load=pre_load)
324 324
325 325 def _get_range(self, slice_obj, pre_load):
326 326 for commit_id in self.commit_ids.__getitem__(slice_obj):
327 327 yield self.get_commit(commit_id=commit_id, pre_load=pre_load)
328 328
329 329 def count(self):
330 330 return len(self.commit_ids)
331 331
332 332 def tag(self, name, user, commit_id=None, message=None, date=None, **opts):
333 333 """
334 334 Creates and returns a tag for the given ``commit_id``.
335 335
336 336 :param name: name for new tag
337 337 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
338 338 :param commit_id: commit id for which new tag would be created
339 339 :param message: message of the tag's commit
340 340 :param date: date of tag's commit
341 341
342 342 :raises TagAlreadyExistError: if tag with same name already exists
343 343 """
344 344 raise NotImplementedError
345 345
346 346 def remove_tag(self, name, user, message=None, date=None):
347 347 """
348 348 Removes tag with the given ``name``.
349 349
350 350 :param name: name of the tag to be removed
351 351 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
352 352 :param message: message of the tag's removal commit
353 353 :param date: date of tag's removal commit
354 354
355 355 :raises TagDoesNotExistError: if tag with given name does not exists
356 356 """
357 357 raise NotImplementedError
358 358
359 359 def get_diff(
360 360 self, commit1, commit2, path=None, ignore_whitespace=False,
361 361 context=3, path1=None):
362 362 """
363 363 Returns (git like) *diff*, as plain text. Shows changes introduced by
364 364 `commit2` since `commit1`.
365 365
366 366 :param commit1: Entry point from which diff is shown. Can be
367 367 ``self.EMPTY_COMMIT`` - in this case, patch showing all
368 368 the changes since empty state of the repository until `commit2`
369 369 :param commit2: Until which commit changes should be shown.
370 370 :param path: Can be set to a path of a file to create a diff of that
371 371 file. If `path1` is also set, this value is only associated to
372 372 `commit2`.
373 373 :param ignore_whitespace: If set to ``True``, would not show whitespace
374 374 changes. Defaults to ``False``.
375 375 :param context: How many lines before/after changed lines should be
376 376 shown. Defaults to ``3``.
377 377 :param path1: Can be set to a path to associate with `commit1`. This
378 378 parameter works only for backends which support diff generation for
379 379 different paths. Other backends will raise a `ValueError` if `path1`
380 380 is set and has a different value than `path`.
381 381 :param file_path: filter this diff by given path pattern
382 382 """
383 383 raise NotImplementedError
384 384
385 385 def strip(self, commit_id, branch=None):
386 386 """
387 387 Strip given commit_id from the repository
388 388 """
389 389 raise NotImplementedError
390 390
391 391 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
392 392 """
393 393 Return a latest common ancestor commit if one exists for this repo
394 394 `commit_id1` vs `commit_id2` from `repo2`.
395 395
396 396 :param commit_id1: Commit it from this repository to use as a
397 397 target for the comparison.
398 398 :param commit_id2: Source commit id to use for comparison.
399 399 :param repo2: Source repository to use for comparison.
400 400 """
401 401 raise NotImplementedError
402 402
403 403 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
404 404 """
405 405 Compare this repository's revision `commit_id1` with `commit_id2`.
406 406
407 407 Returns a tuple(commits, ancestor) that would be merged from
408 408 `commit_id2`. Doing a normal compare (``merge=False``), ``None``
409 409 will be returned as ancestor.
410 410
411 411 :param commit_id1: Commit it from this repository to use as a
412 412 target for the comparison.
413 413 :param commit_id2: Source commit id to use for comparison.
414 414 :param repo2: Source repository to use for comparison.
415 415 :param merge: If set to ``True`` will do a merge compare which also
416 416 returns the common ancestor.
417 417 :param pre_load: Optional. List of commit attributes to load.
418 418 """
419 419 raise NotImplementedError
420 420
421 421 def merge(self, target_ref, source_repo, source_ref, workspace_id,
422 422 user_name='', user_email='', message='', dry_run=False,
423 423 use_rebase=False):
424 424 """
425 425 Merge the revisions specified in `source_ref` from `source_repo`
426 426 onto the `target_ref` of this repository.
427 427
428 428 `source_ref` and `target_ref` are named tupls with the following
429 429 fields `type`, `name` and `commit_id`.
430 430
431 431 Returns a MergeResponse named tuple with the following fields
432 432 'possible', 'executed', 'source_commit', 'target_commit',
433 433 'merge_commit'.
434 434
435 435 :param target_ref: `target_ref` points to the commit on top of which
436 436 the `source_ref` should be merged.
437 437 :param source_repo: The repository that contains the commits to be
438 438 merged.
439 439 :param source_ref: `source_ref` points to the topmost commit from
440 440 the `source_repo` which should be merged.
441 441 :param workspace_id: `workspace_id` unique identifier.
442 442 :param user_name: Merge commit `user_name`.
443 443 :param user_email: Merge commit `user_email`.
444 444 :param message: Merge commit `message`.
445 445 :param dry_run: If `True` the merge will not take place.
446 446 :param use_rebase: If `True` commits from the source will be rebased
447 447 on top of the target instead of being merged.
448 448 """
449 449 if dry_run:
450 450 message = message or 'dry_run_merge_message'
451 451 user_email = user_email or 'dry-run-merge@rhodecode.com'
452 452 user_name = user_name or 'Dry-Run User'
453 453 else:
454 454 if not user_name:
455 455 raise ValueError('user_name cannot be empty')
456 456 if not user_email:
457 457 raise ValueError('user_email cannot be empty')
458 458 if not message:
459 459 raise ValueError('message cannot be empty')
460 460
461 461 shadow_repository_path = self._maybe_prepare_merge_workspace(
462 462 workspace_id, target_ref)
463 463
464 464 try:
465 465 return self._merge_repo(
466 466 shadow_repository_path, target_ref, source_repo,
467 467 source_ref, message, user_name, user_email, dry_run=dry_run,
468 468 use_rebase=use_rebase)
469 469 except RepositoryError:
470 470 log.exception(
471 471 'Unexpected failure when running merge, dry-run=%s',
472 472 dry_run)
473 473 return MergeResponse(
474 474 False, False, None, MergeFailureReason.UNKNOWN)
475 475
476 476 def _merge_repo(self, shadow_repository_path, target_ref,
477 477 source_repo, source_ref, merge_message,
478 478 merger_name, merger_email, dry_run=False, use_rebase=False):
479 479 """Internal implementation of merge."""
480 480 raise NotImplementedError
481 481
482 482 def _maybe_prepare_merge_workspace(self, workspace_id, target_ref):
483 483 """
484 484 Create the merge workspace.
485 485
486 486 :param workspace_id: `workspace_id` unique identifier.
487 487 """
488 488 raise NotImplementedError
489 489
490 490 def cleanup_merge_workspace(self, workspace_id):
491 491 """
492 492 Remove merge workspace.
493 493
494 494 This function MUST not fail in case there is no workspace associated to
495 495 the given `workspace_id`.
496 496
497 497 :param workspace_id: `workspace_id` unique identifier.
498 498 """
499 499 raise NotImplementedError
500 500
501 501 # ========== #
502 502 # COMMIT API #
503 503 # ========== #
504 504
505 505 @LazyProperty
506 506 def in_memory_commit(self):
507 507 """
508 508 Returns :class:`InMemoryCommit` object for this repository.
509 509 """
510 510 raise NotImplementedError
511 511
512 512 # ======================== #
513 513 # UTILITIES FOR SUBCLASSES #
514 514 # ======================== #
515 515
516 516 def _validate_diff_commits(self, commit1, commit2):
517 517 """
518 518 Validates that the given commits are related to this repository.
519 519
520 520 Intended as a utility for sub classes to have a consistent validation
521 521 of input parameters in methods like :meth:`get_diff`.
522 522 """
523 523 self._validate_commit(commit1)
524 524 self._validate_commit(commit2)
525 525 if (isinstance(commit1, EmptyCommit) and
526 526 isinstance(commit2, EmptyCommit)):
527 527 raise ValueError("Cannot compare two empty commits")
528 528
529 529 def _validate_commit(self, commit):
530 530 if not isinstance(commit, BaseCommit):
531 531 raise TypeError(
532 532 "%s is not of type BaseCommit" % repr(commit))
533 533 if commit.repository != self and not isinstance(commit, EmptyCommit):
534 534 raise ValueError(
535 535 "Commit %s must be a valid commit from this repository %s, "
536 536 "related to this repository instead %s." %
537 537 (commit, self, commit.repository))
538 538
539 539 def _validate_commit_id(self, commit_id):
540 540 if not isinstance(commit_id, basestring):
541 541 raise TypeError("commit_id must be a string value")
542 542
543 543 def _validate_commit_idx(self, commit_idx):
544 544 if not isinstance(commit_idx, (int, long)):
545 545 raise TypeError("commit_idx must be a numeric value")
546 546
547 547 def _validate_branch_name(self, branch_name):
548 548 if branch_name and branch_name not in self.branches_all:
549 549 msg = ("Branch %s not found in %s" % (branch_name, self))
550 550 raise BranchDoesNotExistError(msg)
551 551
552 552 #
553 553 # Supporting deprecated API parts
554 554 # TODO: johbo: consider to move this into a mixin
555 555 #
556 556
557 557 @property
558 558 def EMPTY_CHANGESET(self):
559 559 warnings.warn(
560 560 "Use EMPTY_COMMIT or EMPTY_COMMIT_ID instead", DeprecationWarning)
561 561 return self.EMPTY_COMMIT_ID
562 562
563 563 @property
564 564 def revisions(self):
565 565 warnings.warn("Use commits attribute instead", DeprecationWarning)
566 566 return self.commit_ids
567 567
568 568 @revisions.setter
569 569 def revisions(self, value):
570 570 warnings.warn("Use commits attribute instead", DeprecationWarning)
571 571 self.commit_ids = value
572 572
573 573 def get_changeset(self, revision=None, pre_load=None):
574 574 warnings.warn("Use get_commit instead", DeprecationWarning)
575 575 commit_id = None
576 576 commit_idx = None
577 577 if isinstance(revision, basestring):
578 578 commit_id = revision
579 579 else:
580 580 commit_idx = revision
581 581 return self.get_commit(
582 582 commit_id=commit_id, commit_idx=commit_idx, pre_load=pre_load)
583 583
584 584 def get_changesets(
585 585 self, start=None, end=None, start_date=None, end_date=None,
586 586 branch_name=None, pre_load=None):
587 587 warnings.warn("Use get_commits instead", DeprecationWarning)
588 588 start_id = self._revision_to_commit(start)
589 589 end_id = self._revision_to_commit(end)
590 590 return self.get_commits(
591 591 start_id=start_id, end_id=end_id, start_date=start_date,
592 592 end_date=end_date, branch_name=branch_name, pre_load=pre_load)
593 593
594 594 def _revision_to_commit(self, revision):
595 595 """
596 596 Translates a revision to a commit_id
597 597
598 598 Helps to support the old changeset based API which allows to use
599 599 commit ids and commit indices interchangeable.
600 600 """
601 601 if revision is None:
602 602 return revision
603 603
604 604 if isinstance(revision, basestring):
605 605 commit_id = revision
606 606 else:
607 607 commit_id = self.commit_ids[revision]
608 608 return commit_id
609 609
610 610 @property
611 611 def in_memory_changeset(self):
612 612 warnings.warn("Use in_memory_commit instead", DeprecationWarning)
613 613 return self.in_memory_commit
614 614
615 615
616 616 class BaseCommit(object):
617 617 """
618 618 Each backend should implement it's commit representation.
619 619
620 620 **Attributes**
621 621
622 622 ``repository``
623 623 repository object within which commit exists
624 624
625 625 ``id``
626 626 The commit id, may be ``raw_id`` or i.e. for mercurial's tip
627 627 just ``tip``.
628 628
629 629 ``raw_id``
630 630 raw commit representation (i.e. full 40 length sha for git
631 631 backend)
632 632
633 633 ``short_id``
634 634 shortened (if apply) version of ``raw_id``; it would be simple
635 635 shortcut for ``raw_id[:12]`` for git/mercurial backends or same
636 636 as ``raw_id`` for subversion
637 637
638 638 ``idx``
639 639 commit index
640 640
641 641 ``files``
642 642 list of ``FileNode`` (``Node`` with NodeKind.FILE) objects
643 643
644 644 ``dirs``
645 645 list of ``DirNode`` (``Node`` with NodeKind.DIR) objects
646 646
647 647 ``nodes``
648 648 combined list of ``Node`` objects
649 649
650 650 ``author``
651 651 author of the commit, as unicode
652 652
653 653 ``message``
654 654 message of the commit, as unicode
655 655
656 656 ``parents``
657 657 list of parent commits
658 658
659 659 """
660 660
661 661 branch = None
662 662 """
663 663 Depending on the backend this should be set to the branch name of the
664 664 commit. Backends not supporting branches on commits should leave this
665 665 value as ``None``.
666 666 """
667 667
668 668 _ARCHIVE_PREFIX_TEMPLATE = b'{repo_name}-{short_id}'
669 669 """
670 670 This template is used to generate a default prefix for repository archives
671 671 if no prefix has been specified.
672 672 """
673 673
674 674 def __str__(self):
675 675 return '<%s at %s:%s>' % (
676 676 self.__class__.__name__, self.idx, self.short_id)
677 677
678 678 def __repr__(self):
679 679 return self.__str__()
680 680
681 681 def __unicode__(self):
682 682 return u'%s:%s' % (self.idx, self.short_id)
683 683
684 684 def __eq__(self, other):
685 685 same_instance = isinstance(other, self.__class__)
686 686 return same_instance and self.raw_id == other.raw_id
687 687
688 688 def __json__(self):
689 689 parents = []
690 690 try:
691 691 for parent in self.parents:
692 692 parents.append({'raw_id': parent.raw_id})
693 693 except NotImplementedError:
694 694 # empty commit doesn't have parents implemented
695 695 pass
696 696
697 697 return {
698 698 'short_id': self.short_id,
699 699 'raw_id': self.raw_id,
700 700 'revision': self.idx,
701 701 'message': self.message,
702 702 'date': self.date,
703 703 'author': self.author,
704 704 'parents': parents,
705 705 'branch': self.branch
706 706 }
707 707
708 708 @LazyProperty
709 709 def last(self):
710 710 """
711 711 ``True`` if this is last commit in repository, ``False``
712 712 otherwise; trying to access this attribute while there is no
713 713 commits would raise `EmptyRepositoryError`
714 714 """
715 715 if self.repository is None:
716 716 raise CommitError("Cannot check if it's most recent commit")
717 717 return self.raw_id == self.repository.commit_ids[-1]
718 718
719 719 @LazyProperty
720 720 def parents(self):
721 721 """
722 722 Returns list of parent commits.
723 723 """
724 724 raise NotImplementedError
725 725
726 726 @property
727 727 def merge(self):
728 728 """
729 729 Returns boolean if commit is a merge.
730 730 """
731 731 return len(self.parents) > 1
732 732
733 733 @LazyProperty
734 734 def children(self):
735 735 """
736 736 Returns list of child commits.
737 737 """
738 738 raise NotImplementedError
739 739
740 740 @LazyProperty
741 741 def id(self):
742 742 """
743 743 Returns string identifying this commit.
744 744 """
745 745 raise NotImplementedError
746 746
747 747 @LazyProperty
748 748 def raw_id(self):
749 749 """
750 750 Returns raw string identifying this commit.
751 751 """
752 752 raise NotImplementedError
753 753
754 754 @LazyProperty
755 755 def short_id(self):
756 756 """
757 757 Returns shortened version of ``raw_id`` attribute, as string,
758 758 identifying this commit, useful for presentation to users.
759 759 """
760 760 raise NotImplementedError
761 761
762 762 @LazyProperty
763 763 def idx(self):
764 764 """
765 765 Returns integer identifying this commit.
766 766 """
767 767 raise NotImplementedError
768 768
769 769 @LazyProperty
770 770 def committer(self):
771 771 """
772 772 Returns committer for this commit
773 773 """
774 774 raise NotImplementedError
775 775
776 776 @LazyProperty
777 777 def committer_name(self):
778 778 """
779 779 Returns committer name for this commit
780 780 """
781 781
782 782 return author_name(self.committer)
783 783
784 784 @LazyProperty
785 785 def committer_email(self):
786 786 """
787 787 Returns committer email address for this commit
788 788 """
789 789
790 790 return author_email(self.committer)
791 791
792 792 @LazyProperty
793 793 def author(self):
794 794 """
795 795 Returns author for this commit
796 796 """
797 797
798 798 raise NotImplementedError
799 799
800 800 @LazyProperty
801 801 def author_name(self):
802 802 """
803 803 Returns author name for this commit
804 804 """
805 805
806 806 return author_name(self.author)
807 807
808 808 @LazyProperty
809 809 def author_email(self):
810 810 """
811 811 Returns author email address for this commit
812 812 """
813 813
814 814 return author_email(self.author)
815 815
816 816 def get_file_mode(self, path):
817 817 """
818 818 Returns stat mode of the file at `path`.
819 819 """
820 820 raise NotImplementedError
821 821
822 822 def is_link(self, path):
823 823 """
824 824 Returns ``True`` if given `path` is a symlink
825 825 """
826 826 raise NotImplementedError
827 827
828 828 def get_file_content(self, path):
829 829 """
830 830 Returns content of the file at the given `path`.
831 831 """
832 832 raise NotImplementedError
833 833
834 834 def get_file_size(self, path):
835 835 """
836 836 Returns size of the file at the given `path`.
837 837 """
838 838 raise NotImplementedError
839 839
840 840 def get_file_commit(self, path, pre_load=None):
841 841 """
842 842 Returns last commit of the file at the given `path`.
843 843
844 844 :param pre_load: Optional. List of commit attributes to load.
845 845 """
846 846 commits = self.get_file_history(path, limit=1, pre_load=pre_load)
847 847 if not commits:
848 848 raise RepositoryError(
849 849 'Failed to fetch history for path {}. '
850 850 'Please check if such path exists in your repository'.format(
851 851 path))
852 852 return commits[0]
853 853
854 854 def get_file_history(self, path, limit=None, pre_load=None):
855 855 """
856 856 Returns history of file as reversed list of :class:`BaseCommit`
857 857 objects for which file at given `path` has been modified.
858 858
859 859 :param limit: Optional. Allows to limit the size of the returned
860 860 history. This is intended as a hint to the underlying backend, so
861 861 that it can apply optimizations depending on the limit.
862 862 :param pre_load: Optional. List of commit attributes to load.
863 863 """
864 864 raise NotImplementedError
865 865
866 866 def get_file_annotate(self, path, pre_load=None):
867 867 """
868 868 Returns a generator of four element tuples with
869 869 lineno, sha, commit lazy loader and line
870 870
871 871 :param pre_load: Optional. List of commit attributes to load.
872 872 """
873 873 raise NotImplementedError
874 874
875 875 def get_nodes(self, path):
876 876 """
877 877 Returns combined ``DirNode`` and ``FileNode`` objects list representing
878 878 state of commit at the given ``path``.
879 879
880 880 :raises ``CommitError``: if node at the given ``path`` is not
881 881 instance of ``DirNode``
882 882 """
883 883 raise NotImplementedError
884 884
885 885 def get_node(self, path):
886 886 """
887 887 Returns ``Node`` object from the given ``path``.
888 888
889 889 :raises ``NodeDoesNotExistError``: if there is no node at the given
890 890 ``path``
891 891 """
892 892 raise NotImplementedError
893 893
894 894 def get_largefile_node(self, path):
895 895 """
896 896 Returns the path to largefile from Mercurial/Git-lfs storage.
897 897 or None if it's not a largefile node
898 898 """
899 899 return None
900 900
901 901 def archive_repo(self, file_path, kind='tgz', subrepos=None,
902 902 prefix=None, write_metadata=False, mtime=None):
903 903 """
904 904 Creates an archive containing the contents of the repository.
905 905
906 906 :param file_path: path to the file which to create the archive.
907 907 :param kind: one of following: ``"tbz2"``, ``"tgz"``, ``"zip"``.
908 908 :param prefix: name of root directory in archive.
909 909 Default is repository name and commit's short_id joined with dash:
910 910 ``"{repo_name}-{short_id}"``.
911 911 :param write_metadata: write a metadata file into archive.
912 912 :param mtime: custom modification time for archive creation, defaults
913 913 to time.time() if not given.
914 914
915 915 :raise VCSError: If prefix has a problem.
916 916 """
917 917 allowed_kinds = settings.ARCHIVE_SPECS.keys()
918 918 if kind not in allowed_kinds:
919 919 raise ImproperArchiveTypeError(
920 920 'Archive kind (%s) not supported use one of %s' %
921 921 (kind, allowed_kinds))
922 922
923 923 prefix = self._validate_archive_prefix(prefix)
924 924
925 925 mtime = mtime or time.mktime(self.date.timetuple())
926 926
927 927 file_info = []
928 928 cur_rev = self.repository.get_commit(commit_id=self.raw_id)
929 929 for _r, _d, files in cur_rev.walk('/'):
930 930 for f in files:
931 931 f_path = os.path.join(prefix, f.path)
932 932 file_info.append(
933 933 (f_path, f.mode, f.is_link(), f.raw_bytes))
934 934
935 935 if write_metadata:
936 936 metadata = [
937 937 ('repo_name', self.repository.name),
938 938 ('rev', self.raw_id),
939 939 ('create_time', mtime),
940 940 ('branch', self.branch),
941 941 ('tags', ','.join(self.tags)),
942 942 ]
943 943 meta = ["%s:%s" % (f_name, value) for f_name, value in metadata]
944 944 file_info.append(('.archival.txt', 0644, False, '\n'.join(meta)))
945 945
946 946 connection.Hg.archive_repo(file_path, mtime, file_info, kind)
947 947
948 948 def _validate_archive_prefix(self, prefix):
949 949 if prefix is None:
950 950 prefix = self._ARCHIVE_PREFIX_TEMPLATE.format(
951 951 repo_name=safe_str(self.repository.name),
952 952 short_id=self.short_id)
953 953 elif not isinstance(prefix, str):
954 954 raise ValueError("prefix not a bytes object: %s" % repr(prefix))
955 955 elif prefix.startswith('/'):
956 956 raise VCSError("Prefix cannot start with leading slash")
957 957 elif prefix.strip() == '':
958 958 raise VCSError("Prefix cannot be empty")
959 959 return prefix
960 960
961 961 @LazyProperty
962 962 def root(self):
963 963 """
964 964 Returns ``RootNode`` object for this commit.
965 965 """
966 966 return self.get_node('')
967 967
968 968 def next(self, branch=None):
969 969 """
970 970 Returns next commit from current, if branch is gives it will return
971 971 next commit belonging to this branch
972 972
973 973 :param branch: show commits within the given named branch
974 974 """
975 975 indexes = xrange(self.idx + 1, self.repository.count())
976 976 return self._find_next(indexes, branch)
977 977
978 978 def prev(self, branch=None):
979 979 """
980 980 Returns previous commit from current, if branch is gives it will
981 981 return previous commit belonging to this branch
982 982
983 983 :param branch: show commit within the given named branch
984 984 """
985 985 indexes = xrange(self.idx - 1, -1, -1)
986 986 return self._find_next(indexes, branch)
987 987
988 988 def _find_next(self, indexes, branch=None):
989 989 if branch and self.branch != branch:
990 990 raise VCSError('Branch option used on commit not belonging '
991 991 'to that branch')
992 992
993 993 for next_idx in indexes:
994 994 commit = self.repository.get_commit(commit_idx=next_idx)
995 995 if branch and branch != commit.branch:
996 996 continue
997 997 return commit
998 998 raise CommitDoesNotExistError
999 999
1000 1000 def diff(self, ignore_whitespace=True, context=3):
1001 1001 """
1002 1002 Returns a `Diff` object representing the change made by this commit.
1003 1003 """
1004 1004 parent = (
1005 1005 self.parents[0] if self.parents else self.repository.EMPTY_COMMIT)
1006 1006 diff = self.repository.get_diff(
1007 1007 parent, self,
1008 1008 ignore_whitespace=ignore_whitespace,
1009 1009 context=context)
1010 1010 return diff
1011 1011
1012 1012 @LazyProperty
1013 1013 def added(self):
1014 1014 """
1015 1015 Returns list of added ``FileNode`` objects.
1016 1016 """
1017 1017 raise NotImplementedError
1018 1018
1019 1019 @LazyProperty
1020 1020 def changed(self):
1021 1021 """
1022 1022 Returns list of modified ``FileNode`` objects.
1023 1023 """
1024 1024 raise NotImplementedError
1025 1025
1026 1026 @LazyProperty
1027 1027 def removed(self):
1028 1028 """
1029 1029 Returns list of removed ``FileNode`` objects.
1030 1030 """
1031 1031 raise NotImplementedError
1032 1032
1033 1033 @LazyProperty
1034 1034 def size(self):
1035 1035 """
1036 1036 Returns total number of bytes from contents of all filenodes.
1037 1037 """
1038 1038 return sum((node.size for node in self.get_filenodes_generator()))
1039 1039
1040 1040 def walk(self, topurl=''):
1041 1041 """
1042 1042 Similar to os.walk method. Insted of filesystem it walks through
1043 1043 commit starting at given ``topurl``. Returns generator of tuples
1044 1044 (topnode, dirnodes, filenodes).
1045 1045 """
1046 1046 topnode = self.get_node(topurl)
1047 1047 if not topnode.is_dir():
1048 1048 return
1049 1049 yield (topnode, topnode.dirs, topnode.files)
1050 1050 for dirnode in topnode.dirs:
1051 1051 for tup in self.walk(dirnode.path):
1052 1052 yield tup
1053 1053
1054 1054 def get_filenodes_generator(self):
1055 1055 """
1056 1056 Returns generator that yields *all* file nodes.
1057 1057 """
1058 1058 for topnode, dirs, files in self.walk():
1059 1059 for node in files:
1060 1060 yield node
1061 1061
1062 1062 #
1063 1063 # Utilities for sub classes to support consistent behavior
1064 1064 #
1065 1065
1066 1066 def no_node_at_path(self, path):
1067 1067 return NodeDoesNotExistError(
1068 1068 u"There is no file nor directory at the given path: "
1069 u"'%s' at commit %s" % (safe_unicode(path), self.short_id))
1069 u"`%s` at commit %s" % (safe_unicode(path), self.short_id))
1070 1070
1071 1071 def _fix_path(self, path):
1072 1072 """
1073 1073 Paths are stored without trailing slash so we need to get rid off it if
1074 1074 needed.
1075 1075 """
1076 1076 return path.rstrip('/')
1077 1077
1078 1078 #
1079 1079 # Deprecated API based on changesets
1080 1080 #
1081 1081
1082 1082 @property
1083 1083 def revision(self):
1084 1084 warnings.warn("Use idx instead", DeprecationWarning)
1085 1085 return self.idx
1086 1086
1087 1087 @revision.setter
1088 1088 def revision(self, value):
1089 1089 warnings.warn("Use idx instead", DeprecationWarning)
1090 1090 self.idx = value
1091 1091
1092 1092 def get_file_changeset(self, path):
1093 1093 warnings.warn("Use get_file_commit instead", DeprecationWarning)
1094 1094 return self.get_file_commit(path)
1095 1095
1096 1096
1097 1097 class BaseChangesetClass(type):
1098 1098
1099 1099 def __instancecheck__(self, instance):
1100 1100 return isinstance(instance, BaseCommit)
1101 1101
1102 1102
1103 1103 class BaseChangeset(BaseCommit):
1104 1104
1105 1105 __metaclass__ = BaseChangesetClass
1106 1106
1107 1107 def __new__(cls, *args, **kwargs):
1108 1108 warnings.warn(
1109 1109 "Use BaseCommit instead of BaseChangeset", DeprecationWarning)
1110 1110 return super(BaseChangeset, cls).__new__(cls, *args, **kwargs)
1111 1111
1112 1112
1113 1113 class BaseInMemoryCommit(object):
1114 1114 """
1115 1115 Represents differences between repository's state (most recent head) and
1116 1116 changes made *in place*.
1117 1117
1118 1118 **Attributes**
1119 1119
1120 1120 ``repository``
1121 1121 repository object for this in-memory-commit
1122 1122
1123 1123 ``added``
1124 1124 list of ``FileNode`` objects marked as *added*
1125 1125
1126 1126 ``changed``
1127 1127 list of ``FileNode`` objects marked as *changed*
1128 1128
1129 1129 ``removed``
1130 1130 list of ``FileNode`` or ``RemovedFileNode`` objects marked to be
1131 1131 *removed*
1132 1132
1133 1133 ``parents``
1134 1134 list of :class:`BaseCommit` instances representing parents of
1135 1135 in-memory commit. Should always be 2-element sequence.
1136 1136
1137 1137 """
1138 1138
1139 1139 def __init__(self, repository):
1140 1140 self.repository = repository
1141 1141 self.added = []
1142 1142 self.changed = []
1143 1143 self.removed = []
1144 1144 self.parents = []
1145 1145
1146 1146 def add(self, *filenodes):
1147 1147 """
1148 1148 Marks given ``FileNode`` objects as *to be committed*.
1149 1149
1150 1150 :raises ``NodeAlreadyExistsError``: if node with same path exists at
1151 1151 latest commit
1152 1152 :raises ``NodeAlreadyAddedError``: if node with same path is already
1153 1153 marked as *added*
1154 1154 """
1155 1155 # Check if not already marked as *added* first
1156 1156 for node in filenodes:
1157 1157 if node.path in (n.path for n in self.added):
1158 1158 raise NodeAlreadyAddedError(
1159 1159 "Such FileNode %s is already marked for addition"
1160 1160 % node.path)
1161 1161 for node in filenodes:
1162 1162 self.added.append(node)
1163 1163
1164 1164 def change(self, *filenodes):
1165 1165 """
1166 1166 Marks given ``FileNode`` objects to be *changed* in next commit.
1167 1167
1168 1168 :raises ``EmptyRepositoryError``: if there are no commits yet
1169 1169 :raises ``NodeAlreadyExistsError``: if node with same path is already
1170 1170 marked to be *changed*
1171 1171 :raises ``NodeAlreadyRemovedError``: if node with same path is already
1172 1172 marked to be *removed*
1173 1173 :raises ``NodeDoesNotExistError``: if node doesn't exist in latest
1174 1174 commit
1175 1175 :raises ``NodeNotChangedError``: if node hasn't really be changed
1176 1176 """
1177 1177 for node in filenodes:
1178 1178 if node.path in (n.path for n in self.removed):
1179 1179 raise NodeAlreadyRemovedError(
1180 1180 "Node at %s is already marked as removed" % node.path)
1181 1181 try:
1182 1182 self.repository.get_commit()
1183 1183 except EmptyRepositoryError:
1184 1184 raise EmptyRepositoryError(
1185 1185 "Nothing to change - try to *add* new nodes rather than "
1186 1186 "changing them")
1187 1187 for node in filenodes:
1188 1188 if node.path in (n.path for n in self.changed):
1189 1189 raise NodeAlreadyChangedError(
1190 1190 "Node at '%s' is already marked as changed" % node.path)
1191 1191 self.changed.append(node)
1192 1192
1193 1193 def remove(self, *filenodes):
1194 1194 """
1195 1195 Marks given ``FileNode`` (or ``RemovedFileNode``) objects to be
1196 1196 *removed* in next commit.
1197 1197
1198 1198 :raises ``NodeAlreadyRemovedError``: if node has been already marked to
1199 1199 be *removed*
1200 1200 :raises ``NodeAlreadyChangedError``: if node has been already marked to
1201 1201 be *changed*
1202 1202 """
1203 1203 for node in filenodes:
1204 1204 if node.path in (n.path for n in self.removed):
1205 1205 raise NodeAlreadyRemovedError(
1206 1206 "Node is already marked to for removal at %s" % node.path)
1207 1207 if node.path in (n.path for n in self.changed):
1208 1208 raise NodeAlreadyChangedError(
1209 1209 "Node is already marked to be changed at %s" % node.path)
1210 1210 # We only mark node as *removed* - real removal is done by
1211 1211 # commit method
1212 1212 self.removed.append(node)
1213 1213
1214 1214 def reset(self):
1215 1215 """
1216 1216 Resets this instance to initial state (cleans ``added``, ``changed``
1217 1217 and ``removed`` lists).
1218 1218 """
1219 1219 self.added = []
1220 1220 self.changed = []
1221 1221 self.removed = []
1222 1222 self.parents = []
1223 1223
1224 1224 def get_ipaths(self):
1225 1225 """
1226 1226 Returns generator of paths from nodes marked as added, changed or
1227 1227 removed.
1228 1228 """
1229 1229 for node in itertools.chain(self.added, self.changed, self.removed):
1230 1230 yield node.path
1231 1231
1232 1232 def get_paths(self):
1233 1233 """
1234 1234 Returns list of paths from nodes marked as added, changed or removed.
1235 1235 """
1236 1236 return list(self.get_ipaths())
1237 1237
1238 1238 def check_integrity(self, parents=None):
1239 1239 """
1240 1240 Checks in-memory commit's integrity. Also, sets parents if not
1241 1241 already set.
1242 1242
1243 1243 :raises CommitError: if any error occurs (i.e.
1244 1244 ``NodeDoesNotExistError``).
1245 1245 """
1246 1246 if not self.parents:
1247 1247 parents = parents or []
1248 1248 if len(parents) == 0:
1249 1249 try:
1250 1250 parents = [self.repository.get_commit(), None]
1251 1251 except EmptyRepositoryError:
1252 1252 parents = [None, None]
1253 1253 elif len(parents) == 1:
1254 1254 parents += [None]
1255 1255 self.parents = parents
1256 1256
1257 1257 # Local parents, only if not None
1258 1258 parents = [p for p in self.parents if p]
1259 1259
1260 1260 # Check nodes marked as added
1261 1261 for p in parents:
1262 1262 for node in self.added:
1263 1263 try:
1264 1264 p.get_node(node.path)
1265 1265 except NodeDoesNotExistError:
1266 1266 pass
1267 1267 else:
1268 1268 raise NodeAlreadyExistsError(
1269 1269 "Node `%s` already exists at %s" % (node.path, p))
1270 1270
1271 1271 # Check nodes marked as changed
1272 1272 missing = set(self.changed)
1273 1273 not_changed = set(self.changed)
1274 1274 if self.changed and not parents:
1275 1275 raise NodeDoesNotExistError(str(self.changed[0].path))
1276 1276 for p in parents:
1277 1277 for node in self.changed:
1278 1278 try:
1279 1279 old = p.get_node(node.path)
1280 1280 missing.remove(node)
1281 1281 # if content actually changed, remove node from not_changed
1282 1282 if old.content != node.content:
1283 1283 not_changed.remove(node)
1284 1284 except NodeDoesNotExistError:
1285 1285 pass
1286 1286 if self.changed and missing:
1287 1287 raise NodeDoesNotExistError(
1288 1288 "Node `%s` marked as modified but missing in parents: %s"
1289 1289 % (node.path, parents))
1290 1290
1291 1291 if self.changed and not_changed:
1292 1292 raise NodeNotChangedError(
1293 1293 "Node `%s` wasn't actually changed (parents: %s)"
1294 1294 % (not_changed.pop().path, parents))
1295 1295
1296 1296 # Check nodes marked as removed
1297 1297 if self.removed and not parents:
1298 1298 raise NodeDoesNotExistError(
1299 1299 "Cannot remove node at %s as there "
1300 1300 "were no parents specified" % self.removed[0].path)
1301 1301 really_removed = set()
1302 1302 for p in parents:
1303 1303 for node in self.removed:
1304 1304 try:
1305 1305 p.get_node(node.path)
1306 1306 really_removed.add(node)
1307 1307 except CommitError:
1308 1308 pass
1309 1309 not_removed = set(self.removed) - really_removed
1310 1310 if not_removed:
1311 1311 # TODO: johbo: This code branch does not seem to be covered
1312 1312 raise NodeDoesNotExistError(
1313 1313 "Cannot remove node at %s from "
1314 1314 "following parents: %s" % (not_removed, parents))
1315 1315
1316 1316 def commit(
1317 1317 self, message, author, parents=None, branch=None, date=None,
1318 1318 **kwargs):
1319 1319 """
1320 1320 Performs in-memory commit (doesn't check workdir in any way) and
1321 1321 returns newly created :class:`BaseCommit`. Updates repository's
1322 1322 attribute `commits`.
1323 1323
1324 1324 .. note::
1325 1325
1326 1326 While overriding this method each backend's should call
1327 1327 ``self.check_integrity(parents)`` in the first place.
1328 1328
1329 1329 :param message: message of the commit
1330 1330 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
1331 1331 :param parents: single parent or sequence of parents from which commit
1332 1332 would be derived
1333 1333 :param date: ``datetime.datetime`` instance. Defaults to
1334 1334 ``datetime.datetime.now()``.
1335 1335 :param branch: branch name, as string. If none given, default backend's
1336 1336 branch would be used.
1337 1337
1338 1338 :raises ``CommitError``: if any error occurs while committing
1339 1339 """
1340 1340 raise NotImplementedError
1341 1341
1342 1342
1343 1343 class BaseInMemoryChangesetClass(type):
1344 1344
1345 1345 def __instancecheck__(self, instance):
1346 1346 return isinstance(instance, BaseInMemoryCommit)
1347 1347
1348 1348
1349 1349 class BaseInMemoryChangeset(BaseInMemoryCommit):
1350 1350
1351 1351 __metaclass__ = BaseInMemoryChangesetClass
1352 1352
1353 1353 def __new__(cls, *args, **kwargs):
1354 1354 warnings.warn(
1355 1355 "Use BaseCommit instead of BaseInMemoryCommit", DeprecationWarning)
1356 1356 return super(BaseInMemoryChangeset, cls).__new__(cls, *args, **kwargs)
1357 1357
1358 1358
1359 1359 class EmptyCommit(BaseCommit):
1360 1360 """
1361 1361 An dummy empty commit. It's possible to pass hash when creating
1362 1362 an EmptyCommit
1363 1363 """
1364 1364
1365 1365 def __init__(
1366 1366 self, commit_id='0' * 40, repo=None, alias=None, idx=-1,
1367 1367 message='', author='', date=None):
1368 1368 self._empty_commit_id = commit_id
1369 1369 # TODO: johbo: Solve idx parameter, default value does not make
1370 1370 # too much sense
1371 1371 self.idx = idx
1372 1372 self.message = message
1373 1373 self.author = author
1374 1374 self.date = date or datetime.datetime.fromtimestamp(0)
1375 1375 self.repository = repo
1376 1376 self.alias = alias
1377 1377
1378 1378 @LazyProperty
1379 1379 def raw_id(self):
1380 1380 """
1381 1381 Returns raw string identifying this commit, useful for web
1382 1382 representation.
1383 1383 """
1384 1384
1385 1385 return self._empty_commit_id
1386 1386
1387 1387 @LazyProperty
1388 1388 def branch(self):
1389 1389 if self.alias:
1390 1390 from rhodecode.lib.vcs.backends import get_backend
1391 1391 return get_backend(self.alias).DEFAULT_BRANCH_NAME
1392 1392
1393 1393 @LazyProperty
1394 1394 def short_id(self):
1395 1395 return self.raw_id[:12]
1396 1396
1397 1397 @LazyProperty
1398 1398 def id(self):
1399 1399 return self.raw_id
1400 1400
1401 1401 def get_file_commit(self, path):
1402 1402 return self
1403 1403
1404 1404 def get_file_content(self, path):
1405 1405 return u''
1406 1406
1407 1407 def get_file_size(self, path):
1408 1408 return 0
1409 1409
1410 1410
1411 1411 class EmptyChangesetClass(type):
1412 1412
1413 1413 def __instancecheck__(self, instance):
1414 1414 return isinstance(instance, EmptyCommit)
1415 1415
1416 1416
1417 1417 class EmptyChangeset(EmptyCommit):
1418 1418
1419 1419 __metaclass__ = EmptyChangesetClass
1420 1420
1421 1421 def __new__(cls, *args, **kwargs):
1422 1422 warnings.warn(
1423 1423 "Use EmptyCommit instead of EmptyChangeset", DeprecationWarning)
1424 1424 return super(EmptyCommit, cls).__new__(cls, *args, **kwargs)
1425 1425
1426 1426 def __init__(self, cs='0' * 40, repo=None, requested_revision=None,
1427 1427 alias=None, revision=-1, message='', author='', date=None):
1428 1428 if requested_revision is not None:
1429 1429 warnings.warn(
1430 1430 "Parameter requested_revision not supported anymore",
1431 1431 DeprecationWarning)
1432 1432 super(EmptyChangeset, self).__init__(
1433 1433 commit_id=cs, repo=repo, alias=alias, idx=revision,
1434 1434 message=message, author=author, date=date)
1435 1435
1436 1436 @property
1437 1437 def revision(self):
1438 1438 warnings.warn("Use idx instead", DeprecationWarning)
1439 1439 return self.idx
1440 1440
1441 1441 @revision.setter
1442 1442 def revision(self, value):
1443 1443 warnings.warn("Use idx instead", DeprecationWarning)
1444 1444 self.idx = value
1445 1445
1446 1446
1447 1447 class EmptyRepository(BaseRepository):
1448 1448 def __init__(self, repo_path=None, config=None, create=False, **kwargs):
1449 1449 pass
1450 1450
1451 1451 def get_diff(self, *args, **kwargs):
1452 1452 from rhodecode.lib.vcs.backends.git.diff import GitDiff
1453 1453 return GitDiff('')
1454 1454
1455 1455
1456 1456 class CollectionGenerator(object):
1457 1457
1458 1458 def __init__(self, repo, commit_ids, collection_size=None, pre_load=None):
1459 1459 self.repo = repo
1460 1460 self.commit_ids = commit_ids
1461 1461 # TODO: (oliver) this isn't currently hooked up
1462 1462 self.collection_size = None
1463 1463 self.pre_load = pre_load
1464 1464
1465 1465 def __len__(self):
1466 1466 if self.collection_size is not None:
1467 1467 return self.collection_size
1468 1468 return self.commit_ids.__len__()
1469 1469
1470 1470 def __iter__(self):
1471 1471 for commit_id in self.commit_ids:
1472 1472 # TODO: johbo: Mercurial passes in commit indices or commit ids
1473 1473 yield self._commit_factory(commit_id)
1474 1474
1475 1475 def _commit_factory(self, commit_id):
1476 1476 """
1477 1477 Allows backends to override the way commits are generated.
1478 1478 """
1479 1479 return self.repo.get_commit(commit_id=commit_id,
1480 1480 pre_load=self.pre_load)
1481 1481
1482 1482 def __getslice__(self, i, j):
1483 1483 """
1484 1484 Returns an iterator of sliced repository
1485 1485 """
1486 1486 commit_ids = self.commit_ids[i:j]
1487 1487 return self.__class__(
1488 1488 self.repo, commit_ids, pre_load=self.pre_load)
1489 1489
1490 1490 def __repr__(self):
1491 1491 return '<CollectionGenerator[len:%s]>' % (self.__len__())
1492 1492
1493 1493
1494 1494 class Config(object):
1495 1495 """
1496 1496 Represents the configuration for a repository.
1497 1497
1498 1498 The API is inspired by :class:`ConfigParser.ConfigParser` from the
1499 1499 standard library. It implements only the needed subset.
1500 1500 """
1501 1501
1502 1502 def __init__(self):
1503 1503 self._values = {}
1504 1504
1505 1505 def copy(self):
1506 1506 clone = Config()
1507 1507 for section, values in self._values.items():
1508 1508 clone._values[section] = values.copy()
1509 1509 return clone
1510 1510
1511 1511 def __repr__(self):
1512 1512 return '<Config(%s sections) at %s>' % (
1513 1513 len(self._values), hex(id(self)))
1514 1514
1515 1515 def items(self, section):
1516 1516 return self._values.get(section, {}).iteritems()
1517 1517
1518 1518 def get(self, section, option):
1519 1519 return self._values.get(section, {}).get(option)
1520 1520
1521 1521 def set(self, section, option, value):
1522 1522 section_values = self._values.setdefault(section, {})
1523 1523 section_values[option] = value
1524 1524
1525 1525 def clear_section(self, section):
1526 1526 self._values[section] = {}
1527 1527
1528 1528 def serialize(self):
1529 1529 """
1530 1530 Creates a list of three tuples (section, key, value) representing
1531 1531 this config object.
1532 1532 """
1533 1533 items = []
1534 1534 for section in self._values:
1535 1535 for option, value in self._values[section].items():
1536 1536 items.append(
1537 1537 (safe_str(section), safe_str(option), safe_str(value)))
1538 1538 return items
1539 1539
1540 1540
1541 1541 class Diff(object):
1542 1542 """
1543 1543 Represents a diff result from a repository backend.
1544 1544
1545 1545 Subclasses have to provide a backend specific value for
1546 1546 :attr:`_header_re` and :attr:`_meta_re`.
1547 1547 """
1548 1548 _meta_re = None
1549 1549 _header_re = None
1550 1550
1551 1551 def __init__(self, raw_diff):
1552 1552 self.raw = raw_diff
1553 1553
1554 1554 def chunks(self):
1555 1555 """
1556 1556 split the diff in chunks of separate --git a/file b/file chunks
1557 1557 to make diffs consistent we must prepend with \n, and make sure
1558 1558 we can detect last chunk as this was also has special rule
1559 1559 """
1560 1560
1561 1561 diff_parts = ('\n' + self.raw).split('\ndiff --git')
1562 1562 header = diff_parts[0]
1563 1563
1564 1564 if self._meta_re:
1565 1565 match = self._meta_re.match(header)
1566 1566
1567 1567 chunks = diff_parts[1:]
1568 1568 total_chunks = len(chunks)
1569 1569
1570 1570 return (
1571 1571 DiffChunk(chunk, self, cur_chunk == total_chunks)
1572 1572 for cur_chunk, chunk in enumerate(chunks, start=1))
1573 1573
1574 1574
1575 1575 class DiffChunk(object):
1576 1576
1577 1577 def __init__(self, chunk, diff, last_chunk):
1578 1578 self._diff = diff
1579 1579
1580 1580 # since we split by \ndiff --git that part is lost from original diff
1581 1581 # we need to re-apply it at the end, EXCEPT ! if it's last chunk
1582 1582 if not last_chunk:
1583 1583 chunk += '\n'
1584 1584
1585 1585 match = self._diff._header_re.match(chunk)
1586 1586 self.header = match.groupdict()
1587 1587 self.diff = chunk[match.end():]
1588 1588 self.raw = chunk
@@ -1,659 +1,675 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 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 mock
22 22 import pytest
23 23 import lxml.html
24 24
25 25 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
26 26 from rhodecode.tests import url, assert_session_flash
27 27 from rhodecode.tests.utils import AssertResponse, commit_change
28 28
29 29
30 30 @pytest.mark.usefixtures("autologin_user", "app")
31 31 class TestCompareController(object):
32 32
33 33 @pytest.mark.xfail_backends("svn", reason="Requires pull")
34 34 def test_compare_remote_with_different_commit_indexes(self, backend):
35 35 # Preparing the following repository structure:
36 36 #
37 37 # Origin repository has two commits:
38 38 #
39 39 # 0 1
40 40 # A -- D
41 41 #
42 42 # The fork of it has a few more commits and "D" has a commit index
43 43 # which does not exist in origin.
44 44 #
45 45 # 0 1 2 3 4
46 46 # A -- -- -- D -- E
47 47 # \- B -- C
48 48 #
49 49
50 50 fork = backend.create_repo()
51 51
52 52 # prepare fork
53 53 commit0 = commit_change(
54 54 fork.repo_name, filename='file1', content='A',
55 55 message='A', vcs_type=backend.alias, parent=None, newfile=True)
56 56
57 57 commit1 = commit_change(
58 58 fork.repo_name, filename='file1', content='B',
59 59 message='B, child of A', vcs_type=backend.alias, parent=commit0)
60 60
61 61 commit_change( # commit 2
62 62 fork.repo_name, filename='file1', content='C',
63 63 message='C, child of B', vcs_type=backend.alias, parent=commit1)
64 64
65 65 commit3 = commit_change(
66 66 fork.repo_name, filename='file1', content='D',
67 67 message='D, child of A', vcs_type=backend.alias, parent=commit0)
68 68
69 69 commit4 = commit_change(
70 70 fork.repo_name, filename='file1', content='E',
71 71 message='E, child of D', vcs_type=backend.alias, parent=commit3)
72 72
73 73 # prepare origin repository, taking just the history up to D
74 74 origin = backend.create_repo()
75 75
76 76 origin_repo = origin.scm_instance(cache=False)
77 77 origin_repo.config.clear_section('hooks')
78 78 origin_repo.pull(fork.repo_full_path, commit_ids=[commit3.raw_id])
79 79
80 80 # Verify test fixture setup
81 81 # This does not work for git
82 82 if backend.alias != 'git':
83 83 assert 5 == len(fork.scm_instance().commit_ids)
84 84 assert 2 == len(origin_repo.commit_ids)
85 85
86 86 # Comparing the revisions
87 87 response = self.app.get(
88 88 url('compare_url',
89 89 repo_name=origin.repo_name,
90 90 source_ref_type="rev",
91 91 source_ref=commit3.raw_id,
92 92 target_repo=fork.repo_name,
93 93 target_ref_type="rev",
94 94 target_ref=commit4.raw_id,
95 95 merge='1',))
96 96
97 97 compare_page = ComparePage(response)
98 98 compare_page.contains_commits([commit4])
99 99
100 100 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
101 101 def test_compare_forks_on_branch_extra_commits(self, backend):
102 102 repo1 = backend.create_repo()
103 103
104 104 # commit something !
105 105 commit0 = commit_change(
106 106 repo1.repo_name, filename='file1', content='line1\n',
107 107 message='commit1', vcs_type=backend.alias, parent=None,
108 108 newfile=True)
109 109
110 110 # fork this repo
111 111 repo2 = backend.create_fork()
112 112
113 113 # add two extra commit into fork
114 114 commit1 = commit_change(
115 115 repo2.repo_name, filename='file1', content='line1\nline2\n',
116 116 message='commit2', vcs_type=backend.alias, parent=commit0)
117 117
118 118 commit2 = commit_change(
119 119 repo2.repo_name, filename='file1', content='line1\nline2\nline3\n',
120 120 message='commit3', vcs_type=backend.alias, parent=commit1)
121 121
122 122 commit_id1 = repo1.scm_instance().DEFAULT_BRANCH_NAME
123 123 commit_id2 = repo2.scm_instance().DEFAULT_BRANCH_NAME
124 124
125 125 response = self.app.get(
126 126 url('compare_url',
127 127 repo_name=repo1.repo_name,
128 128 source_ref_type="branch",
129 129 source_ref=commit_id2,
130 130 target_repo=repo2.repo_name,
131 131 target_ref_type="branch",
132 132 target_ref=commit_id1,
133 133 merge='1',))
134 134
135 135 response.mustcontain('%s@%s' % (repo1.repo_name, commit_id2))
136 136 response.mustcontain('%s@%s' % (repo2.repo_name, commit_id1))
137 137
138 138 compare_page = ComparePage(response)
139 139 compare_page.contains_change_summary(1, 2, 0)
140 140 compare_page.contains_commits([commit1, commit2])
141 141 compare_page.contains_file_links_and_anchors([
142 142 ('file1', 'a_c--826e8142e6ba'),
143 143 ])
144 144
145 145 # Swap is removed when comparing branches since it's a PR feature and
146 146 # it is then a preview mode
147 147 compare_page.swap_is_hidden()
148 148 compare_page.target_source_are_disabled()
149 149
150 150 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
151 151 def test_compare_forks_on_branch_extra_commits_origin_has_incomming(
152 152 self, backend):
153 153 repo1 = backend.create_repo()
154 154
155 155 # commit something !
156 156 commit0 = commit_change(
157 157 repo1.repo_name, filename='file1', content='line1\n',
158 158 message='commit1', vcs_type=backend.alias, parent=None,
159 159 newfile=True)
160 160
161 161 # fork this repo
162 162 repo2 = backend.create_fork()
163 163
164 164 # now commit something to origin repo
165 165 commit_change(
166 166 repo1.repo_name, filename='file2', content='line1file2\n',
167 167 message='commit2', vcs_type=backend.alias, parent=commit0,
168 168 newfile=True)
169 169
170 170 # add two extra commit into fork
171 171 commit1 = commit_change(
172 172 repo2.repo_name, filename='file1', content='line1\nline2\n',
173 173 message='commit2', vcs_type=backend.alias, parent=commit0)
174 174
175 175 commit2 = commit_change(
176 176 repo2.repo_name, filename='file1', content='line1\nline2\nline3\n',
177 177 message='commit3', vcs_type=backend.alias, parent=commit1)
178 178
179 179 commit_id1 = repo1.scm_instance().DEFAULT_BRANCH_NAME
180 180 commit_id2 = repo2.scm_instance().DEFAULT_BRANCH_NAME
181 181
182 182 response = self.app.get(
183 183 url('compare_url',
184 184 repo_name=repo1.repo_name,
185 185 source_ref_type="branch",
186 186 source_ref=commit_id2,
187 187 target_repo=repo2.repo_name,
188 188 target_ref_type="branch",
189 189 target_ref=commit_id1,
190 190 merge='1'))
191 191
192 192 response.mustcontain('%s@%s' % (repo1.repo_name, commit_id2))
193 193 response.mustcontain('%s@%s' % (repo2.repo_name, commit_id1))
194 194
195 195 compare_page = ComparePage(response)
196 196 compare_page.contains_change_summary(1, 2, 0)
197 197 compare_page.contains_commits([commit1, commit2])
198 198 compare_page.contains_file_links_and_anchors([
199 199 ('file1', 'a_c--826e8142e6ba'),
200 200 ])
201 201
202 202 # Swap is removed when comparing branches since it's a PR feature and
203 203 # it is then a preview mode
204 204 compare_page.swap_is_hidden()
205 205 compare_page.target_source_are_disabled()
206 206
207 207 @pytest.mark.xfail_backends("svn")
208 208 # TODO(marcink): no svn support for compare two seperate repos
209 209 def test_compare_of_unrelated_forks(self, backend):
210 210 orig = backend.create_repo(number_of_commits=1)
211 211 fork = backend.create_repo(number_of_commits=1)
212 212
213 213 response = self.app.get(
214 214 url('compare_url',
215 215 repo_name=orig.repo_name,
216 216 action="compare",
217 217 source_ref_type="rev",
218 218 source_ref="tip",
219 219 target_ref_type="rev",
220 220 target_ref="tip",
221 221 merge='1',
222 222 target_repo=fork.repo_name),
223 223 status=400)
224 224
225 225 response.mustcontain("Repositories unrelated.")
226 226
227 227 @pytest.mark.xfail_backends("svn")
228 228 def test_compare_cherry_pick_commits_from_bottom(self, backend):
229 229
230 230 # repo1:
231 231 # commit0:
232 232 # commit1:
233 233 # repo1-fork- in which we will cherry pick bottom commits
234 234 # commit0:
235 235 # commit1:
236 236 # commit2: x
237 237 # commit3: x
238 238 # commit4: x
239 239 # commit5:
240 240 # make repo1, and commit1+commit2
241 241
242 242 repo1 = backend.create_repo()
243 243
244 244 # commit something !
245 245 commit0 = commit_change(
246 246 repo1.repo_name, filename='file1', content='line1\n',
247 247 message='commit1', vcs_type=backend.alias, parent=None,
248 248 newfile=True)
249 249 commit1 = commit_change(
250 250 repo1.repo_name, filename='file1', content='line1\nline2\n',
251 251 message='commit2', vcs_type=backend.alias, parent=commit0)
252 252
253 253 # fork this repo
254 254 repo2 = backend.create_fork()
255 255
256 256 # now make commit3-6
257 257 commit2 = commit_change(
258 258 repo1.repo_name, filename='file1', content='line1\nline2\nline3\n',
259 259 message='commit3', vcs_type=backend.alias, parent=commit1)
260 260 commit3 = commit_change(
261 261 repo1.repo_name, filename='file1',
262 262 content='line1\nline2\nline3\nline4\n', message='commit4',
263 263 vcs_type=backend.alias, parent=commit2)
264 264 commit4 = commit_change(
265 265 repo1.repo_name, filename='file1',
266 266 content='line1\nline2\nline3\nline4\nline5\n', message='commit5',
267 267 vcs_type=backend.alias, parent=commit3)
268 268 commit_change( # commit 5
269 269 repo1.repo_name, filename='file1',
270 270 content='line1\nline2\nline3\nline4\nline5\nline6\n',
271 271 message='commit6', vcs_type=backend.alias, parent=commit4)
272 272
273 273 response = self.app.get(
274 274 url('compare_url',
275 275 repo_name=repo2.repo_name,
276 276 source_ref_type="rev",
277 277 # parent of commit2, in target repo2
278 278 source_ref=commit1.raw_id,
279 279 target_repo=repo1.repo_name,
280 280 target_ref_type="rev",
281 281 target_ref=commit4.raw_id,
282 282 merge='1',))
283 283 response.mustcontain('%s@%s' % (repo2.repo_name, commit1.short_id))
284 284 response.mustcontain('%s@%s' % (repo1.repo_name, commit4.short_id))
285 285
286 286 # files
287 287 compare_page = ComparePage(response)
288 288 compare_page.contains_change_summary(1, 3, 0)
289 289 compare_page.contains_commits([commit2, commit3, commit4])
290 290 compare_page.contains_file_links_and_anchors([
291 291 ('file1', 'a_c--826e8142e6ba'),
292 292 ])
293 293
294 294 @pytest.mark.xfail_backends("svn")
295 295 def test_compare_cherry_pick_commits_from_top(self, backend):
296 296 # repo1:
297 297 # commit0:
298 298 # commit1:
299 299 # repo1-fork- in which we will cherry pick bottom commits
300 300 # commit0:
301 301 # commit1:
302 302 # commit2:
303 303 # commit3: x
304 304 # commit4: x
305 305 # commit5: x
306 306
307 307 # make repo1, and commit1+commit2
308 308 repo1 = backend.create_repo()
309 309
310 310 # commit something !
311 311 commit0 = commit_change(
312 312 repo1.repo_name, filename='file1', content='line1\n',
313 313 message='commit1', vcs_type=backend.alias, parent=None,
314 314 newfile=True)
315 315 commit1 = commit_change(
316 316 repo1.repo_name, filename='file1', content='line1\nline2\n',
317 317 message='commit2', vcs_type=backend.alias, parent=commit0)
318 318
319 319 # fork this repo
320 320 backend.create_fork()
321 321
322 322 # now make commit3-6
323 323 commit2 = commit_change(
324 324 repo1.repo_name, filename='file1', content='line1\nline2\nline3\n',
325 325 message='commit3', vcs_type=backend.alias, parent=commit1)
326 326 commit3 = commit_change(
327 327 repo1.repo_name, filename='file1',
328 328 content='line1\nline2\nline3\nline4\n', message='commit4',
329 329 vcs_type=backend.alias, parent=commit2)
330 330 commit4 = commit_change(
331 331 repo1.repo_name, filename='file1',
332 332 content='line1\nline2\nline3\nline4\nline5\n', message='commit5',
333 333 vcs_type=backend.alias, parent=commit3)
334 334 commit5 = commit_change(
335 335 repo1.repo_name, filename='file1',
336 336 content='line1\nline2\nline3\nline4\nline5\nline6\n',
337 337 message='commit6', vcs_type=backend.alias, parent=commit4)
338 338
339 339 response = self.app.get(
340 340 url('compare_url',
341 341 repo_name=repo1.repo_name,
342 342 source_ref_type="rev",
343 343 # parent of commit3, not in source repo2
344 344 source_ref=commit2.raw_id,
345 345 target_ref_type="rev",
346 346 target_ref=commit5.raw_id,
347 347 merge='1',))
348 348
349 349 response.mustcontain('%s@%s' % (repo1.repo_name, commit2.short_id))
350 350 response.mustcontain('%s@%s' % (repo1.repo_name, commit5.short_id))
351 351
352 352 compare_page = ComparePage(response)
353 353 compare_page.contains_change_summary(1, 3, 0)
354 354 compare_page.contains_commits([commit3, commit4, commit5])
355 355
356 356 # files
357 357 compare_page.contains_file_links_and_anchors([
358 358 ('file1', 'a_c--826e8142e6ba'),
359 359 ])
360 360
361 361 @pytest.mark.xfail_backends("svn")
362 362 def test_compare_remote_branches(self, backend):
363 363 repo1 = backend.repo
364 364 repo2 = backend.create_fork()
365 365
366 366 commit_id1 = repo1.get_commit(commit_idx=3).raw_id
367 367 commit_id2 = repo1.get_commit(commit_idx=6).raw_id
368 368
369 369 response = self.app.get(
370 370 url('compare_url',
371 371 repo_name=repo1.repo_name,
372 372 source_ref_type="rev",
373 373 source_ref=commit_id1,
374 374 target_ref_type="rev",
375 375 target_ref=commit_id2,
376 376 target_repo=repo2.repo_name,
377 377 merge='1',))
378 378
379 379 response.mustcontain('%s@%s' % (repo1.repo_name, commit_id1))
380 380 response.mustcontain('%s@%s' % (repo2.repo_name, commit_id2))
381 381
382 382 compare_page = ComparePage(response)
383 383
384 384 # outgoing commits between those commits
385 385 compare_page.contains_commits(
386 386 [repo2.get_commit(commit_idx=x) for x in [4, 5, 6]])
387 387
388 388 # files
389 389 compare_page.contains_file_links_and_anchors([
390 390 ('vcs/backends/hg.py', 'a_c--9c390eb52cd6'),
391 391 ('vcs/backends/__init__.py', 'a_c--41b41c1f2796'),
392 392 ('vcs/backends/base.py', 'a_c--2f574d260608'),
393 393 ])
394 394
395 395 @pytest.mark.xfail_backends("svn")
396 396 def test_source_repo_new_commits_after_forking_simple_diff(self, backend):
397 397 repo1 = backend.create_repo()
398 398 r1_name = repo1.repo_name
399 399
400 400 commit0 = commit_change(
401 401 repo=r1_name, filename='file1',
402 402 content='line1', message='commit1', vcs_type=backend.alias,
403 403 newfile=True)
404 404 assert repo1.scm_instance().commit_ids == [commit0.raw_id]
405 405
406 406 # fork the repo1
407 407 repo2 = backend.create_fork()
408 408 assert repo2.scm_instance().commit_ids == [commit0.raw_id]
409 409
410 410 self.r2_id = repo2.repo_id
411 411 r2_name = repo2.repo_name
412 412
413 413 commit1 = commit_change(
414 414 repo=r2_name, filename='file1-fork',
415 415 content='file1-line1-from-fork', message='commit1-fork',
416 416 vcs_type=backend.alias, parent=repo2.scm_instance()[-1],
417 417 newfile=True)
418 418
419 419 commit2 = commit_change(
420 420 repo=r2_name, filename='file2-fork',
421 421 content='file2-line1-from-fork', message='commit2-fork',
422 422 vcs_type=backend.alias, parent=commit1,
423 423 newfile=True)
424 424
425 425 commit_change( # commit 3
426 426 repo=r2_name, filename='file3-fork',
427 427 content='file3-line1-from-fork', message='commit3-fork',
428 428 vcs_type=backend.alias, parent=commit2, newfile=True)
429 429
430 430 # compare !
431 431 commit_id1 = repo1.scm_instance().DEFAULT_BRANCH_NAME
432 432 commit_id2 = repo2.scm_instance().DEFAULT_BRANCH_NAME
433 433
434 434 response = self.app.get(
435 435 url('compare_url',
436 436 repo_name=r2_name,
437 437 source_ref_type="branch",
438 438 source_ref=commit_id1,
439 439 target_ref_type="branch",
440 440 target_ref=commit_id2,
441 441 target_repo=r1_name,
442 442 merge='1',))
443 443
444 444 response.mustcontain('%s@%s' % (r2_name, commit_id1))
445 445 response.mustcontain('%s@%s' % (r1_name, commit_id2))
446 446 response.mustcontain('No files')
447 447 response.mustcontain('No commits in this compare')
448 448
449 449 commit0 = commit_change(
450 450 repo=r1_name, filename='file2',
451 451 content='line1-added-after-fork', message='commit2-parent',
452 452 vcs_type=backend.alias, parent=None, newfile=True)
453 453
454 454 # compare !
455 455 response = self.app.get(
456 456 url('compare_url',
457 457 repo_name=r2_name,
458 458 source_ref_type="branch",
459 459 source_ref=commit_id1,
460 460 target_ref_type="branch",
461 461 target_ref=commit_id2,
462 462 target_repo=r1_name,
463 463 merge='1',))
464 464
465 465 response.mustcontain('%s@%s' % (r2_name, commit_id1))
466 466 response.mustcontain('%s@%s' % (r1_name, commit_id2))
467 467
468 468 response.mustcontain("""commit2-parent""")
469 469 response.mustcontain("""line1-added-after-fork""")
470 470 compare_page = ComparePage(response)
471 471 compare_page.contains_change_summary(1, 1, 0)
472 472
473 473 @pytest.mark.xfail_backends("svn")
474 474 def test_compare_commits(self, backend):
475 475 commit0 = backend.repo.get_commit(commit_idx=0)
476 476 commit1 = backend.repo.get_commit(commit_idx=1)
477 477
478 478 response = self.app.get(
479 479 url('compare_url',
480 480 repo_name=backend.repo_name,
481 481 source_ref_type="rev",
482 482 source_ref=commit0.raw_id,
483 483 target_ref_type="rev",
484 484 target_ref=commit1.raw_id,
485 485 merge='1',),
486 486 extra_environ={'HTTP_X_PARTIAL_XHR': '1'},)
487 487
488 488 # outgoing commits between those commits
489 489 compare_page = ComparePage(response)
490 490 compare_page.contains_commits(commits=[commit1], ancestors=[commit0])
491 491
492 def test_errors_when_comparing_unknown_repo(self, backend):
492 def test_errors_when_comparing_unknown_source_repo(self, backend):
493 repo = backend.repo
494 badrepo = 'badrepo'
495
496 response = self.app.get(
497 url('compare_url',
498 repo_name=badrepo,
499 source_ref_type="rev",
500 source_ref='tip',
501 target_ref_type="rev",
502 target_ref='tip',
503 target_repo=repo.repo_name,
504 merge='1',),
505 status=404)
506
507 def test_errors_when_comparing_unknown_target_repo(self, backend):
493 508 repo = backend.repo
494 509 badrepo = 'badrepo'
495 510
496 511 response = self.app.get(
497 512 url('compare_url',
498 513 repo_name=repo.repo_name,
499 514 source_ref_type="rev",
500 515 source_ref='tip',
501 516 target_ref_type="rev",
502 517 target_ref='tip',
503 518 target_repo=badrepo,
504 519 merge='1',),
505 520 status=302)
506 521 redirected = response.follow()
507 redirected.mustcontain('Could not find the other repo: %s' % badrepo)
522 redirected.mustcontain(
523 'Could not find the target repo: `{}`'.format(badrepo))
508 524
509 525 def test_compare_not_in_preview_mode(self, backend_stub):
510 526 commit0 = backend_stub.repo.get_commit(commit_idx=0)
511 527 commit1 = backend_stub.repo.get_commit(commit_idx=1)
512 528
513 529 response = self.app.get(url('compare_url',
514 530 repo_name=backend_stub.repo_name,
515 531 source_ref_type="rev",
516 532 source_ref=commit0.raw_id,
517 533 target_ref_type="rev",
518 534 target_ref=commit1.raw_id,
519 535 ),)
520 536
521 537 # outgoing commits between those commits
522 538 compare_page = ComparePage(response)
523 539 compare_page.swap_is_visible()
524 540 compare_page.target_source_are_enabled()
525 541
526 542 def test_compare_of_fork_with_largefiles(self, backend_hg, settings_util):
527 543 orig = backend_hg.create_repo(number_of_commits=1)
528 544 fork = backend_hg.create_fork()
529 545
530 546 settings_util.create_repo_rhodecode_ui(
531 547 orig, 'extensions', value='', key='largefiles', active=False)
532 548 settings_util.create_repo_rhodecode_ui(
533 549 fork, 'extensions', value='', key='largefiles', active=True)
534 550
535 551 compare_module = ('rhodecode.lib.vcs.backends.hg.repository.'
536 552 'MercurialRepository.compare')
537 553 with mock.patch(compare_module) as compare_mock:
538 554 compare_mock.side_effect = RepositoryRequirementError()
539 555
540 556 response = self.app.get(
541 557 url('compare_url',
542 558 repo_name=orig.repo_name,
543 559 action="compare",
544 560 source_ref_type="rev",
545 561 source_ref="tip",
546 562 target_ref_type="rev",
547 563 target_ref="tip",
548 564 merge='1',
549 565 target_repo=fork.repo_name),
550 566 status=302)
551 567
552 568 assert_session_flash(
553 569 response,
554 570 'Could not compare repos with different large file settings')
555 571
556 572
557 573 @pytest.mark.usefixtures("autologin_user")
558 574 class TestCompareControllerSvn(object):
559 575
560 576 def test_supports_references_with_path(self, app, backend_svn):
561 577 repo = backend_svn['svn-simple-layout']
562 578 commit_id = repo.get_commit(commit_idx=-1).raw_id
563 579 response = app.get(
564 580 url('compare_url',
565 581 repo_name=repo.repo_name,
566 582 source_ref_type="tag",
567 583 source_ref="%s@%s" % ('tags/v0.1', commit_id),
568 584 target_ref_type="tag",
569 585 target_ref="%s@%s" % ('tags/v0.2', commit_id),
570 586 merge='1',),
571 587 status=200)
572 588
573 589 # Expecting no commits, since both paths are at the same revision
574 590 response.mustcontain('No commits in this compare')
575 591
576 592 # Should find only one file changed when comparing those two tags
577 593 response.mustcontain('example.py')
578 594 compare_page = ComparePage(response)
579 595 compare_page.contains_change_summary(1, 5, 1)
580 596
581 597 def test_shows_commits_if_different_ids(self, app, backend_svn):
582 598 repo = backend_svn['svn-simple-layout']
583 599 source_id = repo.get_commit(commit_idx=-6).raw_id
584 600 target_id = repo.get_commit(commit_idx=-1).raw_id
585 601 response = app.get(
586 602 url('compare_url',
587 603 repo_name=repo.repo_name,
588 604 source_ref_type="tag",
589 605 source_ref="%s@%s" % ('tags/v0.1', source_id),
590 606 target_ref_type="tag",
591 607 target_ref="%s@%s" % ('tags/v0.2', target_id),
592 608 merge='1',),
593 609 status=200)
594 610
595 611 # It should show commits
596 612 assert 'No commits in this compare' not in response.body
597 613
598 614 # Should find only one file changed when comparing those two tags
599 615 response.mustcontain('example.py')
600 616 compare_page = ComparePage(response)
601 617 compare_page.contains_change_summary(1, 5, 1)
602 618
603 619
604 620 class ComparePage(AssertResponse):
605 621 """
606 622 Abstracts the page template from the tests
607 623 """
608 624
609 625 def contains_file_links_and_anchors(self, files):
610 626 doc = lxml.html.fromstring(self.response.body)
611 627 for filename, file_id in files:
612 628 self.contains_one_anchor(file_id)
613 629 diffblock = doc.cssselect('[data-f-path="%s"]' % filename)
614 630 assert len(diffblock) == 1
615 631 assert len(diffblock[0].cssselect('a[href="#%s"]' % file_id)) == 1
616 632
617 633 def contains_change_summary(self, files_changed, inserted, deleted):
618 634 template = (
619 635 "{files_changed} file{plural} changed: "
620 636 "{inserted} inserted, {deleted} deleted")
621 637 self.response.mustcontain(template.format(
622 638 files_changed=files_changed,
623 639 plural="s" if files_changed > 1 else "",
624 640 inserted=inserted,
625 641 deleted=deleted))
626 642
627 643 def contains_commits(self, commits, ancestors=None):
628 644 response = self.response
629 645
630 646 for commit in commits:
631 647 # Expecting to see the commit message in an element which
632 648 # has the ID "c-{commit.raw_id}"
633 649 self.element_contains('#c-' + commit.raw_id, commit.message)
634 650 self.contains_one_link(
635 651 'r%s:%s' % (commit.idx, commit.short_id),
636 652 self._commit_url(commit))
637 653 if ancestors:
638 654 response.mustcontain('Ancestor')
639 655 for ancestor in ancestors:
640 656 self.contains_one_link(
641 657 ancestor.short_id, self._commit_url(ancestor))
642 658
643 659 def _commit_url(self, commit):
644 660 return '/%s/changeset/%s' % (commit.repository.name, commit.raw_id)
645 661
646 662 def swap_is_hidden(self):
647 663 assert '<a id="btn-swap"' not in self.response.text
648 664
649 665 def swap_is_visible(self):
650 666 assert '<a id="btn-swap"' in self.response.text
651 667
652 668 def target_source_are_disabled(self):
653 669 response = self.response
654 670 response.mustcontain("var enable_fields = false;")
655 671 response.mustcontain('.select2("enable", enable_fields)')
656 672
657 673 def target_source_are_enabled(self):
658 674 response = self.response
659 675 response.mustcontain("var enable_fields = true;")
@@ -1,987 +1,987 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 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
23 23 import mock
24 24 import pytest
25 25
26 26 from rhodecode.controllers.files import FilesController
27 27 from rhodecode.lib import helpers as h
28 28 from rhodecode.lib.compat import OrderedDict
29 29 from rhodecode.lib.ext_json import json
30 30 from rhodecode.lib.vcs import nodes
31 31
32 32 from rhodecode.lib.vcs.conf import settings
33 33 from rhodecode.tests import (
34 34 url, assert_session_flash, assert_not_in_session_flash)
35 35 from rhodecode.tests.fixture import Fixture
36 36
37 37 fixture = Fixture()
38 38
39 39 NODE_HISTORY = {
40 40 'hg': json.loads(fixture.load_resource('hg_node_history_response.json')),
41 41 'git': json.loads(fixture.load_resource('git_node_history_response.json')),
42 42 'svn': json.loads(fixture.load_resource('svn_node_history_response.json')),
43 43 }
44 44
45 45
46 46
47 47 @pytest.mark.usefixtures("app")
48 48 class TestFilesController:
49 49
50 50 def test_index(self, backend):
51 51 response = self.app.get(url(
52 52 controller='files', action='index',
53 53 repo_name=backend.repo_name, revision='tip', f_path='/'))
54 54 commit = backend.repo.get_commit()
55 55
56 56 params = {
57 57 'repo_name': backend.repo_name,
58 58 'commit_id': commit.raw_id,
59 59 'date': commit.date
60 60 }
61 61 assert_dirs_in_response(response, ['docs', 'vcs'], params)
62 62 files = [
63 63 '.gitignore',
64 64 '.hgignore',
65 65 '.hgtags',
66 66 # TODO: missing in Git
67 67 # '.travis.yml',
68 68 'MANIFEST.in',
69 69 'README.rst',
70 70 # TODO: File is missing in svn repository
71 71 # 'run_test_and_report.sh',
72 72 'setup.cfg',
73 73 'setup.py',
74 74 'test_and_report.sh',
75 75 'tox.ini',
76 76 ]
77 77 assert_files_in_response(response, files, params)
78 78 assert_timeago_in_response(response, files, params)
79 79
80 80 def test_index_links_submodules_with_absolute_url(self, backend_hg):
81 81 repo = backend_hg['subrepos']
82 82 response = self.app.get(url(
83 83 controller='files', action='index',
84 84 repo_name=repo.repo_name, revision='tip', f_path='/'))
85 85 assert_response = response.assert_response()
86 86 assert_response.contains_one_link(
87 87 'absolute-path @ 000000000000', 'http://example.com/absolute-path')
88 88
89 89 def test_index_links_submodules_with_absolute_url_subpaths(
90 90 self, backend_hg):
91 91 repo = backend_hg['subrepos']
92 92 response = self.app.get(url(
93 93 controller='files', action='index',
94 94 repo_name=repo.repo_name, revision='tip', f_path='/'))
95 95 assert_response = response.assert_response()
96 96 assert_response.contains_one_link(
97 97 'subpaths-path @ 000000000000',
98 98 'http://sub-base.example.com/subpaths-path')
99 99
100 100 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
101 101 def test_files_menu(self, backend):
102 102 new_branch = "temp_branch_name"
103 103 commits = [
104 104 {'message': 'a'},
105 105 {'message': 'b', 'branch': new_branch}
106 106 ]
107 107 backend.create_repo(commits)
108 108
109 109 backend.repo.landing_rev = "branch:%s" % new_branch
110 110
111 111 # get response based on tip and not new revision
112 112 response = self.app.get(url(
113 113 controller='files', action='index',
114 114 repo_name=backend.repo_name, revision='tip', f_path='/'),
115 115 status=200)
116 116
117 117 # make sure Files menu url is not tip but new revision
118 118 landing_rev = backend.repo.landing_rev[1]
119 119 files_url = url('files_home', repo_name=backend.repo_name,
120 120 revision=landing_rev)
121 121
122 122 assert landing_rev != 'tip'
123 123 response.mustcontain('<li class="active"><a class="menulink" href="%s">' % files_url)
124 124
125 125 def test_index_commit(self, backend):
126 126 commit = backend.repo.get_commit(commit_idx=32)
127 127
128 128 response = self.app.get(url(
129 129 controller='files', action='index',
130 130 repo_name=backend.repo_name,
131 131 revision=commit.raw_id,
132 132 f_path='/')
133 133 )
134 134
135 135 dirs = ['docs', 'tests']
136 136 files = ['README.rst']
137 137 params = {
138 138 'repo_name': backend.repo_name,
139 139 'commit_id': commit.raw_id,
140 140 }
141 141 assert_dirs_in_response(response, dirs, params)
142 142 assert_files_in_response(response, files, params)
143 143
144 144 def test_index_different_branch(self, backend):
145 145 branches = dict(
146 146 hg=(150, ['git']),
147 147 # TODO: Git test repository does not contain other branches
148 148 git=(633, ['master']),
149 149 # TODO: Branch support in Subversion
150 150 svn=(150, [])
151 151 )
152 152 idx, branches = branches[backend.alias]
153 153 commit = backend.repo.get_commit(commit_idx=idx)
154 154 response = self.app.get(url(
155 155 controller='files', action='index',
156 156 repo_name=backend.repo_name,
157 157 revision=commit.raw_id,
158 158 f_path='/'))
159 159 assert_response = response.assert_response()
160 160 for branch in branches:
161 161 assert_response.element_contains('.tags .branchtag', branch)
162 162
163 163 def test_index_paging(self, backend):
164 164 repo = backend.repo
165 165 indexes = [73, 92, 109, 1, 0]
166 166 idx_map = [(rev, repo.get_commit(commit_idx=rev).raw_id)
167 167 for rev in indexes]
168 168
169 169 for idx in idx_map:
170 170 response = self.app.get(url(
171 171 controller='files', action='index',
172 172 repo_name=backend.repo_name,
173 173 revision=idx[1],
174 174 f_path='/'))
175 175
176 176 response.mustcontain("""r%s:%s""" % (idx[0], idx[1][:8]))
177 177
178 178 def test_file_source(self, backend):
179 179 commit = backend.repo.get_commit(commit_idx=167)
180 180 response = self.app.get(url(
181 181 controller='files', action='index',
182 182 repo_name=backend.repo_name,
183 183 revision=commit.raw_id,
184 184 f_path='vcs/nodes.py'))
185 185
186 186 msgbox = """<div class="commit right-content">%s</div>"""
187 187 response.mustcontain(msgbox % (commit.message, ))
188 188
189 189 assert_response = response.assert_response()
190 190 if commit.branch:
191 191 assert_response.element_contains('.tags.tags-main .branchtag', commit.branch)
192 192 if commit.tags:
193 193 for tag in commit.tags:
194 194 assert_response.element_contains('.tags.tags-main .tagtag', tag)
195 195
196 196 def test_file_source_history(self, backend):
197 197 response = self.app.get(
198 198 url(
199 199 controller='files', action='history',
200 200 repo_name=backend.repo_name,
201 201 revision='tip',
202 202 f_path='vcs/nodes.py'),
203 203 extra_environ={'HTTP_X_PARTIAL_XHR': '1'})
204 204 assert NODE_HISTORY[backend.alias] == json.loads(response.body)
205 205
206 206 def test_file_source_history_svn(self, backend_svn):
207 207 simple_repo = backend_svn['svn-simple-layout']
208 208 response = self.app.get(
209 209 url(
210 210 controller='files', action='history',
211 211 repo_name=simple_repo.repo_name,
212 212 revision='tip',
213 213 f_path='trunk/example.py'),
214 214 extra_environ={'HTTP_X_PARTIAL_XHR': '1'})
215 215
216 216 expected_data = json.loads(
217 217 fixture.load_resource('svn_node_history_branches.json'))
218 218 assert expected_data == response.json
219 219
220 220 def test_file_annotation_history(self, backend):
221 221 response = self.app.get(
222 222 url(
223 223 controller='files', action='history',
224 224 repo_name=backend.repo_name,
225 225 revision='tip',
226 226 f_path='vcs/nodes.py',
227 227 annotate=True),
228 228 extra_environ={'HTTP_X_PARTIAL_XHR': '1'})
229 229 assert NODE_HISTORY[backend.alias] == json.loads(response.body)
230 230
231 231 def test_file_annotation(self, backend):
232 232 response = self.app.get(url(
233 233 controller='files', action='index',
234 234 repo_name=backend.repo_name, revision='tip', f_path='vcs/nodes.py',
235 235 annotate=True))
236 236
237 237 expected_revisions = {
238 238 'hg': 'r356',
239 239 'git': 'r345',
240 240 'svn': 'r208',
241 241 }
242 242 response.mustcontain(expected_revisions[backend.alias])
243 243
244 244 def test_file_authors(self, backend):
245 245 response = self.app.get(url(
246 246 controller='files', action='authors',
247 247 repo_name=backend.repo_name,
248 248 revision='tip',
249 249 f_path='vcs/nodes.py',
250 250 annotate=True))
251 251
252 252 expected_authors = {
253 253 'hg': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
254 254 'git': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
255 255 'svn': ('marcin', 'lukasz'),
256 256 }
257 257
258 258 for author in expected_authors[backend.alias]:
259 259 response.mustcontain(author)
260 260
261 261 def test_tree_search_top_level(self, backend, xhr_header):
262 262 commit = backend.repo.get_commit(commit_idx=173)
263 263 response = self.app.get(
264 264 url('files_nodelist_home', repo_name=backend.repo_name,
265 265 revision=commit.raw_id, f_path='/'),
266 266 extra_environ=xhr_header)
267 267 assert 'nodes' in response.json
268 268 assert {'name': 'docs', 'type': 'dir'} in response.json['nodes']
269 269
270 270 def test_tree_search_at_path(self, backend, xhr_header):
271 271 commit = backend.repo.get_commit(commit_idx=173)
272 272 response = self.app.get(
273 273 url('files_nodelist_home', repo_name=backend.repo_name,
274 274 revision=commit.raw_id, f_path='/docs'),
275 275 extra_environ=xhr_header)
276 276 assert 'nodes' in response.json
277 277 nodes = response.json['nodes']
278 278 assert {'name': 'docs/api', 'type': 'dir'} in nodes
279 279 assert {'name': 'docs/index.rst', 'type': 'file'} in nodes
280 280
281 281 def test_tree_search_at_path_missing_xhr(self, backend):
282 282 self.app.get(
283 283 url('files_nodelist_home', repo_name=backend.repo_name,
284 284 revision='tip', f_path=''), status=400)
285 285
286 286 def test_tree_view_list(self, backend, xhr_header):
287 287 commit = backend.repo.get_commit(commit_idx=173)
288 288 response = self.app.get(
289 289 url('files_nodelist_home', repo_name=backend.repo_name,
290 290 f_path='/', revision=commit.raw_id),
291 291 extra_environ=xhr_header,
292 292 )
293 293 response.mustcontain("vcs/web/simplevcs/views/repository.py")
294 294
295 295 def test_tree_view_list_at_path(self, backend, xhr_header):
296 296 commit = backend.repo.get_commit(commit_idx=173)
297 297 response = self.app.get(
298 298 url('files_nodelist_home', repo_name=backend.repo_name,
299 299 f_path='/docs', revision=commit.raw_id),
300 300 extra_environ=xhr_header,
301 301 )
302 302 response.mustcontain("docs/index.rst")
303 303
304 304 def test_tree_view_list_missing_xhr(self, backend):
305 305 self.app.get(
306 306 url('files_nodelist_home', repo_name=backend.repo_name,
307 307 f_path='/', revision='tip'), status=400)
308 308
309 309 def test_nodetree_full_success(self, backend, xhr_header):
310 310 commit = backend.repo.get_commit(commit_idx=173)
311 311 response = self.app.get(
312 312 url('files_nodetree_full', repo_name=backend.repo_name,
313 313 f_path='/', commit_id=commit.raw_id),
314 314 extra_environ=xhr_header)
315 315
316 316 assert_response = response.assert_response()
317 317
318 318 for attr in ['data-commit-id', 'data-date', 'data-author']:
319 319 elements = assert_response.get_elements('[{}]'.format(attr))
320 320 assert len(elements) > 1
321 321
322 322 for element in elements:
323 323 assert element.get(attr)
324 324
325 325 def test_nodetree_full_if_file(self, backend, xhr_header):
326 326 commit = backend.repo.get_commit(commit_idx=173)
327 327 response = self.app.get(
328 328 url('files_nodetree_full', repo_name=backend.repo_name,
329 329 f_path='README.rst', commit_id=commit.raw_id),
330 330 extra_environ=xhr_header)
331 331 assert response.body == ''
332 332
333 333 def test_tree_metadata_list_missing_xhr(self, backend):
334 334 self.app.get(
335 335 url('files_nodetree_full', repo_name=backend.repo_name,
336 336 f_path='/', commit_id='tip'), status=400)
337 337
338 338 def test_access_empty_repo_redirect_to_summary_with_alert_write_perms(
339 339 self, app, backend_stub, autologin_regular_user, user_regular,
340 340 user_util):
341 341 repo = backend_stub.create_repo()
342 342 user_util.grant_user_permission_to_repo(
343 343 repo, user_regular, 'repository.write')
344 344 response = self.app.get(url(
345 345 controller='files', action='index',
346 346 repo_name=repo.repo_name, revision='tip', f_path='/'))
347 347 assert_session_flash(
348 348 response,
349 349 'There are no files yet. <a class="alert-link" '
350 350 'href="/%s/add/0/#edit">Click here to add a new file.</a>'
351 351 % (repo.repo_name))
352 352
353 353 def test_access_empty_repo_redirect_to_summary_with_alert_no_write_perms(
354 354 self, backend_stub, user_util):
355 355 repo = backend_stub.create_repo()
356 356 repo_file_url = url(
357 357 'files_add_home',
358 358 repo_name=repo.repo_name,
359 359 revision=0, f_path='', anchor='edit')
360 360 response = self.app.get(url(
361 361 controller='files', action='index',
362 362 repo_name=repo.repo_name, revision='tip', f_path='/'))
363 363 assert_not_in_session_flash(response, repo_file_url)
364 364
365 365
366 366 # TODO: johbo: Think about a better place for these tests. Either controller
367 367 # specific unit tests or we move down the whole logic further towards the vcs
368 368 # layer
369 369 class TestAdjustFilePathForSvn(object):
370 370 """SVN specific adjustments of node history in FileController."""
371 371
372 372 def test_returns_path_relative_to_matched_reference(self):
373 373 repo = self._repo(branches=['trunk'])
374 374 self.assert_file_adjustment('trunk/file', 'file', repo)
375 375
376 376 def test_does_not_modify_file_if_no_reference_matches(self):
377 377 repo = self._repo(branches=['trunk'])
378 378 self.assert_file_adjustment('notes/file', 'notes/file', repo)
379 379
380 380 def test_does_not_adjust_partial_directory_names(self):
381 381 repo = self._repo(branches=['trun'])
382 382 self.assert_file_adjustment('trunk/file', 'trunk/file', repo)
383 383
384 384 def test_is_robust_to_patterns_which_prefix_other_patterns(self):
385 385 repo = self._repo(branches=['trunk', 'trunk/new', 'trunk/old'])
386 386 self.assert_file_adjustment('trunk/new/file', 'file', repo)
387 387
388 388 def assert_file_adjustment(self, f_path, expected, repo):
389 389 controller = FilesController()
390 390 result = controller._adjust_file_path_for_svn(f_path, repo)
391 391 assert result == expected
392 392
393 393 def _repo(self, branches=None):
394 394 repo = mock.Mock()
395 395 repo.branches = OrderedDict((name, '0') for name in branches or [])
396 396 repo.tags = {}
397 397 return repo
398 398
399 399
400 400 @pytest.mark.usefixtures("app")
401 401 class TestRepositoryArchival(object):
402 402
403 403 def test_archival(self, backend):
404 404 backend.enable_downloads()
405 405 commit = backend.repo.get_commit(commit_idx=173)
406 406 for archive, info in settings.ARCHIVE_SPECS.items():
407 407 mime_type, arch_ext = info
408 408 short = commit.short_id + arch_ext
409 409 fname = commit.raw_id + arch_ext
410 410 filename = '%s-%s' % (backend.repo_name, short)
411 411 response = self.app.get(url(controller='files',
412 412 action='archivefile',
413 413 repo_name=backend.repo_name,
414 414 fname=fname))
415 415
416 416 assert response.status == '200 OK'
417 417 headers = {
418 418 'Pragma': 'no-cache',
419 419 'Cache-Control': 'no-cache',
420 420 'Content-Disposition': 'attachment; filename=%s' % filename,
421 421 'Content-Type': '%s; charset=utf-8' % mime_type,
422 422 }
423 423 if 'Set-Cookie' in response.response.headers:
424 424 del response.response.headers['Set-Cookie']
425 425 assert response.response.headers == headers
426 426
427 427 def test_archival_wrong_ext(self, backend):
428 428 backend.enable_downloads()
429 429 commit = backend.repo.get_commit(commit_idx=173)
430 430 for arch_ext in ['tar', 'rar', 'x', '..ax', '.zipz']:
431 431 fname = commit.raw_id + arch_ext
432 432
433 433 response = self.app.get(url(controller='files',
434 434 action='archivefile',
435 435 repo_name=backend.repo_name,
436 436 fname=fname))
437 437 response.mustcontain('Unknown archive type')
438 438
439 439 def test_archival_wrong_commit_id(self, backend):
440 440 backend.enable_downloads()
441 441 for commit_id in ['00x000000', 'tar', 'wrong', '@##$@$42413232',
442 442 '232dffcd']:
443 443 fname = '%s.zip' % commit_id
444 444
445 445 response = self.app.get(url(controller='files',
446 446 action='archivefile',
447 447 repo_name=backend.repo_name,
448 448 fname=fname))
449 449 response.mustcontain('Unknown revision')
450 450
451 451
452 452 @pytest.mark.usefixtures("app", "autologin_user")
453 453 class TestRawFileHandling(object):
454 454
455 455 def test_raw_file_ok(self, backend):
456 456 commit = backend.repo.get_commit(commit_idx=173)
457 457 response = self.app.get(url(controller='files', action='rawfile',
458 458 repo_name=backend.repo_name,
459 459 revision=commit.raw_id,
460 460 f_path='vcs/nodes.py'))
461 461
462 462 assert response.content_disposition == "attachment; filename=nodes.py"
463 463 assert response.content_type == "text/x-python"
464 464
465 465 def test_raw_file_wrong_cs(self, backend):
466 466 commit_id = u'ERRORce30c96924232dffcd24178a07ffeb5dfc'
467 467 f_path = 'vcs/nodes.py'
468 468
469 469 response = self.app.get(url(controller='files', action='rawfile',
470 470 repo_name=backend.repo_name,
471 471 revision=commit_id,
472 472 f_path=f_path), status=404)
473 473
474 474 msg = """No such commit exists for this repository"""
475 475 response.mustcontain(msg)
476 476
477 477 def test_raw_file_wrong_f_path(self, backend):
478 478 commit = backend.repo.get_commit(commit_idx=173)
479 479 f_path = 'vcs/ERRORnodes.py'
480 480 response = self.app.get(url(controller='files', action='rawfile',
481 481 repo_name=backend.repo_name,
482 482 revision=commit.raw_id,
483 483 f_path=f_path), status=404)
484 484
485 485 msg = (
486 486 "There is no file nor directory at the given path: "
487 "&#39;%s&#39; at commit %s" % (f_path, commit.short_id))
487 "`%s` at commit %s" % (f_path, commit.short_id))
488 488 response.mustcontain(msg)
489 489
490 490 def test_raw_ok(self, backend):
491 491 commit = backend.repo.get_commit(commit_idx=173)
492 492 response = self.app.get(url(controller='files', action='raw',
493 493 repo_name=backend.repo_name,
494 494 revision=commit.raw_id,
495 495 f_path='vcs/nodes.py'))
496 496
497 497 assert response.content_type == "text/plain"
498 498
499 499 def test_raw_wrong_cs(self, backend):
500 500 commit_id = u'ERRORcce30c96924232dffcd24178a07ffeb5dfc'
501 501 f_path = 'vcs/nodes.py'
502 502
503 503 response = self.app.get(url(controller='files', action='raw',
504 504 repo_name=backend.repo_name,
505 505 revision=commit_id,
506 506 f_path=f_path), status=404)
507 507
508 508 msg = """No such commit exists for this repository"""
509 509 response.mustcontain(msg)
510 510
511 511 def test_raw_wrong_f_path(self, backend):
512 512 commit = backend.repo.get_commit(commit_idx=173)
513 513 f_path = 'vcs/ERRORnodes.py'
514 514 response = self.app.get(url(controller='files', action='raw',
515 515 repo_name=backend.repo_name,
516 516 revision=commit.raw_id,
517 517 f_path=f_path), status=404)
518 518 msg = (
519 519 "There is no file nor directory at the given path: "
520 "&#39;%s&#39; at commit %s" % (f_path, commit.short_id))
520 "`%s` at commit %s" % (f_path, commit.short_id))
521 521 response.mustcontain(msg)
522 522
523 523 def test_raw_svg_should_not_be_rendered(self, backend):
524 524 backend.create_repo()
525 525 backend.ensure_file("xss.svg")
526 526 response = self.app.get(url(controller='files', action='raw',
527 527 repo_name=backend.repo_name,
528 528 revision='tip',
529 529 f_path='xss.svg'))
530 530
531 531 # If the content type is image/svg+xml then it allows to render HTML
532 532 # and malicious SVG.
533 533 assert response.content_type == "text/plain"
534 534
535 535
536 536 @pytest.mark.usefixtures("app")
537 537 class TestFilesDiff:
538 538
539 539 @pytest.mark.parametrize("diff", ['diff', 'download', 'raw'])
540 540 def test_file_full_diff(self, backend, diff):
541 541 commit1 = backend.repo.get_commit(commit_idx=-1)
542 542 commit2 = backend.repo.get_commit(commit_idx=-2)
543 543
544 544 response = self.app.get(
545 545 url(
546 546 controller='files',
547 547 action='diff',
548 548 repo_name=backend.repo_name,
549 549 f_path='README'),
550 550 params={
551 551 'diff1': commit2.raw_id,
552 552 'diff2': commit1.raw_id,
553 553 'fulldiff': '1',
554 554 'diff': diff,
555 555 })
556 556
557 557 if diff == 'diff':
558 558 # use redirect since this is OLD view redirecting to compare page
559 559 response = response.follow()
560 560
561 561 # It's a symlink to README.rst
562 562 response.mustcontain('README.rst')
563 563 response.mustcontain('No newline at end of file')
564 564
565 565 def test_file_binary_diff(self, backend):
566 566 commits = [
567 567 {'message': 'First commit'},
568 568 {'message': 'Commit with binary',
569 569 'added': [nodes.FileNode('file.bin', content='\0BINARY\0')]},
570 570 ]
571 571 repo = backend.create_repo(commits=commits)
572 572
573 573 response = self.app.get(
574 574 url(
575 575 controller='files',
576 576 action='diff',
577 577 repo_name=backend.repo_name,
578 578 f_path='file.bin'),
579 579 params={
580 580 'diff1': repo.get_commit(commit_idx=0).raw_id,
581 581 'diff2': repo.get_commit(commit_idx=1).raw_id,
582 582 'fulldiff': '1',
583 583 'diff': 'diff',
584 584 })
585 585 # use redirect since this is OLD view redirecting to compare page
586 586 response = response.follow()
587 587 response.mustcontain('Expand 1 commit')
588 588 response.mustcontain('1 file changed: 0 inserted, 0 deleted')
589 589
590 590 if backend.alias == 'svn':
591 591 response.mustcontain('new file 10644')
592 592 # TODO(marcink): SVN doesn't yet detect binary changes
593 593 else:
594 594 response.mustcontain('new file 100644')
595 595 response.mustcontain('binary diff hidden')
596 596
597 597 def test_diff_2way(self, backend):
598 598 commit1 = backend.repo.get_commit(commit_idx=-1)
599 599 commit2 = backend.repo.get_commit(commit_idx=-2)
600 600 response = self.app.get(
601 601 url(
602 602 controller='files',
603 603 action='diff_2way',
604 604 repo_name=backend.repo_name,
605 605 f_path='README'),
606 606 params={
607 607 'diff1': commit2.raw_id,
608 608 'diff2': commit1.raw_id,
609 609 })
610 610 # use redirect since this is OLD view redirecting to compare page
611 611 response = response.follow()
612 612
613 613 # It's a symlink to README.rst
614 614 response.mustcontain('README.rst')
615 615 response.mustcontain('No newline at end of file')
616 616
617 617 def test_requires_one_commit_id(self, backend, autologin_user):
618 618 response = self.app.get(
619 619 url(
620 620 controller='files',
621 621 action='diff',
622 622 repo_name=backend.repo_name,
623 623 f_path='README.rst'),
624 624 status=400)
625 625 response.mustcontain(
626 626 'Need query parameter', 'diff1', 'diff2', 'to generate a diff.')
627 627
628 628 def test_returns_no_files_if_file_does_not_exist(self, vcsbackend):
629 629 repo = vcsbackend.repo
630 630 response = self.app.get(
631 631 url(
632 632 controller='files',
633 633 action='diff',
634 634 repo_name=repo.name,
635 635 f_path='does-not-exist-in-any-commit',
636 636 diff1=repo[0].raw_id,
637 637 diff2=repo[1].raw_id),)
638 638
639 639 response = response.follow()
640 640 response.mustcontain('No files')
641 641
642 642 def test_returns_redirect_if_file_not_changed(self, backend):
643 643 commit = backend.repo.get_commit(commit_idx=-1)
644 644 f_path = 'README'
645 645 response = self.app.get(
646 646 url(
647 647 controller='files',
648 648 action='diff_2way',
649 649 repo_name=backend.repo_name,
650 650 f_path=f_path,
651 651 diff1=commit.raw_id,
652 652 diff2=commit.raw_id,
653 653 ),
654 654 )
655 655 response = response.follow()
656 656 response.mustcontain('No files')
657 657 response.mustcontain('No commits in this compare')
658 658
659 659 def test_supports_diff_to_different_path_svn(self, backend_svn):
660 660 #TODO: check this case
661 661 return
662 662
663 663 repo = backend_svn['svn-simple-layout'].scm_instance()
664 664 commit_id_1 = '24'
665 665 commit_id_2 = '26'
666 666
667 667
668 668 print( url(
669 669 controller='files',
670 670 action='diff',
671 671 repo_name=repo.name,
672 672 f_path='trunk/example.py',
673 673 diff1='tags/v0.2/example.py@' + commit_id_1,
674 674 diff2=commit_id_2))
675 675
676 676 response = self.app.get(
677 677 url(
678 678 controller='files',
679 679 action='diff',
680 680 repo_name=repo.name,
681 681 f_path='trunk/example.py',
682 682 diff1='tags/v0.2/example.py@' + commit_id_1,
683 683 diff2=commit_id_2))
684 684
685 685 response = response.follow()
686 686 response.mustcontain(
687 687 # diff contains this
688 688 "Will print out a useful message on invocation.")
689 689
690 690 # Note: Expecting that we indicate the user what's being compared
691 691 response.mustcontain("trunk/example.py")
692 692 response.mustcontain("tags/v0.2/example.py")
693 693
694 694 def test_show_rev_redirects_to_svn_path(self, backend_svn):
695 695 #TODO: check this case
696 696 return
697 697
698 698 repo = backend_svn['svn-simple-layout'].scm_instance()
699 699 commit_id = repo[-1].raw_id
700 700 response = self.app.get(
701 701 url(
702 702 controller='files',
703 703 action='diff',
704 704 repo_name=repo.name,
705 705 f_path='trunk/example.py',
706 706 diff1='branches/argparse/example.py@' + commit_id,
707 707 diff2=commit_id),
708 708 params={'show_rev': 'Show at Revision'},
709 709 status=302)
710 710 assert response.headers['Location'].endswith(
711 711 'svn-svn-simple-layout/files/26/branches/argparse/example.py')
712 712
713 713 def test_show_rev_and_annotate_redirects_to_svn_path(self, backend_svn):
714 714 #TODO: check this case
715 715 return
716 716
717 717 repo = backend_svn['svn-simple-layout'].scm_instance()
718 718 commit_id = repo[-1].raw_id
719 719 response = self.app.get(
720 720 url(
721 721 controller='files',
722 722 action='diff',
723 723 repo_name=repo.name,
724 724 f_path='trunk/example.py',
725 725 diff1='branches/argparse/example.py@' + commit_id,
726 726 diff2=commit_id),
727 727 params={
728 728 'show_rev': 'Show at Revision',
729 729 'annotate': 'true',
730 730 },
731 731 status=302)
732 732 assert response.headers['Location'].endswith(
733 733 'svn-svn-simple-layout/annotate/26/branches/argparse/example.py')
734 734
735 735
736 736 @pytest.mark.usefixtures("app", "autologin_user")
737 737 class TestChangingFiles:
738 738
739 739 def test_add_file_view(self, backend):
740 740 self.app.get(url(
741 741 'files_add_home',
742 742 repo_name=backend.repo_name,
743 743 revision='tip', f_path='/'))
744 744
745 745 @pytest.mark.xfail_backends("svn", reason="Depends on online editing")
746 746 def test_add_file_into_repo_missing_content(self, backend, csrf_token):
747 747 repo = backend.create_repo()
748 748 filename = 'init.py'
749 749 response = self.app.post(
750 750 url(
751 751 'files_add',
752 752 repo_name=repo.repo_name,
753 753 revision='tip', f_path='/'),
754 754 params={
755 755 'content': "",
756 756 'filename': filename,
757 757 'location': "",
758 758 'csrf_token': csrf_token,
759 759 },
760 760 status=302)
761 761 assert_session_flash(response,
762 762 'Successfully committed new file `{}`'.format(os.path.join(filename)))
763 763
764 764 def test_add_file_into_repo_missing_filename(self, backend, csrf_token):
765 765 response = self.app.post(
766 766 url(
767 767 'files_add',
768 768 repo_name=backend.repo_name,
769 769 revision='tip', f_path='/'),
770 770 params={
771 771 'content': "foo",
772 772 'csrf_token': csrf_token,
773 773 },
774 774 status=302)
775 775
776 776 assert_session_flash(response, 'No filename')
777 777
778 778 def test_add_file_into_repo_errors_and_no_commits(
779 779 self, backend, csrf_token):
780 780 repo = backend.create_repo()
781 781 # Create a file with no filename, it will display an error but
782 782 # the repo has no commits yet
783 783 response = self.app.post(
784 784 url(
785 785 'files_add',
786 786 repo_name=repo.repo_name,
787 787 revision='tip', f_path='/'),
788 788 params={
789 789 'content': "foo",
790 790 'csrf_token': csrf_token,
791 791 },
792 792 status=302)
793 793
794 794 assert_session_flash(response, 'No filename')
795 795
796 796 # Not allowed, redirect to the summary
797 797 redirected = response.follow()
798 798 summary_url = h.route_path('repo_summary', repo_name=repo.repo_name)
799 799
800 800 # As there are no commits, displays the summary page with the error of
801 801 # creating a file with no filename
802 802
803 803 assert redirected.request.path == summary_url
804 804
805 805 @pytest.mark.parametrize("location, filename", [
806 806 ('/abs', 'foo'),
807 807 ('../rel', 'foo'),
808 808 ('file/../foo', 'foo'),
809 809 ])
810 810 def test_add_file_into_repo_bad_filenames(
811 811 self, location, filename, backend, csrf_token):
812 812 response = self.app.post(
813 813 url(
814 814 'files_add',
815 815 repo_name=backend.repo_name,
816 816 revision='tip', f_path='/'),
817 817 params={
818 818 'content': "foo",
819 819 'filename': filename,
820 820 'location': location,
821 821 'csrf_token': csrf_token,
822 822 },
823 823 status=302)
824 824
825 825 assert_session_flash(
826 826 response,
827 827 'The location specified must be a relative path and must not '
828 828 'contain .. in the path')
829 829
830 830 @pytest.mark.parametrize("cnt, location, filename", [
831 831 (1, '', 'foo.txt'),
832 832 (2, 'dir', 'foo.rst'),
833 833 (3, 'rel/dir', 'foo.bar'),
834 834 ])
835 835 def test_add_file_into_repo(self, cnt, location, filename, backend,
836 836 csrf_token):
837 837 repo = backend.create_repo()
838 838 response = self.app.post(
839 839 url(
840 840 'files_add',
841 841 repo_name=repo.repo_name,
842 842 revision='tip', f_path='/'),
843 843 params={
844 844 'content': "foo",
845 845 'filename': filename,
846 846 'location': location,
847 847 'csrf_token': csrf_token,
848 848 },
849 849 status=302)
850 850 assert_session_flash(response,
851 851 'Successfully committed new file `{}`'.format(
852 852 os.path.join(location, filename)))
853 853
854 854 def test_edit_file_view(self, backend):
855 855 response = self.app.get(
856 856 url(
857 857 'files_edit_home',
858 858 repo_name=backend.repo_name,
859 859 revision=backend.default_head_id,
860 860 f_path='vcs/nodes.py'),
861 861 status=200)
862 862 response.mustcontain("Module holding everything related to vcs nodes.")
863 863
864 864 def test_edit_file_view_not_on_branch(self, backend):
865 865 repo = backend.create_repo()
866 866 backend.ensure_file("vcs/nodes.py")
867 867
868 868 response = self.app.get(
869 869 url(
870 870 'files_edit_home',
871 871 repo_name=repo.repo_name,
872 872 revision='tip', f_path='vcs/nodes.py'),
873 873 status=302)
874 874 assert_session_flash(
875 875 response,
876 876 'You can only edit files with revision being a valid branch')
877 877
878 878 def test_edit_file_view_commit_changes(self, backend, csrf_token):
879 879 repo = backend.create_repo()
880 880 backend.ensure_file("vcs/nodes.py", content="print 'hello'")
881 881
882 882 response = self.app.post(
883 883 url(
884 884 'files_edit',
885 885 repo_name=repo.repo_name,
886 886 revision=backend.default_head_id,
887 887 f_path='vcs/nodes.py'),
888 888 params={
889 889 'content': "print 'hello world'",
890 890 'message': 'I committed',
891 891 'filename': "vcs/nodes.py",
892 892 'csrf_token': csrf_token,
893 893 },
894 894 status=302)
895 895 assert_session_flash(
896 896 response, 'Successfully committed changes to file `vcs/nodes.py`')
897 897 tip = repo.get_commit(commit_idx=-1)
898 898 assert tip.message == 'I committed'
899 899
900 900 def test_edit_file_view_commit_changes_default_message(self, backend,
901 901 csrf_token):
902 902 repo = backend.create_repo()
903 903 backend.ensure_file("vcs/nodes.py", content="print 'hello'")
904 904
905 905 commit_id = (
906 906 backend.default_branch_name or
907 907 backend.repo.scm_instance().commit_ids[-1])
908 908
909 909 response = self.app.post(
910 910 url(
911 911 'files_edit',
912 912 repo_name=repo.repo_name,
913 913 revision=commit_id,
914 914 f_path='vcs/nodes.py'),
915 915 params={
916 916 'content': "print 'hello world'",
917 917 'message': '',
918 918 'filename': "vcs/nodes.py",
919 919 'csrf_token': csrf_token,
920 920 },
921 921 status=302)
922 922 assert_session_flash(
923 923 response, 'Successfully committed changes to file `vcs/nodes.py`')
924 924 tip = repo.get_commit(commit_idx=-1)
925 925 assert tip.message == 'Edited file vcs/nodes.py via RhodeCode Enterprise'
926 926
927 927 def test_delete_file_view(self, backend):
928 928 self.app.get(url(
929 929 'files_delete_home',
930 930 repo_name=backend.repo_name,
931 931 revision='tip', f_path='vcs/nodes.py'))
932 932
933 933 def test_delete_file_view_not_on_branch(self, backend):
934 934 repo = backend.create_repo()
935 935 backend.ensure_file('vcs/nodes.py')
936 936
937 937 response = self.app.get(
938 938 url(
939 939 'files_delete_home',
940 940 repo_name=repo.repo_name,
941 941 revision='tip', f_path='vcs/nodes.py'),
942 942 status=302)
943 943 assert_session_flash(
944 944 response,
945 945 'You can only delete files with revision being a valid branch')
946 946
947 947 def test_delete_file_view_commit_changes(self, backend, csrf_token):
948 948 repo = backend.create_repo()
949 949 backend.ensure_file("vcs/nodes.py")
950 950
951 951 response = self.app.post(
952 952 url(
953 953 'files_delete_home',
954 954 repo_name=repo.repo_name,
955 955 revision=backend.default_head_id,
956 956 f_path='vcs/nodes.py'),
957 957 params={
958 958 'message': 'i commited',
959 959 'csrf_token': csrf_token,
960 960 },
961 961 status=302)
962 962 assert_session_flash(
963 963 response, 'Successfully deleted file `vcs/nodes.py`')
964 964
965 965
966 966 def assert_files_in_response(response, files, params):
967 967 template = (
968 968 'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"')
969 969 _assert_items_in_response(response, files, template, params)
970 970
971 971
972 972 def assert_dirs_in_response(response, dirs, params):
973 973 template = (
974 974 'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"')
975 975 _assert_items_in_response(response, dirs, template, params)
976 976
977 977
978 978 def _assert_items_in_response(response, items, template, params):
979 979 for item in items:
980 980 item_params = {'name': item}
981 981 item_params.update(params)
982 982 response.mustcontain(template % item_params)
983 983
984 984
985 985 def assert_timeago_in_response(response, items, params):
986 986 for item in items:
987 987 response.mustcontain(h.age_component(params['date']))
General Comments 0
You need to be logged in to leave comments. Login now