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