##// END OF EJS Templates
tests: fixes for commit pages changes.
marcink -
r3885:a66c5e67 default
parent child Browse files
Show More
@@ -1,320 +1,320 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2019 RhodeCode GmbH
3 # Copyright (C) 2010-2019 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 pytest
21 import pytest
22
22
23 from rhodecode.tests import TestController
23 from rhodecode.tests import TestController
24
24
25 from rhodecode.model.db import (
25 from rhodecode.model.db import (
26 ChangesetComment, Notification, UserNotification)
26 ChangesetComment, Notification, UserNotification)
27 from rhodecode.model.meta import Session
27 from rhodecode.model.meta import Session
28 from rhodecode.lib import helpers as h
28 from rhodecode.lib import helpers as h
29
29
30
30
31 def route_path(name, params=None, **kwargs):
31 def route_path(name, params=None, **kwargs):
32 import urllib
32 import urllib
33
33
34 base_url = {
34 base_url = {
35 'repo_commit': '/{repo_name}/changeset/{commit_id}',
35 'repo_commit': '/{repo_name}/changeset/{commit_id}',
36 'repo_commit_comment_create': '/{repo_name}/changeset/{commit_id}/comment/create',
36 'repo_commit_comment_create': '/{repo_name}/changeset/{commit_id}/comment/create',
37 'repo_commit_comment_preview': '/{repo_name}/changeset/{commit_id}/comment/preview',
37 'repo_commit_comment_preview': '/{repo_name}/changeset/{commit_id}/comment/preview',
38 'repo_commit_comment_delete': '/{repo_name}/changeset/{commit_id}/comment/{comment_id}/delete',
38 'repo_commit_comment_delete': '/{repo_name}/changeset/{commit_id}/comment/{comment_id}/delete',
39 }[name].format(**kwargs)
39 }[name].format(**kwargs)
40
40
41 if params:
41 if params:
42 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
42 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
43 return base_url
43 return base_url
44
44
45
45
46 @pytest.mark.backends("git", "hg", "svn")
46 @pytest.mark.backends("git", "hg", "svn")
47 class TestRepoCommitCommentsView(TestController):
47 class TestRepoCommitCommentsView(TestController):
48
48
49 @pytest.fixture(autouse=True)
49 @pytest.fixture(autouse=True)
50 def prepare(self, request, baseapp):
50 def prepare(self, request, baseapp):
51 for x in ChangesetComment.query().all():
51 for x in ChangesetComment.query().all():
52 Session().delete(x)
52 Session().delete(x)
53 Session().commit()
53 Session().commit()
54
54
55 for x in Notification.query().all():
55 for x in Notification.query().all():
56 Session().delete(x)
56 Session().delete(x)
57 Session().commit()
57 Session().commit()
58
58
59 request.addfinalizer(self.cleanup)
59 request.addfinalizer(self.cleanup)
60
60
61 def cleanup(self):
61 def cleanup(self):
62 for x in ChangesetComment.query().all():
62 for x in ChangesetComment.query().all():
63 Session().delete(x)
63 Session().delete(x)
64 Session().commit()
64 Session().commit()
65
65
66 for x in Notification.query().all():
66 for x in Notification.query().all():
67 Session().delete(x)
67 Session().delete(x)
68 Session().commit()
68 Session().commit()
69
69
70 @pytest.mark.parametrize('comment_type', ChangesetComment.COMMENT_TYPES)
70 @pytest.mark.parametrize('comment_type', ChangesetComment.COMMENT_TYPES)
71 def test_create(self, comment_type, backend):
71 def test_create(self, comment_type, backend):
72 self.log_user()
72 self.log_user()
73 commit = backend.repo.get_commit('300')
73 commit = backend.repo.get_commit('300')
74 commit_id = commit.raw_id
74 commit_id = commit.raw_id
75 text = u'CommentOnCommit'
75 text = u'CommentOnCommit'
76
76
77 params = {'text': text, 'csrf_token': self.csrf_token,
77 params = {'text': text, 'csrf_token': self.csrf_token,
78 'comment_type': comment_type}
78 'comment_type': comment_type}
79 self.app.post(
79 self.app.post(
80 route_path('repo_commit_comment_create',
80 route_path('repo_commit_comment_create',
81 repo_name=backend.repo_name, commit_id=commit_id),
81 repo_name=backend.repo_name, commit_id=commit_id),
82 params=params)
82 params=params)
83
83
84 response = self.app.get(
84 response = self.app.get(
85 route_path('repo_commit',
85 route_path('repo_commit',
86 repo_name=backend.repo_name, commit_id=commit_id))
86 repo_name=backend.repo_name, commit_id=commit_id))
87
87
88 # test DB
88 # test DB
89 assert ChangesetComment.query().count() == 1
89 assert ChangesetComment.query().count() == 1
90 assert_comment_links(response, ChangesetComment.query().count(), 0)
90 assert_comment_links(response, ChangesetComment.query().count(), 0)
91
91
92 assert Notification.query().count() == 1
92 assert Notification.query().count() == 1
93 assert ChangesetComment.query().count() == 1
93 assert ChangesetComment.query().count() == 1
94
94
95 notification = Notification.query().all()[0]
95 notification = Notification.query().all()[0]
96
96
97 comment_id = ChangesetComment.query().first().comment_id
97 comment_id = ChangesetComment.query().first().comment_id
98 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
98 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
99
99
100 author = notification.created_by_user.username_and_name
100 author = notification.created_by_user.username_and_name
101 sbj = '{0} left a {1} on commit `{2}` in the {3} repository'.format(
101 sbj = '{0} left a {1} on commit `{2}` in the {3} repository'.format(
102 author, comment_type, h.show_id(commit), backend.repo_name)
102 author, comment_type, h.show_id(commit), backend.repo_name)
103 assert sbj == notification.subject
103 assert sbj == notification.subject
104
104
105 lnk = (u'/{0}/changeset/{1}#comment-{2}'.format(
105 lnk = (u'/{0}/changeset/{1}#comment-{2}'.format(
106 backend.repo_name, commit_id, comment_id))
106 backend.repo_name, commit_id, comment_id))
107 assert lnk in notification.body
107 assert lnk in notification.body
108
108
109 @pytest.mark.parametrize('comment_type', ChangesetComment.COMMENT_TYPES)
109 @pytest.mark.parametrize('comment_type', ChangesetComment.COMMENT_TYPES)
110 def test_create_inline(self, comment_type, backend):
110 def test_create_inline(self, comment_type, backend):
111 self.log_user()
111 self.log_user()
112 commit = backend.repo.get_commit('300')
112 commit = backend.repo.get_commit('300')
113 commit_id = commit.raw_id
113 commit_id = commit.raw_id
114 text = u'CommentOnCommit'
114 text = u'CommentOnCommit'
115 f_path = 'vcs/web/simplevcs/views/repository.py'
115 f_path = 'vcs/web/simplevcs/views/repository.py'
116 line = 'n1'
116 line = 'n1'
117
117
118 params = {'text': text, 'f_path': f_path, 'line': line,
118 params = {'text': text, 'f_path': f_path, 'line': line,
119 'comment_type': comment_type,
119 'comment_type': comment_type,
120 'csrf_token': self.csrf_token}
120 'csrf_token': self.csrf_token}
121
121
122 self.app.post(
122 self.app.post(
123 route_path('repo_commit_comment_create',
123 route_path('repo_commit_comment_create',
124 repo_name=backend.repo_name, commit_id=commit_id),
124 repo_name=backend.repo_name, commit_id=commit_id),
125 params=params)
125 params=params)
126
126
127 response = self.app.get(
127 response = self.app.get(
128 route_path('repo_commit',
128 route_path('repo_commit',
129 repo_name=backend.repo_name, commit_id=commit_id))
129 repo_name=backend.repo_name, commit_id=commit_id))
130
130
131 # test DB
131 # test DB
132 assert ChangesetComment.query().count() == 1
132 assert ChangesetComment.query().count() == 1
133 assert_comment_links(response, 0, ChangesetComment.query().count())
133 assert_comment_links(response, 0, ChangesetComment.query().count())
134
134
135 if backend.alias == 'svn':
135 if backend.alias == 'svn':
136 response.mustcontain(
136 response.mustcontain(
137 '''data-f-path="vcs/commands/summary.py" '''
137 '''data-f-path="vcs/commands/summary.py" '''
138 '''data-anchor-id="c-300-ad05457a43f8"'''
138 '''data-anchor-id="c-300-ad05457a43f8"'''
139 )
139 )
140 if backend.alias == 'git':
140 if backend.alias == 'git':
141 response.mustcontain(
141 response.mustcontain(
142 '''data-f-path="vcs/backends/hg.py" '''
142 '''data-f-path="vcs/backends/hg.py" '''
143 '''data-anchor-id="c-883e775e89ea-9c390eb52cd6"'''
143 '''data-anchor-id="c-883e775e89ea-9c390eb52cd6"'''
144 )
144 )
145
145
146 if backend.alias == 'hg':
146 if backend.alias == 'hg':
147 response.mustcontain(
147 response.mustcontain(
148 '''data-f-path="vcs/backends/hg.py" '''
148 '''data-f-path="vcs/backends/hg.py" '''
149 '''data-anchor-id="c-e58d85a3973b-9c390eb52cd6"'''
149 '''data-anchor-id="c-e58d85a3973b-9c390eb52cd6"'''
150 )
150 )
151
151
152 assert Notification.query().count() == 1
152 assert Notification.query().count() == 1
153 assert ChangesetComment.query().count() == 1
153 assert ChangesetComment.query().count() == 1
154
154
155 notification = Notification.query().all()[0]
155 notification = Notification.query().all()[0]
156 comment = ChangesetComment.query().first()
156 comment = ChangesetComment.query().first()
157 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
157 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
158
158
159 assert comment.revision == commit_id
159 assert comment.revision == commit_id
160
160
161 author = notification.created_by_user.username_and_name
161 author = notification.created_by_user.username_and_name
162 sbj = '{0} left a {1} on file `{2}` in commit `{3}` in the {4} repository'.format(
162 sbj = '{0} left a {1} on file `{2}` in commit `{3}` in the {4} repository'.format(
163 author, comment_type, f_path, h.show_id(commit), backend.repo_name)
163 author, comment_type, f_path, h.show_id(commit), backend.repo_name)
164
164
165 assert sbj == notification.subject
165 assert sbj == notification.subject
166
166
167 lnk = (u'/{0}/changeset/{1}#comment-{2}'.format(
167 lnk = (u'/{0}/changeset/{1}#comment-{2}'.format(
168 backend.repo_name, commit_id, comment.comment_id))
168 backend.repo_name, commit_id, comment.comment_id))
169 assert lnk in notification.body
169 assert lnk in notification.body
170 assert 'on line n1' in notification.body
170 assert 'on line n1' in notification.body
171
171
172 def test_create_with_mention(self, backend):
172 def test_create_with_mention(self, backend):
173 self.log_user()
173 self.log_user()
174
174
175 commit_id = backend.repo.get_commit('300').raw_id
175 commit_id = backend.repo.get_commit('300').raw_id
176 text = u'@test_regular check CommentOnCommit'
176 text = u'@test_regular check CommentOnCommit'
177
177
178 params = {'text': text, 'csrf_token': self.csrf_token}
178 params = {'text': text, 'csrf_token': self.csrf_token}
179 self.app.post(
179 self.app.post(
180 route_path('repo_commit_comment_create',
180 route_path('repo_commit_comment_create',
181 repo_name=backend.repo_name, commit_id=commit_id),
181 repo_name=backend.repo_name, commit_id=commit_id),
182 params=params)
182 params=params)
183
183
184 response = self.app.get(
184 response = self.app.get(
185 route_path('repo_commit',
185 route_path('repo_commit',
186 repo_name=backend.repo_name, commit_id=commit_id))
186 repo_name=backend.repo_name, commit_id=commit_id))
187 # test DB
187 # test DB
188 assert ChangesetComment.query().count() == 1
188 assert ChangesetComment.query().count() == 1
189 assert_comment_links(response, ChangesetComment.query().count(), 0)
189 assert_comment_links(response, ChangesetComment.query().count(), 0)
190
190
191 notification = Notification.query().one()
191 notification = Notification.query().one()
192
192
193 assert len(notification.recipients) == 2
193 assert len(notification.recipients) == 2
194 users = [x.username for x in notification.recipients]
194 users = [x.username for x in notification.recipients]
195
195
196 # test_regular gets notification by @mention
196 # test_regular gets notification by @mention
197 assert sorted(users) == [u'test_admin', u'test_regular']
197 assert sorted(users) == [u'test_admin', u'test_regular']
198
198
199 def test_create_with_status_change(self, backend):
199 def test_create_with_status_change(self, backend):
200 self.log_user()
200 self.log_user()
201 commit = backend.repo.get_commit('300')
201 commit = backend.repo.get_commit('300')
202 commit_id = commit.raw_id
202 commit_id = commit.raw_id
203 text = u'CommentOnCommit'
203 text = u'CommentOnCommit'
204 f_path = 'vcs/web/simplevcs/views/repository.py'
204 f_path = 'vcs/web/simplevcs/views/repository.py'
205 line = 'n1'
205 line = 'n1'
206
206
207 params = {'text': text, 'changeset_status': 'approved',
207 params = {'text': text, 'changeset_status': 'approved',
208 'csrf_token': self.csrf_token}
208 'csrf_token': self.csrf_token}
209
209
210 self.app.post(
210 self.app.post(
211 route_path(
211 route_path(
212 'repo_commit_comment_create',
212 'repo_commit_comment_create',
213 repo_name=backend.repo_name, commit_id=commit_id),
213 repo_name=backend.repo_name, commit_id=commit_id),
214 params=params)
214 params=params)
215
215
216 response = self.app.get(
216 response = self.app.get(
217 route_path('repo_commit',
217 route_path('repo_commit',
218 repo_name=backend.repo_name, commit_id=commit_id))
218 repo_name=backend.repo_name, commit_id=commit_id))
219
219
220 # test DB
220 # test DB
221 assert ChangesetComment.query().count() == 1
221 assert ChangesetComment.query().count() == 1
222 assert_comment_links(response, ChangesetComment.query().count(), 0)
222 assert_comment_links(response, ChangesetComment.query().count(), 0)
223
223
224 assert Notification.query().count() == 1
224 assert Notification.query().count() == 1
225 assert ChangesetComment.query().count() == 1
225 assert ChangesetComment.query().count() == 1
226
226
227 notification = Notification.query().all()[0]
227 notification = Notification.query().all()[0]
228
228
229 comment_id = ChangesetComment.query().first().comment_id
229 comment_id = ChangesetComment.query().first().comment_id
230 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
230 assert notification.type_ == Notification.TYPE_CHANGESET_COMMENT
231
231
232 author = notification.created_by_user.username_and_name
232 author = notification.created_by_user.username_and_name
233 sbj = '[status: Approved] {0} left a note on commit `{1}` in the {2} repository'.format(
233 sbj = '[status: Approved] {0} left a note on commit `{1}` in the {2} repository'.format(
234 author, h.show_id(commit), backend.repo_name)
234 author, h.show_id(commit), backend.repo_name)
235 assert sbj == notification.subject
235 assert sbj == notification.subject
236
236
237 lnk = (u'/{0}/changeset/{1}#comment-{2}'.format(
237 lnk = (u'/{0}/changeset/{1}#comment-{2}'.format(
238 backend.repo_name, commit_id, comment_id))
238 backend.repo_name, commit_id, comment_id))
239 assert lnk in notification.body
239 assert lnk in notification.body
240
240
241 def test_delete(self, backend):
241 def test_delete(self, backend):
242 self.log_user()
242 self.log_user()
243 commit_id = backend.repo.get_commit('300').raw_id
243 commit_id = backend.repo.get_commit('300').raw_id
244 text = u'CommentOnCommit'
244 text = u'CommentOnCommit'
245
245
246 params = {'text': text, 'csrf_token': self.csrf_token}
246 params = {'text': text, 'csrf_token': self.csrf_token}
247 self.app.post(
247 self.app.post(
248 route_path(
248 route_path(
249 'repo_commit_comment_create',
249 'repo_commit_comment_create',
250 repo_name=backend.repo_name, commit_id=commit_id),
250 repo_name=backend.repo_name, commit_id=commit_id),
251 params=params)
251 params=params)
252
252
253 comments = ChangesetComment.query().all()
253 comments = ChangesetComment.query().all()
254 assert len(comments) == 1
254 assert len(comments) == 1
255 comment_id = comments[0].comment_id
255 comment_id = comments[0].comment_id
256
256
257 self.app.post(
257 self.app.post(
258 route_path('repo_commit_comment_delete',
258 route_path('repo_commit_comment_delete',
259 repo_name=backend.repo_name,
259 repo_name=backend.repo_name,
260 commit_id=commit_id,
260 commit_id=commit_id,
261 comment_id=comment_id),
261 comment_id=comment_id),
262 params={'csrf_token': self.csrf_token})
262 params={'csrf_token': self.csrf_token})
263
263
264 comments = ChangesetComment.query().all()
264 comments = ChangesetComment.query().all()
265 assert len(comments) == 0
265 assert len(comments) == 0
266
266
267 response = self.app.get(
267 response = self.app.get(
268 route_path('repo_commit',
268 route_path('repo_commit',
269 repo_name=backend.repo_name, commit_id=commit_id))
269 repo_name=backend.repo_name, commit_id=commit_id))
270 assert_comment_links(response, 0, 0)
270 assert_comment_links(response, 0, 0)
271
271
272 @pytest.mark.parametrize('renderer, input, output', [
272 @pytest.mark.parametrize('renderer, input, output', [
273 ('rst', 'plain text', '<p>plain text</p>'),
273 ('rst', 'plain text', '<p>plain text</p>'),
274 ('rst', 'header\n======', '<h1 class="title">header</h1>'),
274 ('rst', 'header\n======', '<h1 class="title">header</h1>'),
275 ('rst', '*italics*', '<em>italics</em>'),
275 ('rst', '*italics*', '<em>italics</em>'),
276 ('rst', '**bold**', '<strong>bold</strong>'),
276 ('rst', '**bold**', '<strong>bold</strong>'),
277 ('markdown', 'plain text', '<p>plain text</p>'),
277 ('markdown', 'plain text', '<p>plain text</p>'),
278 ('markdown', '# header', '<h1>header</h1>'),
278 ('markdown', '# header', '<h1>header</h1>'),
279 ('markdown', '*italics*', '<em>italics</em>'),
279 ('markdown', '*italics*', '<em>italics</em>'),
280 ('markdown', '**bold**', '<strong>bold</strong>'),
280 ('markdown', '**bold**', '<strong>bold</strong>'),
281 ], ids=['rst-plain', 'rst-header', 'rst-italics', 'rst-bold', 'md-plain',
281 ], ids=['rst-plain', 'rst-header', 'rst-italics', 'rst-bold', 'md-plain',
282 'md-header', 'md-italics', 'md-bold', ])
282 'md-header', 'md-italics', 'md-bold', ])
283 def test_preview(self, renderer, input, output, backend, xhr_header):
283 def test_preview(self, renderer, input, output, backend, xhr_header):
284 self.log_user()
284 self.log_user()
285 params = {
285 params = {
286 'renderer': renderer,
286 'renderer': renderer,
287 'text': input,
287 'text': input,
288 'csrf_token': self.csrf_token
288 'csrf_token': self.csrf_token
289 }
289 }
290 commit_id = '0' * 16 # fake this for tests
290 commit_id = '0' * 16 # fake this for tests
291 response = self.app.post(
291 response = self.app.post(
292 route_path('repo_commit_comment_preview',
292 route_path('repo_commit_comment_preview',
293 repo_name=backend.repo_name, commit_id=commit_id,),
293 repo_name=backend.repo_name, commit_id=commit_id,),
294 params=params,
294 params=params,
295 extra_environ=xhr_header)
295 extra_environ=xhr_header)
296
296
297 response.mustcontain(output)
297 response.mustcontain(output)
298
298
299
299
300 def assert_comment_links(response, comments, inline_comments):
300 def assert_comment_links(response, comments, inline_comments):
301 if comments == 1:
301 if comments == 1:
302 comments_text = "%d Commit comment" % comments
302 comments_text = "%d General" % comments
303 else:
303 else:
304 comments_text = "%d Commit comments" % comments
304 comments_text = "%d General" % comments
305
305
306 if inline_comments == 1:
306 if inline_comments == 1:
307 inline_comments_text = "%d Inline Comment" % inline_comments
307 inline_comments_text = "%d Inline" % inline_comments
308 else:
308 else:
309 inline_comments_text = "%d Inline Comments" % inline_comments
309 inline_comments_text = "%d Inline" % inline_comments
310
310
311 if comments:
311 if comments:
312 response.mustcontain('<a href="#comments">%s</a>,' % comments_text)
312 response.mustcontain('<a href="#comments">%s</a>,' % comments_text)
313 else:
313 else:
314 response.mustcontain(comments_text)
314 response.mustcontain(comments_text)
315
315
316 if inline_comments:
316 if inline_comments:
317 response.mustcontain(
317 response.mustcontain(
318 'id="inline-comments-counter">%s</' % inline_comments_text)
318 'id="inline-comments-counter">%s' % inline_comments_text)
319 else:
319 else:
320 response.mustcontain(inline_comments_text)
320 response.mustcontain(inline_comments_text)
@@ -1,318 +1,327 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2019 RhodeCode GmbH
3 # Copyright (C) 2010-2019 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 pytest
21 import pytest
22
22
23 from rhodecode.apps.repository.tests.test_repo_compare import ComparePage
23 from rhodecode.lib.helpers import _shorten_commit_id
24 from rhodecode.lib.helpers import _shorten_commit_id
24
25
25
26
26 def route_path(name, params=None, **kwargs):
27 def route_path(name, params=None, **kwargs):
27 import urllib
28 import urllib
28
29
29 base_url = {
30 base_url = {
30 'repo_commit': '/{repo_name}/changeset/{commit_id}',
31 'repo_commit': '/{repo_name}/changeset/{commit_id}',
31 'repo_commit_children': '/{repo_name}/changeset_children/{commit_id}',
32 'repo_commit_children': '/{repo_name}/changeset_children/{commit_id}',
32 'repo_commit_parents': '/{repo_name}/changeset_parents/{commit_id}',
33 'repo_commit_parents': '/{repo_name}/changeset_parents/{commit_id}',
33 'repo_commit_raw': '/{repo_name}/changeset-diff/{commit_id}',
34 'repo_commit_raw': '/{repo_name}/changeset-diff/{commit_id}',
34 'repo_commit_patch': '/{repo_name}/changeset-patch/{commit_id}',
35 'repo_commit_patch': '/{repo_name}/changeset-patch/{commit_id}',
35 'repo_commit_download': '/{repo_name}/changeset-download/{commit_id}',
36 'repo_commit_download': '/{repo_name}/changeset-download/{commit_id}',
36 'repo_commit_data': '/{repo_name}/changeset-data/{commit_id}',
37 'repo_commit_data': '/{repo_name}/changeset-data/{commit_id}',
37 'repo_compare': '/{repo_name}/compare/{source_ref_type}@{source_ref}...{target_ref_type}@{target_ref}',
38 'repo_compare': '/{repo_name}/compare/{source_ref_type}@{source_ref}...{target_ref_type}@{target_ref}',
38 }[name].format(**kwargs)
39 }[name].format(**kwargs)
39
40
40 if params:
41 if params:
41 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
42 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
42 return base_url
43 return base_url
43
44
44
45
45 @pytest.mark.usefixtures("app")
46 @pytest.mark.usefixtures("app")
46 class TestRepoCommitView(object):
47 class TestRepoCommitView(object):
47
48
48 def test_show_commit(self, backend):
49 def test_show_commit(self, backend):
49 commit_id = self.commit_id[backend.alias]
50 commit_id = self.commit_id[backend.alias]
50 response = self.app.get(route_path(
51 response = self.app.get(route_path(
51 'repo_commit', repo_name=backend.repo_name, commit_id=commit_id))
52 'repo_commit', repo_name=backend.repo_name, commit_id=commit_id))
52 response.mustcontain('Added a symlink')
53 response.mustcontain('Added a symlink')
53 response.mustcontain(commit_id)
54 response.mustcontain(commit_id)
54 response.mustcontain('No newline at end of file')
55 response.mustcontain('No newline at end of file')
55
56
56 def test_show_raw(self, backend):
57 def test_show_raw(self, backend):
57 commit_id = self.commit_id[backend.alias]
58 commit_id = self.commit_id[backend.alias]
58 response = self.app.get(route_path(
59 response = self.app.get(route_path(
59 'repo_commit_raw',
60 'repo_commit_raw',
60 repo_name=backend.repo_name, commit_id=commit_id))
61 repo_name=backend.repo_name, commit_id=commit_id))
61 assert response.body == self.diffs[backend.alias]
62 assert response.body == self.diffs[backend.alias]
62
63
63 def test_show_raw_patch(self, backend):
64 def test_show_raw_patch(self, backend):
64 response = self.app.get(route_path(
65 response = self.app.get(route_path(
65 'repo_commit_patch', repo_name=backend.repo_name,
66 'repo_commit_patch', repo_name=backend.repo_name,
66 commit_id=self.commit_id[backend.alias]))
67 commit_id=self.commit_id[backend.alias]))
67 assert response.body == self.patches[backend.alias]
68 assert response.body == self.patches[backend.alias]
68
69
69 def test_commit_download(self, backend):
70 def test_commit_download(self, backend):
70 response = self.app.get(route_path(
71 response = self.app.get(route_path(
71 'repo_commit_download',
72 'repo_commit_download',
72 repo_name=backend.repo_name,
73 repo_name=backend.repo_name,
73 commit_id=self.commit_id[backend.alias]))
74 commit_id=self.commit_id[backend.alias]))
74 assert response.body == self.diffs[backend.alias]
75 assert response.body == self.diffs[backend.alias]
75
76
76 def test_single_commit_page_different_ops(self, backend):
77 def test_single_commit_page_different_ops(self, backend):
77 commit_id = {
78 commit_id = {
78 'hg': '603d6c72c46d953420c89d36372f08d9f305f5dd',
79 'hg': '603d6c72c46d953420c89d36372f08d9f305f5dd',
79 'git': '03fa803d7e9fb14daa9a3089e0d1494eda75d986',
80 'git': '03fa803d7e9fb14daa9a3089e0d1494eda75d986',
80 'svn': '337',
81 'svn': '337',
81 }
82 }
82 diff_stat = {
83 diff_stat = {
83 'git': '20 files changed: 941 inserted, 286 deleted',
84 'hg': (21, 943, 288),
84 'svn': '21 files changed: 943 inserted, 288 deleted',
85 'git': (20, 941, 286),
85 'hg': '21 files changed: 943 inserted, 288 deleted',
86 'svn': (21, 943, 288),
87 }
86
88
87 }
88 commit_id = commit_id[backend.alias]
89 commit_id = commit_id[backend.alias]
89 response = self.app.get(route_path(
90 response = self.app.get(route_path(
90 'repo_commit',
91 'repo_commit',
91 repo_name=backend.repo_name, commit_id=commit_id))
92 repo_name=backend.repo_name, commit_id=commit_id))
92
93
93 response.mustcontain(_shorten_commit_id(commit_id))
94 response.mustcontain(_shorten_commit_id(commit_id))
94 response.mustcontain(diff_stat[backend.alias])
95
96 compare_page = ComparePage(response)
97 file_changes = diff_stat[backend.alias]
98 compare_page.contains_change_summary(*file_changes)
95
99
96 # files op files
100 # files op files
97 response.mustcontain('File not present at commit: %s' %
101 response.mustcontain('File not present at commit: %s' %
98 _shorten_commit_id(commit_id))
102 _shorten_commit_id(commit_id))
99
103
100 # svn uses a different filename
104 # svn uses a different filename
101 if backend.alias == 'svn':
105 if backend.alias == 'svn':
102 response.mustcontain('new file 10644')
106 response.mustcontain('new file 10644')
103 else:
107 else:
104 response.mustcontain('new file 100644')
108 response.mustcontain('new file 100644')
105 response.mustcontain('Changed theme to ADC theme') # commit msg
109 response.mustcontain('Changed theme to ADC theme') # commit msg
106
110
107 self._check_new_diff_menus(response, right_menu=True)
111 self._check_new_diff_menus(response, right_menu=True)
108
112
109 def test_commit_range_page_different_ops(self, backend):
113 def test_commit_range_page_different_ops(self, backend):
110 commit_id_range = {
114 commit_id_range = {
111 'hg': (
115 'hg': (
112 '25d7e49c18b159446cadfa506a5cf8ad1cb04067',
116 '25d7e49c18b159446cadfa506a5cf8ad1cb04067',
113 '603d6c72c46d953420c89d36372f08d9f305f5dd'),
117 '603d6c72c46d953420c89d36372f08d9f305f5dd'),
114 'git': (
118 'git': (
115 '6fc9270775aaf5544c1deb014f4ddd60c952fcbb',
119 '6fc9270775aaf5544c1deb014f4ddd60c952fcbb',
116 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'),
120 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'),
117 'svn': (
121 'svn': (
118 '335',
122 '335',
119 '337'),
123 '337'),
120 }
124 }
121 commit_ids = commit_id_range[backend.alias]
125 commit_ids = commit_id_range[backend.alias]
122 commit_id = '%s...%s' % (commit_ids[0], commit_ids[1])
126 commit_id = '%s...%s' % (commit_ids[0], commit_ids[1])
123 response = self.app.get(route_path(
127 response = self.app.get(route_path(
124 'repo_commit',
128 'repo_commit',
125 repo_name=backend.repo_name, commit_id=commit_id))
129 repo_name=backend.repo_name, commit_id=commit_id))
126
130
127 response.mustcontain(_shorten_commit_id(commit_ids[0]))
131 response.mustcontain(_shorten_commit_id(commit_ids[0]))
128 response.mustcontain(_shorten_commit_id(commit_ids[1]))
132 response.mustcontain(_shorten_commit_id(commit_ids[1]))
129
133
134 compare_page = ComparePage(response)
135
130 # svn is special
136 # svn is special
131 if backend.alias == 'svn':
137 if backend.alias == 'svn':
132 response.mustcontain('new file 10644')
138 response.mustcontain('new file 10644')
133 response.mustcontain('1 file changed: 5 inserted, 1 deleted')
139 for file_changes in [(1, 5, 1), (12, 236, 22), (21, 943, 288)]:
134 response.mustcontain('12 files changed: 236 inserted, 22 deleted')
140 compare_page.contains_change_summary(*file_changes)
135 response.mustcontain('21 files changed: 943 inserted, 288 deleted')
136 elif backend.alias == 'git':
141 elif backend.alias == 'git':
137 response.mustcontain('new file 100644')
142 response.mustcontain('new file 100644')
138 response.mustcontain('12 files changed: 222 inserted, 20 deleted')
143 for file_changes in [(12, 222, 20), (20, 941, 286)]:
139 response.mustcontain('20 files changed: 941 inserted, 286 deleted')
144 compare_page.contains_change_summary(*file_changes)
140 else:
145 else:
141 response.mustcontain('new file 100644')
146 response.mustcontain('new file 100644')
142 response.mustcontain('12 files changed: 222 inserted, 20 deleted')
147 for file_changes in [(12, 222, 20), (21, 943, 288)]:
143 response.mustcontain('21 files changed: 943 inserted, 288 deleted')
148 compare_page.contains_change_summary(*file_changes)
144
149
145 # files op files
150 # files op files
146 response.mustcontain('File not present at commit: %s' %
151 response.mustcontain('File not present at commit: %s' % _shorten_commit_id(commit_ids[1]))
147 _shorten_commit_id(commit_ids[1]))
148 response.mustcontain('Added docstrings to vcs.cli') # commit msg
152 response.mustcontain('Added docstrings to vcs.cli') # commit msg
149 response.mustcontain('Changed theme to ADC theme') # commit msg
153 response.mustcontain('Changed theme to ADC theme') # commit msg
150
154
151 self._check_new_diff_menus(response)
155 self._check_new_diff_menus(response)
152
156
153 def test_combined_compare_commit_page_different_ops(self, backend):
157 def test_combined_compare_commit_page_different_ops(self, backend):
154 commit_id_range = {
158 commit_id_range = {
155 'hg': (
159 'hg': (
156 '4fdd71e9427417b2e904e0464c634fdee85ec5a7',
160 '4fdd71e9427417b2e904e0464c634fdee85ec5a7',
157 '603d6c72c46d953420c89d36372f08d9f305f5dd'),
161 '603d6c72c46d953420c89d36372f08d9f305f5dd'),
158 'git': (
162 'git': (
159 'f5fbf9cfd5f1f1be146f6d3b38bcd791a7480c13',
163 'f5fbf9cfd5f1f1be146f6d3b38bcd791a7480c13',
160 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'),
164 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'),
161 'svn': (
165 'svn': (
162 '335',
166 '335',
163 '337'),
167 '337'),
164 }
168 }
165 commit_ids = commit_id_range[backend.alias]
169 commit_ids = commit_id_range[backend.alias]
166 response = self.app.get(route_path(
170 response = self.app.get(route_path(
167 'repo_compare',
171 'repo_compare',
168 repo_name=backend.repo_name,
172 repo_name=backend.repo_name,
169 source_ref_type='rev', source_ref=commit_ids[0],
173 source_ref_type='rev', source_ref=commit_ids[0],
170 target_ref_type='rev', target_ref=commit_ids[1], ))
174 target_ref_type='rev', target_ref=commit_ids[1], ))
171
175
172 response.mustcontain(_shorten_commit_id(commit_ids[0]))
176 response.mustcontain(_shorten_commit_id(commit_ids[0]))
173 response.mustcontain(_shorten_commit_id(commit_ids[1]))
177 response.mustcontain(_shorten_commit_id(commit_ids[1]))
174
178
175 # files op files
179 # files op files
176 response.mustcontain('File not present at commit: %s' %
180 response.mustcontain('File not present at commit: %s' %
177 _shorten_commit_id(commit_ids[1]))
181 _shorten_commit_id(commit_ids[1]))
178
182
183 compare_page = ComparePage(response)
184
179 # svn is special
185 # svn is special
180 if backend.alias == 'svn':
186 if backend.alias == 'svn':
181 response.mustcontain('new file 10644')
187 response.mustcontain('new file 10644')
182 response.mustcontain('32 files changed: 1179 inserted, 310 deleted')
188 file_changes = (32, 1179, 310)
189 compare_page.contains_change_summary(*file_changes)
183 elif backend.alias == 'git':
190 elif backend.alias == 'git':
184 response.mustcontain('new file 100644')
191 response.mustcontain('new file 100644')
185 response.mustcontain('31 files changed: 1163 inserted, 306 deleted')
192 file_changes = (31, 1163, 306)
193 compare_page.contains_change_summary(*file_changes)
186 else:
194 else:
187 response.mustcontain('new file 100644')
195 response.mustcontain('new file 100644')
188 response.mustcontain('32 files changed: 1165 inserted, 308 deleted')
196 file_changes = (32, 1165, 308)
197 compare_page.contains_change_summary(*file_changes)
189
198
190 response.mustcontain('Added docstrings to vcs.cli') # commit msg
199 response.mustcontain('Added docstrings to vcs.cli') # commit msg
191 response.mustcontain('Changed theme to ADC theme') # commit msg
200 response.mustcontain('Changed theme to ADC theme') # commit msg
192
201
193 self._check_new_diff_menus(response)
202 self._check_new_diff_menus(response)
194
203
195 def test_changeset_range(self, backend):
204 def test_changeset_range(self, backend):
196 self._check_changeset_range(
205 self._check_changeset_range(
197 backend, self.commit_id_range, self.commit_id_range_result)
206 backend, self.commit_id_range, self.commit_id_range_result)
198
207
199 def test_changeset_range_with_initial_commit(self, backend):
208 def test_changeset_range_with_initial_commit(self, backend):
200 commit_id_range = {
209 commit_id_range = {
201 'hg': (
210 'hg': (
202 'b986218ba1c9b0d6a259fac9b050b1724ed8e545'
211 'b986218ba1c9b0d6a259fac9b050b1724ed8e545'
203 '...6cba7170863a2411822803fa77a0a264f1310b35'),
212 '...6cba7170863a2411822803fa77a0a264f1310b35'),
204 'git': (
213 'git': (
205 'c1214f7e79e02fc37156ff215cd71275450cffc3'
214 'c1214f7e79e02fc37156ff215cd71275450cffc3'
206 '...fa6600f6848800641328adbf7811fd2372c02ab2'),
215 '...fa6600f6848800641328adbf7811fd2372c02ab2'),
207 'svn': '1...3',
216 'svn': '1...3',
208 }
217 }
209 commit_id_range_result = {
218 commit_id_range_result = {
210 'hg': ['b986218ba1c9', '3d8f361e72ab', '6cba7170863a'],
219 'hg': ['b986218ba1c9', '3d8f361e72ab', '6cba7170863a'],
211 'git': ['c1214f7e79e0', '38b5fe81f109', 'fa6600f68488'],
220 'git': ['c1214f7e79e0', '38b5fe81f109', 'fa6600f68488'],
212 'svn': ['1', '2', '3'],
221 'svn': ['1', '2', '3'],
213 }
222 }
214 self._check_changeset_range(
223 self._check_changeset_range(
215 backend, commit_id_range, commit_id_range_result)
224 backend, commit_id_range, commit_id_range_result)
216
225
217 def _check_changeset_range(
226 def _check_changeset_range(
218 self, backend, commit_id_ranges, commit_id_range_result):
227 self, backend, commit_id_ranges, commit_id_range_result):
219 response = self.app.get(
228 response = self.app.get(
220 route_path('repo_commit',
229 route_path('repo_commit',
221 repo_name=backend.repo_name,
230 repo_name=backend.repo_name,
222 commit_id=commit_id_ranges[backend.alias]))
231 commit_id=commit_id_ranges[backend.alias]))
223
232
224 expected_result = commit_id_range_result[backend.alias]
233 expected_result = commit_id_range_result[backend.alias]
225 response.mustcontain('{} commits'.format(len(expected_result)))
234 response.mustcontain('{} commits'.format(len(expected_result)))
226 for commit_id in expected_result:
235 for commit_id in expected_result:
227 response.mustcontain(commit_id)
236 response.mustcontain(commit_id)
228
237
229 commit_id = {
238 commit_id = {
230 'hg': '2062ec7beeeaf9f44a1c25c41479565040b930b2',
239 'hg': '2062ec7beeeaf9f44a1c25c41479565040b930b2',
231 'svn': '393',
240 'svn': '393',
232 'git': 'fd627b9e0dd80b47be81af07c4a98518244ed2f7',
241 'git': 'fd627b9e0dd80b47be81af07c4a98518244ed2f7',
233 }
242 }
234
243
235 commit_id_range = {
244 commit_id_range = {
236 'hg': (
245 'hg': (
237 'a53d9201d4bc278910d416d94941b7ea007ecd52'
246 'a53d9201d4bc278910d416d94941b7ea007ecd52'
238 '...2062ec7beeeaf9f44a1c25c41479565040b930b2'),
247 '...2062ec7beeeaf9f44a1c25c41479565040b930b2'),
239 'git': (
248 'git': (
240 '7ab37bc680b4aa72c34d07b230c866c28e9fc204'
249 '7ab37bc680b4aa72c34d07b230c866c28e9fc204'
241 '...fd627b9e0dd80b47be81af07c4a98518244ed2f7'),
250 '...fd627b9e0dd80b47be81af07c4a98518244ed2f7'),
242 'svn': '391...393',
251 'svn': '391...393',
243 }
252 }
244
253
245 commit_id_range_result = {
254 commit_id_range_result = {
246 'hg': ['a53d9201d4bc', '96507bd11ecc', '2062ec7beeea'],
255 'hg': ['a53d9201d4bc', '96507bd11ecc', '2062ec7beeea'],
247 'git': ['7ab37bc680b4', '5f2c6ee19592', 'fd627b9e0dd8'],
256 'git': ['7ab37bc680b4', '5f2c6ee19592', 'fd627b9e0dd8'],
248 'svn': ['391', '392', '393'],
257 'svn': ['391', '392', '393'],
249 }
258 }
250
259
251 diffs = {
260 diffs = {
252 'hg': r"""diff --git a/README b/README
261 'hg': r"""diff --git a/README b/README
253 new file mode 120000
262 new file mode 120000
254 --- /dev/null
263 --- /dev/null
255 +++ b/README
264 +++ b/README
256 @@ -0,0 +1,1 @@
265 @@ -0,0 +1,1 @@
257 +README.rst
266 +README.rst
258 \ No newline at end of file
267 \ No newline at end of file
259 """,
268 """,
260 'git': r"""diff --git a/README b/README
269 'git': r"""diff --git a/README b/README
261 new file mode 120000
270 new file mode 120000
262 index 0000000..92cacd2
271 index 0000000..92cacd2
263 --- /dev/null
272 --- /dev/null
264 +++ b/README
273 +++ b/README
265 @@ -0,0 +1 @@
274 @@ -0,0 +1 @@
266 +README.rst
275 +README.rst
267 \ No newline at end of file
276 \ No newline at end of file
268 """,
277 """,
269 'svn': """Index: README
278 'svn': """Index: README
270 ===================================================================
279 ===================================================================
271 diff --git a/README b/README
280 diff --git a/README b/README
272 new file mode 10644
281 new file mode 10644
273 --- /dev/null\t(revision 0)
282 --- /dev/null\t(revision 0)
274 +++ b/README\t(revision 393)
283 +++ b/README\t(revision 393)
275 @@ -0,0 +1 @@
284 @@ -0,0 +1 @@
276 +link README.rst
285 +link README.rst
277 \\ No newline at end of file
286 \\ No newline at end of file
278 """,
287 """,
279 }
288 }
280
289
281 patches = {
290 patches = {
282 'hg': r"""# HG changeset patch
291 'hg': r"""# HG changeset patch
283 # User Marcin Kuzminski <marcin@python-works.com>
292 # User Marcin Kuzminski <marcin@python-works.com>
284 # Date 2014-01-07 12:21:40
293 # Date 2014-01-07 12:21:40
285 # Node ID 2062ec7beeeaf9f44a1c25c41479565040b930b2
294 # Node ID 2062ec7beeeaf9f44a1c25c41479565040b930b2
286 # Parent 96507bd11ecc815ebc6270fdf6db110928c09c1e
295 # Parent 96507bd11ecc815ebc6270fdf6db110928c09c1e
287
296
288 Added a symlink
297 Added a symlink
289
298
290 """ + diffs['hg'],
299 """ + diffs['hg'],
291 'git': r"""From fd627b9e0dd80b47be81af07c4a98518244ed2f7 2014-01-07 12:22:20
300 'git': r"""From fd627b9e0dd80b47be81af07c4a98518244ed2f7 2014-01-07 12:22:20
292 From: Marcin Kuzminski <marcin@python-works.com>
301 From: Marcin Kuzminski <marcin@python-works.com>
293 Date: 2014-01-07 12:22:20
302 Date: 2014-01-07 12:22:20
294 Subject: [PATCH] Added a symlink
303 Subject: [PATCH] Added a symlink
295
304
296 ---
305 ---
297
306
298 """ + diffs['git'],
307 """ + diffs['git'],
299 'svn': r"""# SVN changeset patch
308 'svn': r"""# SVN changeset patch
300 # User marcin
309 # User marcin
301 # Date 2014-09-02 12:25:22.071142
310 # Date 2014-09-02 12:25:22.071142
302 # Revision 393
311 # Revision 393
303
312
304 Added a symlink
313 Added a symlink
305
314
306 """ + diffs['svn'],
315 """ + diffs['svn'],
307 }
316 }
308
317
309 def _check_new_diff_menus(self, response, right_menu=False,):
318 def _check_new_diff_menus(self, response, right_menu=False,):
310 # individual file diff menus
319 # individual file diff menus
311 for elem in ['Show file before', 'Show file after']:
320 for elem in ['Show file before', 'Show file after']:
312 response.mustcontain(elem)
321 response.mustcontain(elem)
313
322
314 # right pane diff menus
323 # right pane diff menus
315 if right_menu:
324 if right_menu:
316 for elem in ['Hide whitespace changes', 'Toggle Wide Mode diff',
325 for elem in ['Hide whitespace changes', 'Toggle wide diff',
317 'Show full context diff']:
326 'Show full context diff']:
318 response.mustcontain(elem)
327 response.mustcontain(elem)
@@ -1,666 +1,666 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2019 RhodeCode GmbH
3 # Copyright (C) 2010-2019 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 assert_session_flash
26 from rhodecode.tests import 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 def route_path(name, params=None, **kwargs):
30 def route_path(name, params=None, **kwargs):
31 import urllib
31 import urllib
32
32
33 base_url = {
33 base_url = {
34 'repo_compare_select': '/{repo_name}/compare',
34 'repo_compare_select': '/{repo_name}/compare',
35 'repo_compare': '/{repo_name}/compare/{source_ref_type}@{source_ref}...{target_ref_type}@{target_ref}',
35 'repo_compare': '/{repo_name}/compare/{source_ref_type}@{source_ref}...{target_ref_type}@{target_ref}',
36 }[name].format(**kwargs)
36 }[name].format(**kwargs)
37
37
38 if params:
38 if params:
39 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
39 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
40 return base_url
40 return base_url
41
41
42
42
43 @pytest.mark.usefixtures("autologin_user", "app")
43 @pytest.mark.usefixtures("autologin_user", "app")
44 class TestCompareView(object):
44 class TestCompareView(object):
45
45
46 def test_compare_index_is_reached_at_least_once(self, backend):
46 def test_compare_index_is_reached_at_least_once(self, backend):
47 repo = backend.repo
47 repo = backend.repo
48 self.app.get(
48 self.app.get(
49 route_path('repo_compare_select', repo_name=repo.repo_name))
49 route_path('repo_compare_select', repo_name=repo.repo_name))
50
50
51 @pytest.mark.xfail_backends("svn", reason="Requires pull")
51 @pytest.mark.xfail_backends("svn", reason="Requires pull")
52 def test_compare_remote_with_different_commit_indexes(self, backend):
52 def test_compare_remote_with_different_commit_indexes(self, backend):
53 # Preparing the following repository structure:
53 # Preparing the following repository structure:
54 #
54 #
55 # Origin repository has two commits:
55 # Origin repository has two commits:
56 #
56 #
57 # 0 1
57 # 0 1
58 # A -- D
58 # A -- D
59 #
59 #
60 # The fork of it has a few more commits and "D" has a commit index
60 # The fork of it has a few more commits and "D" has a commit index
61 # which does not exist in origin.
61 # which does not exist in origin.
62 #
62 #
63 # 0 1 2 3 4
63 # 0 1 2 3 4
64 # A -- -- -- D -- E
64 # A -- -- -- D -- E
65 # \- B -- C
65 # \- B -- C
66 #
66 #
67
67
68 fork = backend.create_repo()
68 fork = backend.create_repo()
69
69
70 # prepare fork
70 # prepare fork
71 commit0 = commit_change(
71 commit0 = commit_change(
72 fork.repo_name, filename='file1', content='A',
72 fork.repo_name, filename='file1', content='A',
73 message='A', vcs_type=backend.alias, parent=None, newfile=True)
73 message='A', vcs_type=backend.alias, parent=None, newfile=True)
74
74
75 commit1 = commit_change(
75 commit1 = commit_change(
76 fork.repo_name, filename='file1', content='B',
76 fork.repo_name, filename='file1', content='B',
77 message='B, child of A', vcs_type=backend.alias, parent=commit0)
77 message='B, child of A', vcs_type=backend.alias, parent=commit0)
78
78
79 commit_change( # commit 2
79 commit_change( # commit 2
80 fork.repo_name, filename='file1', content='C',
80 fork.repo_name, filename='file1', content='C',
81 message='C, child of B', vcs_type=backend.alias, parent=commit1)
81 message='C, child of B', vcs_type=backend.alias, parent=commit1)
82
82
83 commit3 = commit_change(
83 commit3 = commit_change(
84 fork.repo_name, filename='file1', content='D',
84 fork.repo_name, filename='file1', content='D',
85 message='D, child of A', vcs_type=backend.alias, parent=commit0)
85 message='D, child of A', vcs_type=backend.alias, parent=commit0)
86
86
87 commit4 = commit_change(
87 commit4 = commit_change(
88 fork.repo_name, filename='file1', content='E',
88 fork.repo_name, filename='file1', content='E',
89 message='E, child of D', vcs_type=backend.alias, parent=commit3)
89 message='E, child of D', vcs_type=backend.alias, parent=commit3)
90
90
91 # prepare origin repository, taking just the history up to D
91 # prepare origin repository, taking just the history up to D
92 origin = backend.create_repo()
92 origin = backend.create_repo()
93
93
94 origin_repo = origin.scm_instance(cache=False)
94 origin_repo = origin.scm_instance(cache=False)
95 origin_repo.config.clear_section('hooks')
95 origin_repo.config.clear_section('hooks')
96 origin_repo.pull(fork.repo_full_path, commit_ids=[commit3.raw_id])
96 origin_repo.pull(fork.repo_full_path, commit_ids=[commit3.raw_id])
97 origin_repo = origin.scm_instance(cache=False) # cache rebuild
97 origin_repo = origin.scm_instance(cache=False) # cache rebuild
98
98
99 # Verify test fixture setup
99 # Verify test fixture setup
100 # This does not work for git
100 # This does not work for git
101 if backend.alias != 'git':
101 if backend.alias != 'git':
102 assert 5 == len(fork.scm_instance().commit_ids)
102 assert 5 == len(fork.scm_instance().commit_ids)
103 assert 2 == len(origin_repo.commit_ids)
103 assert 2 == len(origin_repo.commit_ids)
104
104
105 # Comparing the revisions
105 # Comparing the revisions
106 response = self.app.get(
106 response = self.app.get(
107 route_path('repo_compare',
107 route_path('repo_compare',
108 repo_name=origin.repo_name,
108 repo_name=origin.repo_name,
109 source_ref_type="rev", source_ref=commit3.raw_id,
109 source_ref_type="rev", source_ref=commit3.raw_id,
110 target_ref_type="rev", target_ref=commit4.raw_id,
110 target_ref_type="rev", target_ref=commit4.raw_id,
111 params=dict(merge='1', target_repo=fork.repo_name)
111 params=dict(merge='1', target_repo=fork.repo_name)
112 ))
112 ))
113
113
114 compare_page = ComparePage(response)
114 compare_page = ComparePage(response)
115 compare_page.contains_commits([commit4])
115 compare_page.contains_commits([commit4])
116
116
117 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
117 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
118 def test_compare_forks_on_branch_extra_commits(self, backend):
118 def test_compare_forks_on_branch_extra_commits(self, backend):
119 repo1 = backend.create_repo()
119 repo1 = backend.create_repo()
120
120
121 # commit something !
121 # commit something !
122 commit0 = commit_change(
122 commit0 = commit_change(
123 repo1.repo_name, filename='file1', content='line1\n',
123 repo1.repo_name, filename='file1', content='line1\n',
124 message='commit1', vcs_type=backend.alias, parent=None,
124 message='commit1', vcs_type=backend.alias, parent=None,
125 newfile=True)
125 newfile=True)
126
126
127 # fork this repo
127 # fork this repo
128 repo2 = backend.create_fork()
128 repo2 = backend.create_fork()
129
129
130 # add two extra commit into fork
130 # add two extra commit into fork
131 commit1 = commit_change(
131 commit1 = commit_change(
132 repo2.repo_name, filename='file1', content='line1\nline2\n',
132 repo2.repo_name, filename='file1', content='line1\nline2\n',
133 message='commit2', vcs_type=backend.alias, parent=commit0)
133 message='commit2', vcs_type=backend.alias, parent=commit0)
134
134
135 commit2 = commit_change(
135 commit2 = commit_change(
136 repo2.repo_name, filename='file1', content='line1\nline2\nline3\n',
136 repo2.repo_name, filename='file1', content='line1\nline2\nline3\n',
137 message='commit3', vcs_type=backend.alias, parent=commit1)
137 message='commit3', vcs_type=backend.alias, parent=commit1)
138
138
139 commit_id1 = repo1.scm_instance().DEFAULT_BRANCH_NAME
139 commit_id1 = repo1.scm_instance().DEFAULT_BRANCH_NAME
140 commit_id2 = repo2.scm_instance().DEFAULT_BRANCH_NAME
140 commit_id2 = repo2.scm_instance().DEFAULT_BRANCH_NAME
141
141
142 response = self.app.get(
142 response = self.app.get(
143 route_path('repo_compare',
143 route_path('repo_compare',
144 repo_name=repo1.repo_name,
144 repo_name=repo1.repo_name,
145 source_ref_type="branch", source_ref=commit_id2,
145 source_ref_type="branch", source_ref=commit_id2,
146 target_ref_type="branch", target_ref=commit_id1,
146 target_ref_type="branch", target_ref=commit_id1,
147 params=dict(merge='1', target_repo=repo2.repo_name)
147 params=dict(merge='1', target_repo=repo2.repo_name)
148 ))
148 ))
149
149
150 response.mustcontain('%s@%s' % (repo1.repo_name, commit_id2))
150 response.mustcontain('%s@%s' % (repo1.repo_name, commit_id2))
151 response.mustcontain('%s@%s' % (repo2.repo_name, commit_id1))
151 response.mustcontain('%s@%s' % (repo2.repo_name, commit_id1))
152
152
153 compare_page = ComparePage(response)
153 compare_page = ComparePage(response)
154 compare_page.contains_change_summary(1, 2, 0)
154 compare_page.contains_change_summary(1, 2, 0)
155 compare_page.contains_commits([commit1, commit2])
155 compare_page.contains_commits([commit1, commit2])
156
156
157 anchor = 'a_c-{}-826e8142e6ba'.format(commit0.short_id)
157 anchor = 'a_c-{}-826e8142e6ba'.format(commit0.short_id)
158 compare_page.contains_file_links_and_anchors([('file1', anchor), ])
158 compare_page.contains_file_links_and_anchors([('file1', anchor), ])
159
159
160 # Swap is removed when comparing branches since it's a PR feature and
160 # Swap is removed when comparing branches since it's a PR feature and
161 # it is then a preview mode
161 # it is then a preview mode
162 compare_page.swap_is_hidden()
162 compare_page.swap_is_hidden()
163 compare_page.target_source_are_disabled()
163 compare_page.target_source_are_disabled()
164
164
165 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
165 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
166 def test_compare_forks_on_branch_extra_commits_origin_has_incomming(self, backend):
166 def test_compare_forks_on_branch_extra_commits_origin_has_incomming(self, backend):
167 repo1 = backend.create_repo()
167 repo1 = backend.create_repo()
168
168
169 # commit something !
169 # commit something !
170 commit0 = commit_change(
170 commit0 = commit_change(
171 repo1.repo_name, filename='file1', content='line1\n',
171 repo1.repo_name, filename='file1', content='line1\n',
172 message='commit1', vcs_type=backend.alias, parent=None,
172 message='commit1', vcs_type=backend.alias, parent=None,
173 newfile=True)
173 newfile=True)
174
174
175 # fork this repo
175 # fork this repo
176 repo2 = backend.create_fork()
176 repo2 = backend.create_fork()
177
177
178 # now commit something to origin repo
178 # now commit something to origin repo
179 commit_change(
179 commit_change(
180 repo1.repo_name, filename='file2', content='line1file2\n',
180 repo1.repo_name, filename='file2', content='line1file2\n',
181 message='commit2', vcs_type=backend.alias, parent=commit0,
181 message='commit2', vcs_type=backend.alias, parent=commit0,
182 newfile=True)
182 newfile=True)
183
183
184 # add two extra commit into fork
184 # add two extra commit into fork
185 commit1 = commit_change(
185 commit1 = commit_change(
186 repo2.repo_name, filename='file1', content='line1\nline2\n',
186 repo2.repo_name, filename='file1', content='line1\nline2\n',
187 message='commit2', vcs_type=backend.alias, parent=commit0)
187 message='commit2', vcs_type=backend.alias, parent=commit0)
188
188
189 commit2 = commit_change(
189 commit2 = commit_change(
190 repo2.repo_name, filename='file1', content='line1\nline2\nline3\n',
190 repo2.repo_name, filename='file1', content='line1\nline2\nline3\n',
191 message='commit3', vcs_type=backend.alias, parent=commit1)
191 message='commit3', vcs_type=backend.alias, parent=commit1)
192
192
193 commit_id1 = repo1.scm_instance().DEFAULT_BRANCH_NAME
193 commit_id1 = repo1.scm_instance().DEFAULT_BRANCH_NAME
194 commit_id2 = repo2.scm_instance().DEFAULT_BRANCH_NAME
194 commit_id2 = repo2.scm_instance().DEFAULT_BRANCH_NAME
195
195
196 response = self.app.get(
196 response = self.app.get(
197 route_path('repo_compare',
197 route_path('repo_compare',
198 repo_name=repo1.repo_name,
198 repo_name=repo1.repo_name,
199 source_ref_type="branch", source_ref=commit_id2,
199 source_ref_type="branch", source_ref=commit_id2,
200 target_ref_type="branch", target_ref=commit_id1,
200 target_ref_type="branch", target_ref=commit_id1,
201 params=dict(merge='1', target_repo=repo2.repo_name),
201 params=dict(merge='1', target_repo=repo2.repo_name),
202 ))
202 ))
203
203
204 response.mustcontain('%s@%s' % (repo1.repo_name, commit_id2))
204 response.mustcontain('%s@%s' % (repo1.repo_name, commit_id2))
205 response.mustcontain('%s@%s' % (repo2.repo_name, commit_id1))
205 response.mustcontain('%s@%s' % (repo2.repo_name, commit_id1))
206
206
207 compare_page = ComparePage(response)
207 compare_page = ComparePage(response)
208 compare_page.contains_change_summary(1, 2, 0)
208 compare_page.contains_change_summary(1, 2, 0)
209 compare_page.contains_commits([commit1, commit2])
209 compare_page.contains_commits([commit1, commit2])
210 anchor = 'a_c-{}-826e8142e6ba'.format(commit0.short_id)
210 anchor = 'a_c-{}-826e8142e6ba'.format(commit0.short_id)
211 compare_page.contains_file_links_and_anchors([('file1', anchor), ])
211 compare_page.contains_file_links_and_anchors([('file1', anchor), ])
212
212
213 # Swap is removed when comparing branches since it's a PR feature and
213 # Swap is removed when comparing branches since it's a PR feature and
214 # it is then a preview mode
214 # it is then a preview mode
215 compare_page.swap_is_hidden()
215 compare_page.swap_is_hidden()
216 compare_page.target_source_are_disabled()
216 compare_page.target_source_are_disabled()
217
217
218 @pytest.mark.xfail_backends("svn")
218 @pytest.mark.xfail_backends("svn")
219 # TODO(marcink): no svn support for compare two seperate repos
219 # TODO(marcink): no svn support for compare two seperate repos
220 def test_compare_of_unrelated_forks(self, backend):
220 def test_compare_of_unrelated_forks(self, backend):
221 orig = backend.create_repo(number_of_commits=1)
221 orig = backend.create_repo(number_of_commits=1)
222 fork = backend.create_repo(number_of_commits=1)
222 fork = backend.create_repo(number_of_commits=1)
223
223
224 response = self.app.get(
224 response = self.app.get(
225 route_path('repo_compare',
225 route_path('repo_compare',
226 repo_name=orig.repo_name,
226 repo_name=orig.repo_name,
227 source_ref_type="rev", source_ref="tip",
227 source_ref_type="rev", source_ref="tip",
228 target_ref_type="rev", target_ref="tip",
228 target_ref_type="rev", target_ref="tip",
229 params=dict(merge='1', target_repo=fork.repo_name),
229 params=dict(merge='1', target_repo=fork.repo_name),
230 ),
230 ),
231 status=302)
231 status=302)
232 response = response.follow()
232 response = response.follow()
233 response.mustcontain("Repositories unrelated.")
233 response.mustcontain("Repositories unrelated.")
234
234
235 @pytest.mark.xfail_backends("svn")
235 @pytest.mark.xfail_backends("svn")
236 def test_compare_cherry_pick_commits_from_bottom(self, backend):
236 def test_compare_cherry_pick_commits_from_bottom(self, backend):
237
237
238 # repo1:
238 # repo1:
239 # commit0:
239 # commit0:
240 # commit1:
240 # commit1:
241 # repo1-fork- in which we will cherry pick bottom commits
241 # repo1-fork- in which we will cherry pick bottom commits
242 # commit0:
242 # commit0:
243 # commit1:
243 # commit1:
244 # commit2: x
244 # commit2: x
245 # commit3: x
245 # commit3: x
246 # commit4: x
246 # commit4: x
247 # commit5:
247 # commit5:
248 # make repo1, and commit1+commit2
248 # make repo1, and commit1+commit2
249
249
250 repo1 = backend.create_repo()
250 repo1 = backend.create_repo()
251
251
252 # commit something !
252 # commit something !
253 commit0 = commit_change(
253 commit0 = commit_change(
254 repo1.repo_name, filename='file1', content='line1\n',
254 repo1.repo_name, filename='file1', content='line1\n',
255 message='commit1', vcs_type=backend.alias, parent=None,
255 message='commit1', vcs_type=backend.alias, parent=None,
256 newfile=True)
256 newfile=True)
257 commit1 = commit_change(
257 commit1 = commit_change(
258 repo1.repo_name, filename='file1', content='line1\nline2\n',
258 repo1.repo_name, filename='file1', content='line1\nline2\n',
259 message='commit2', vcs_type=backend.alias, parent=commit0)
259 message='commit2', vcs_type=backend.alias, parent=commit0)
260
260
261 # fork this repo
261 # fork this repo
262 repo2 = backend.create_fork()
262 repo2 = backend.create_fork()
263
263
264 # now make commit3-6
264 # now make commit3-6
265 commit2 = commit_change(
265 commit2 = commit_change(
266 repo1.repo_name, filename='file1', content='line1\nline2\nline3\n',
266 repo1.repo_name, filename='file1', content='line1\nline2\nline3\n',
267 message='commit3', vcs_type=backend.alias, parent=commit1)
267 message='commit3', vcs_type=backend.alias, parent=commit1)
268 commit3 = commit_change(
268 commit3 = commit_change(
269 repo1.repo_name, filename='file1',
269 repo1.repo_name, filename='file1',
270 content='line1\nline2\nline3\nline4\n', message='commit4',
270 content='line1\nline2\nline3\nline4\n', message='commit4',
271 vcs_type=backend.alias, parent=commit2)
271 vcs_type=backend.alias, parent=commit2)
272 commit4 = commit_change(
272 commit4 = commit_change(
273 repo1.repo_name, filename='file1',
273 repo1.repo_name, filename='file1',
274 content='line1\nline2\nline3\nline4\nline5\n', message='commit5',
274 content='line1\nline2\nline3\nline4\nline5\n', message='commit5',
275 vcs_type=backend.alias, parent=commit3)
275 vcs_type=backend.alias, parent=commit3)
276 commit_change( # commit 5
276 commit_change( # commit 5
277 repo1.repo_name, filename='file1',
277 repo1.repo_name, filename='file1',
278 content='line1\nline2\nline3\nline4\nline5\nline6\n',
278 content='line1\nline2\nline3\nline4\nline5\nline6\n',
279 message='commit6', vcs_type=backend.alias, parent=commit4)
279 message='commit6', vcs_type=backend.alias, parent=commit4)
280
280
281 response = self.app.get(
281 response = self.app.get(
282 route_path('repo_compare',
282 route_path('repo_compare',
283 repo_name=repo2.repo_name,
283 repo_name=repo2.repo_name,
284 # parent of commit2, in target repo2
284 # parent of commit2, in target repo2
285 source_ref_type="rev", source_ref=commit1.raw_id,
285 source_ref_type="rev", source_ref=commit1.raw_id,
286 target_ref_type="rev", target_ref=commit4.raw_id,
286 target_ref_type="rev", target_ref=commit4.raw_id,
287 params=dict(merge='1', target_repo=repo1.repo_name),
287 params=dict(merge='1', target_repo=repo1.repo_name),
288 ))
288 ))
289 response.mustcontain('%s@%s' % (repo2.repo_name, commit1.short_id))
289 response.mustcontain('%s@%s' % (repo2.repo_name, commit1.short_id))
290 response.mustcontain('%s@%s' % (repo1.repo_name, commit4.short_id))
290 response.mustcontain('%s@%s' % (repo1.repo_name, commit4.short_id))
291
291
292 # files
292 # files
293 compare_page = ComparePage(response)
293 compare_page = ComparePage(response)
294 compare_page.contains_change_summary(1, 3, 0)
294 compare_page.contains_change_summary(1, 3, 0)
295 compare_page.contains_commits([commit2, commit3, commit4])
295 compare_page.contains_commits([commit2, commit3, commit4])
296 anchor = 'a_c-{}-826e8142e6ba'.format(commit1.short_id)
296 anchor = 'a_c-{}-826e8142e6ba'.format(commit1.short_id)
297 compare_page.contains_file_links_and_anchors([('file1', anchor),])
297 compare_page.contains_file_links_and_anchors([('file1', anchor),])
298
298
299 @pytest.mark.xfail_backends("svn")
299 @pytest.mark.xfail_backends("svn")
300 def test_compare_cherry_pick_commits_from_top(self, backend):
300 def test_compare_cherry_pick_commits_from_top(self, backend):
301 # repo1:
301 # repo1:
302 # commit0:
302 # commit0:
303 # commit1:
303 # commit1:
304 # repo1-fork- in which we will cherry pick bottom commits
304 # repo1-fork- in which we will cherry pick bottom commits
305 # commit0:
305 # commit0:
306 # commit1:
306 # commit1:
307 # commit2:
307 # commit2:
308 # commit3: x
308 # commit3: x
309 # commit4: x
309 # commit4: x
310 # commit5: x
310 # commit5: x
311
311
312 # make repo1, and commit1+commit2
312 # make repo1, and commit1+commit2
313 repo1 = backend.create_repo()
313 repo1 = backend.create_repo()
314
314
315 # commit something !
315 # commit something !
316 commit0 = commit_change(
316 commit0 = commit_change(
317 repo1.repo_name, filename='file1', content='line1\n',
317 repo1.repo_name, filename='file1', content='line1\n',
318 message='commit1', vcs_type=backend.alias, parent=None,
318 message='commit1', vcs_type=backend.alias, parent=None,
319 newfile=True)
319 newfile=True)
320 commit1 = commit_change(
320 commit1 = commit_change(
321 repo1.repo_name, filename='file1', content='line1\nline2\n',
321 repo1.repo_name, filename='file1', content='line1\nline2\n',
322 message='commit2', vcs_type=backend.alias, parent=commit0)
322 message='commit2', vcs_type=backend.alias, parent=commit0)
323
323
324 # fork this repo
324 # fork this repo
325 backend.create_fork()
325 backend.create_fork()
326
326
327 # now make commit3-6
327 # now make commit3-6
328 commit2 = commit_change(
328 commit2 = commit_change(
329 repo1.repo_name, filename='file1', content='line1\nline2\nline3\n',
329 repo1.repo_name, filename='file1', content='line1\nline2\nline3\n',
330 message='commit3', vcs_type=backend.alias, parent=commit1)
330 message='commit3', vcs_type=backend.alias, parent=commit1)
331 commit3 = commit_change(
331 commit3 = commit_change(
332 repo1.repo_name, filename='file1',
332 repo1.repo_name, filename='file1',
333 content='line1\nline2\nline3\nline4\n', message='commit4',
333 content='line1\nline2\nline3\nline4\n', message='commit4',
334 vcs_type=backend.alias, parent=commit2)
334 vcs_type=backend.alias, parent=commit2)
335 commit4 = commit_change(
335 commit4 = commit_change(
336 repo1.repo_name, filename='file1',
336 repo1.repo_name, filename='file1',
337 content='line1\nline2\nline3\nline4\nline5\n', message='commit5',
337 content='line1\nline2\nline3\nline4\nline5\n', message='commit5',
338 vcs_type=backend.alias, parent=commit3)
338 vcs_type=backend.alias, parent=commit3)
339 commit5 = commit_change(
339 commit5 = commit_change(
340 repo1.repo_name, filename='file1',
340 repo1.repo_name, filename='file1',
341 content='line1\nline2\nline3\nline4\nline5\nline6\n',
341 content='line1\nline2\nline3\nline4\nline5\nline6\n',
342 message='commit6', vcs_type=backend.alias, parent=commit4)
342 message='commit6', vcs_type=backend.alias, parent=commit4)
343
343
344 response = self.app.get(
344 response = self.app.get(
345 route_path('repo_compare',
345 route_path('repo_compare',
346 repo_name=repo1.repo_name,
346 repo_name=repo1.repo_name,
347 # parent of commit3, not in source repo2
347 # parent of commit3, not in source repo2
348 source_ref_type="rev", source_ref=commit2.raw_id,
348 source_ref_type="rev", source_ref=commit2.raw_id,
349 target_ref_type="rev", target_ref=commit5.raw_id,
349 target_ref_type="rev", target_ref=commit5.raw_id,
350 params=dict(merge='1'),))
350 params=dict(merge='1'),))
351
351
352 response.mustcontain('%s@%s' % (repo1.repo_name, commit2.short_id))
352 response.mustcontain('%s@%s' % (repo1.repo_name, commit2.short_id))
353 response.mustcontain('%s@%s' % (repo1.repo_name, commit5.short_id))
353 response.mustcontain('%s@%s' % (repo1.repo_name, commit5.short_id))
354
354
355 compare_page = ComparePage(response)
355 compare_page = ComparePage(response)
356 compare_page.contains_change_summary(1, 3, 0)
356 compare_page.contains_change_summary(1, 3, 0)
357 compare_page.contains_commits([commit3, commit4, commit5])
357 compare_page.contains_commits([commit3, commit4, commit5])
358
358
359 # files
359 # files
360 anchor = 'a_c-{}-826e8142e6ba'.format(commit2.short_id)
360 anchor = 'a_c-{}-826e8142e6ba'.format(commit2.short_id)
361 compare_page.contains_file_links_and_anchors([('file1', anchor),])
361 compare_page.contains_file_links_and_anchors([('file1', anchor),])
362
362
363 @pytest.mark.xfail_backends("svn")
363 @pytest.mark.xfail_backends("svn")
364 def test_compare_remote_branches(self, backend):
364 def test_compare_remote_branches(self, backend):
365 repo1 = backend.repo
365 repo1 = backend.repo
366 repo2 = backend.create_fork()
366 repo2 = backend.create_fork()
367
367
368 commit_id1 = repo1.get_commit(commit_idx=3).raw_id
368 commit_id1 = repo1.get_commit(commit_idx=3).raw_id
369 commit_id1_short = repo1.get_commit(commit_idx=3).short_id
369 commit_id1_short = repo1.get_commit(commit_idx=3).short_id
370 commit_id2 = repo1.get_commit(commit_idx=6).raw_id
370 commit_id2 = repo1.get_commit(commit_idx=6).raw_id
371 commit_id2_short = repo1.get_commit(commit_idx=6).short_id
371 commit_id2_short = repo1.get_commit(commit_idx=6).short_id
372
372
373 response = self.app.get(
373 response = self.app.get(
374 route_path('repo_compare',
374 route_path('repo_compare',
375 repo_name=repo1.repo_name,
375 repo_name=repo1.repo_name,
376 source_ref_type="rev", source_ref=commit_id1,
376 source_ref_type="rev", source_ref=commit_id1,
377 target_ref_type="rev", target_ref=commit_id2,
377 target_ref_type="rev", target_ref=commit_id2,
378 params=dict(merge='1', target_repo=repo2.repo_name),
378 params=dict(merge='1', target_repo=repo2.repo_name),
379 ))
379 ))
380
380
381 response.mustcontain('%s@%s' % (repo1.repo_name, commit_id1))
381 response.mustcontain('%s@%s' % (repo1.repo_name, commit_id1))
382 response.mustcontain('%s@%s' % (repo2.repo_name, commit_id2))
382 response.mustcontain('%s@%s' % (repo2.repo_name, commit_id2))
383
383
384 compare_page = ComparePage(response)
384 compare_page = ComparePage(response)
385
385
386 # outgoing commits between those commits
386 # outgoing commits between those commits
387 compare_page.contains_commits(
387 compare_page.contains_commits(
388 [repo2.get_commit(commit_idx=x) for x in [4, 5, 6]])
388 [repo2.get_commit(commit_idx=x) for x in [4, 5, 6]])
389
389
390 # files
390 # files
391 compare_page.contains_file_links_and_anchors([
391 compare_page.contains_file_links_and_anchors([
392 ('vcs/backends/hg.py', 'a_c-{}-9c390eb52cd6'.format(commit_id2_short)),
392 ('vcs/backends/hg.py', 'a_c-{}-9c390eb52cd6'.format(commit_id2_short)),
393 ('vcs/backends/__init__.py', 'a_c-{}-41b41c1f2796'.format(commit_id1_short)),
393 ('vcs/backends/__init__.py', 'a_c-{}-41b41c1f2796'.format(commit_id1_short)),
394 ('vcs/backends/base.py', 'a_c-{}-2f574d260608'.format(commit_id1_short)),
394 ('vcs/backends/base.py', 'a_c-{}-2f574d260608'.format(commit_id1_short)),
395 ])
395 ])
396
396
397 @pytest.mark.xfail_backends("svn")
397 @pytest.mark.xfail_backends("svn")
398 def test_source_repo_new_commits_after_forking_simple_diff(self, backend):
398 def test_source_repo_new_commits_after_forking_simple_diff(self, backend):
399 repo1 = backend.create_repo()
399 repo1 = backend.create_repo()
400 r1_name = repo1.repo_name
400 r1_name = repo1.repo_name
401
401
402 commit0 = commit_change(
402 commit0 = commit_change(
403 repo=r1_name, filename='file1',
403 repo=r1_name, filename='file1',
404 content='line1', message='commit1', vcs_type=backend.alias,
404 content='line1', message='commit1', vcs_type=backend.alias,
405 newfile=True)
405 newfile=True)
406 assert repo1.scm_instance().commit_ids == [commit0.raw_id]
406 assert repo1.scm_instance().commit_ids == [commit0.raw_id]
407
407
408 # fork the repo1
408 # fork the repo1
409 repo2 = backend.create_fork()
409 repo2 = backend.create_fork()
410 assert repo2.scm_instance().commit_ids == [commit0.raw_id]
410 assert repo2.scm_instance().commit_ids == [commit0.raw_id]
411
411
412 self.r2_id = repo2.repo_id
412 self.r2_id = repo2.repo_id
413 r2_name = repo2.repo_name
413 r2_name = repo2.repo_name
414
414
415 commit1 = commit_change(
415 commit1 = commit_change(
416 repo=r2_name, filename='file1-fork',
416 repo=r2_name, filename='file1-fork',
417 content='file1-line1-from-fork', message='commit1-fork',
417 content='file1-line1-from-fork', message='commit1-fork',
418 vcs_type=backend.alias, parent=repo2.scm_instance()[-1],
418 vcs_type=backend.alias, parent=repo2.scm_instance()[-1],
419 newfile=True)
419 newfile=True)
420
420
421 commit2 = commit_change(
421 commit2 = commit_change(
422 repo=r2_name, filename='file2-fork',
422 repo=r2_name, filename='file2-fork',
423 content='file2-line1-from-fork', message='commit2-fork',
423 content='file2-line1-from-fork', message='commit2-fork',
424 vcs_type=backend.alias, parent=commit1,
424 vcs_type=backend.alias, parent=commit1,
425 newfile=True)
425 newfile=True)
426
426
427 commit_change( # commit 3
427 commit_change( # commit 3
428 repo=r2_name, filename='file3-fork',
428 repo=r2_name, filename='file3-fork',
429 content='file3-line1-from-fork', message='commit3-fork',
429 content='file3-line1-from-fork', message='commit3-fork',
430 vcs_type=backend.alias, parent=commit2, newfile=True)
430 vcs_type=backend.alias, parent=commit2, newfile=True)
431
431
432 # compare !
432 # compare !
433 commit_id1 = repo1.scm_instance().DEFAULT_BRANCH_NAME
433 commit_id1 = repo1.scm_instance().DEFAULT_BRANCH_NAME
434 commit_id2 = repo2.scm_instance().DEFAULT_BRANCH_NAME
434 commit_id2 = repo2.scm_instance().DEFAULT_BRANCH_NAME
435
435
436 response = self.app.get(
436 response = self.app.get(
437 route_path('repo_compare',
437 route_path('repo_compare',
438 repo_name=r2_name,
438 repo_name=r2_name,
439 source_ref_type="branch", source_ref=commit_id1,
439 source_ref_type="branch", source_ref=commit_id1,
440 target_ref_type="branch", target_ref=commit_id2,
440 target_ref_type="branch", target_ref=commit_id2,
441 params=dict(merge='1', target_repo=r1_name),
441 params=dict(merge='1', target_repo=r1_name),
442 ))
442 ))
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 route_path('repo_compare',
456 route_path('repo_compare',
457 repo_name=r2_name,
457 repo_name=r2_name,
458 source_ref_type="branch", source_ref=commit_id1,
458 source_ref_type="branch", source_ref=commit_id1,
459 target_ref_type="branch", target_ref=commit_id2,
459 target_ref_type="branch", target_ref=commit_id2,
460 params=dict(merge='1', target_repo=r1_name),
460 params=dict(merge='1', target_repo=r1_name),
461 ))
461 ))
462
462
463 response.mustcontain('%s@%s' % (r2_name, commit_id1))
463 response.mustcontain('%s@%s' % (r2_name, commit_id1))
464 response.mustcontain('%s@%s' % (r1_name, commit_id2))
464 response.mustcontain('%s@%s' % (r1_name, commit_id2))
465
465
466 response.mustcontain("""commit2-parent""")
466 response.mustcontain("""commit2-parent""")
467 response.mustcontain("""line1-added-after-fork""")
467 response.mustcontain("""line1-added-after-fork""")
468 compare_page = ComparePage(response)
468 compare_page = ComparePage(response)
469 compare_page.contains_change_summary(1, 1, 0)
469 compare_page.contains_change_summary(1, 1, 0)
470
470
471 @pytest.mark.xfail_backends("svn")
471 @pytest.mark.xfail_backends("svn")
472 def test_compare_commits(self, backend, xhr_header):
472 def test_compare_commits(self, backend, xhr_header):
473 commit0 = backend.repo.get_commit(commit_idx=0)
473 commit0 = backend.repo.get_commit(commit_idx=0)
474 commit1 = backend.repo.get_commit(commit_idx=1)
474 commit1 = backend.repo.get_commit(commit_idx=1)
475
475
476 response = self.app.get(
476 response = self.app.get(
477 route_path('repo_compare',
477 route_path('repo_compare',
478 repo_name=backend.repo_name,
478 repo_name=backend.repo_name,
479 source_ref_type="rev", source_ref=commit0.raw_id,
479 source_ref_type="rev", source_ref=commit0.raw_id,
480 target_ref_type="rev", target_ref=commit1.raw_id,
480 target_ref_type="rev", target_ref=commit1.raw_id,
481 params=dict(merge='1')
481 params=dict(merge='1')
482 ),
482 ),
483 extra_environ=xhr_header, )
483 extra_environ=xhr_header, )
484
484
485 # outgoing commits between those commits
485 # outgoing commits between those commits
486 compare_page = ComparePage(response)
486 compare_page = ComparePage(response)
487 compare_page.contains_commits(commits=[commit1], ancestors=[commit0])
487 compare_page.contains_commits(commits=[commit1], ancestors=[commit0])
488
488
489 def test_errors_when_comparing_unknown_source_repo(self, backend):
489 def test_errors_when_comparing_unknown_source_repo(self, backend):
490 repo = backend.repo
490 repo = backend.repo
491 badrepo = 'badrepo'
491 badrepo = 'badrepo'
492
492
493 response = self.app.get(
493 response = self.app.get(
494 route_path('repo_compare',
494 route_path('repo_compare',
495 repo_name=badrepo,
495 repo_name=badrepo,
496 source_ref_type="rev", source_ref='tip',
496 source_ref_type="rev", source_ref='tip',
497 target_ref_type="rev", target_ref='tip',
497 target_ref_type="rev", target_ref='tip',
498 params=dict(merge='1', target_repo=repo.repo_name)
498 params=dict(merge='1', target_repo=repo.repo_name)
499 ),
499 ),
500 status=404)
500 status=404)
501
501
502 def test_errors_when_comparing_unknown_target_repo(self, backend):
502 def test_errors_when_comparing_unknown_target_repo(self, backend):
503 repo = backend.repo
503 repo = backend.repo
504 badrepo = 'badrepo'
504 badrepo = 'badrepo'
505
505
506 response = self.app.get(
506 response = self.app.get(
507 route_path('repo_compare',
507 route_path('repo_compare',
508 repo_name=repo.repo_name,
508 repo_name=repo.repo_name,
509 source_ref_type="rev", source_ref='tip',
509 source_ref_type="rev", source_ref='tip',
510 target_ref_type="rev", target_ref='tip',
510 target_ref_type="rev", target_ref='tip',
511 params=dict(merge='1', target_repo=badrepo),
511 params=dict(merge='1', target_repo=badrepo),
512 ),
512 ),
513 status=302)
513 status=302)
514 redirected = response.follow()
514 redirected = response.follow()
515 redirected.mustcontain(
515 redirected.mustcontain(
516 'Could not find the target repo: `{}`'.format(badrepo))
516 'Could not find the target repo: `{}`'.format(badrepo))
517
517
518 def test_compare_not_in_preview_mode(self, backend_stub):
518 def test_compare_not_in_preview_mode(self, backend_stub):
519 commit0 = backend_stub.repo.get_commit(commit_idx=0)
519 commit0 = backend_stub.repo.get_commit(commit_idx=0)
520 commit1 = backend_stub.repo.get_commit(commit_idx=1)
520 commit1 = backend_stub.repo.get_commit(commit_idx=1)
521
521
522 response = self.app.get(
522 response = self.app.get(
523 route_path('repo_compare',
523 route_path('repo_compare',
524 repo_name=backend_stub.repo_name,
524 repo_name=backend_stub.repo_name,
525 source_ref_type="rev", source_ref=commit0.raw_id,
525 source_ref_type="rev", source_ref=commit0.raw_id,
526 target_ref_type="rev", target_ref=commit1.raw_id,
526 target_ref_type="rev", target_ref=commit1.raw_id,
527 ))
527 ))
528
528
529 # outgoing commits between those commits
529 # outgoing commits between those commits
530 compare_page = ComparePage(response)
530 compare_page = ComparePage(response)
531 compare_page.swap_is_visible()
531 compare_page.swap_is_visible()
532 compare_page.target_source_are_enabled()
532 compare_page.target_source_are_enabled()
533
533
534 def test_compare_of_fork_with_largefiles(self, backend_hg, settings_util):
534 def test_compare_of_fork_with_largefiles(self, backend_hg, settings_util):
535 orig = backend_hg.create_repo(number_of_commits=1)
535 orig = backend_hg.create_repo(number_of_commits=1)
536 fork = backend_hg.create_fork()
536 fork = backend_hg.create_fork()
537
537
538 settings_util.create_repo_rhodecode_ui(
538 settings_util.create_repo_rhodecode_ui(
539 orig, 'extensions', value='', key='largefiles', active=False)
539 orig, 'extensions', value='', key='largefiles', active=False)
540 settings_util.create_repo_rhodecode_ui(
540 settings_util.create_repo_rhodecode_ui(
541 fork, 'extensions', value='', key='largefiles', active=True)
541 fork, 'extensions', value='', key='largefiles', active=True)
542
542
543 compare_module = ('rhodecode.lib.vcs.backends.hg.repository.'
543 compare_module = ('rhodecode.lib.vcs.backends.hg.repository.'
544 'MercurialRepository.compare')
544 'MercurialRepository.compare')
545 with mock.patch(compare_module) as compare_mock:
545 with mock.patch(compare_module) as compare_mock:
546 compare_mock.side_effect = RepositoryRequirementError()
546 compare_mock.side_effect = RepositoryRequirementError()
547
547
548 response = self.app.get(
548 response = self.app.get(
549 route_path('repo_compare',
549 route_path('repo_compare',
550 repo_name=orig.repo_name,
550 repo_name=orig.repo_name,
551 source_ref_type="rev", source_ref="tip",
551 source_ref_type="rev", source_ref="tip",
552 target_ref_type="rev", target_ref="tip",
552 target_ref_type="rev", target_ref="tip",
553 params=dict(merge='1', target_repo=fork.repo_name),
553 params=dict(merge='1', target_repo=fork.repo_name),
554 ),
554 ),
555 status=302)
555 status=302)
556
556
557 assert_session_flash(
557 assert_session_flash(
558 response,
558 response,
559 'Could not compare repos with different large file settings')
559 'Could not compare repos with different large file settings')
560
560
561
561
562 @pytest.mark.usefixtures("autologin_user")
562 @pytest.mark.usefixtures("autologin_user")
563 class TestCompareControllerSvn(object):
563 class TestCompareControllerSvn(object):
564
564
565 def test_supports_references_with_path(self, app, backend_svn):
565 def test_supports_references_with_path(self, app, backend_svn):
566 repo = backend_svn['svn-simple-layout']
566 repo = backend_svn['svn-simple-layout']
567 commit_id = repo.get_commit(commit_idx=-1).raw_id
567 commit_id = repo.get_commit(commit_idx=-1).raw_id
568 response = app.get(
568 response = app.get(
569 route_path('repo_compare',
569 route_path('repo_compare',
570 repo_name=repo.repo_name,
570 repo_name=repo.repo_name,
571 source_ref_type="tag",
571 source_ref_type="tag",
572 source_ref="%s@%s" % ('tags/v0.1', commit_id),
572 source_ref="%s@%s" % ('tags/v0.1', commit_id),
573 target_ref_type="tag",
573 target_ref_type="tag",
574 target_ref="%s@%s" % ('tags/v0.2', commit_id),
574 target_ref="%s@%s" % ('tags/v0.2', commit_id),
575 params=dict(merge='1'),
575 params=dict(merge='1'),
576 ),
576 ),
577 status=200)
577 status=200)
578
578
579 # Expecting no commits, since both paths are at the same revision
579 # Expecting no commits, since both paths are at the same revision
580 response.mustcontain('No commits in this compare')
580 response.mustcontain('No commits in this compare')
581
581
582 # Should find only one file changed when comparing those two tags
582 # Should find only one file changed when comparing those two tags
583 response.mustcontain('example.py')
583 response.mustcontain('example.py')
584 compare_page = ComparePage(response)
584 compare_page = ComparePage(response)
585 compare_page.contains_change_summary(1, 5, 1)
585 compare_page.contains_change_summary(1, 5, 1)
586
586
587 def test_shows_commits_if_different_ids(self, app, backend_svn):
587 def test_shows_commits_if_different_ids(self, app, backend_svn):
588 repo = backend_svn['svn-simple-layout']
588 repo = backend_svn['svn-simple-layout']
589 source_id = repo.get_commit(commit_idx=-6).raw_id
589 source_id = repo.get_commit(commit_idx=-6).raw_id
590 target_id = repo.get_commit(commit_idx=-1).raw_id
590 target_id = repo.get_commit(commit_idx=-1).raw_id
591 response = app.get(
591 response = app.get(
592 route_path('repo_compare',
592 route_path('repo_compare',
593 repo_name=repo.repo_name,
593 repo_name=repo.repo_name,
594 source_ref_type="tag",
594 source_ref_type="tag",
595 source_ref="%s@%s" % ('tags/v0.1', source_id),
595 source_ref="%s@%s" % ('tags/v0.1', source_id),
596 target_ref_type="tag",
596 target_ref_type="tag",
597 target_ref="%s@%s" % ('tags/v0.2', target_id),
597 target_ref="%s@%s" % ('tags/v0.2', target_id),
598 params=dict(merge='1')
598 params=dict(merge='1')
599 ),
599 ),
600 status=200)
600 status=200)
601
601
602 # It should show commits
602 # It should show commits
603 assert 'No commits in this compare' not in response.body
603 assert 'No commits in this compare' not in response.body
604
604
605 # Should find only one file changed when comparing those two tags
605 # Should find only one file changed when comparing those two tags
606 response.mustcontain('example.py')
606 response.mustcontain('example.py')
607 compare_page = ComparePage(response)
607 compare_page = ComparePage(response)
608 compare_page.contains_change_summary(1, 5, 1)
608 compare_page.contains_change_summary(1, 5, 1)
609
609
610
610
611 class ComparePage(AssertResponse):
611 class ComparePage(AssertResponse):
612 """
612 """
613 Abstracts the page template from the tests
613 Abstracts the page template from the tests
614 """
614 """
615
615
616 def contains_file_links_and_anchors(self, files):
616 def contains_file_links_and_anchors(self, files):
617 doc = lxml.html.fromstring(self.response.body)
617 doc = lxml.html.fromstring(self.response.body)
618 for filename, file_id in files:
618 for filename, file_id in files:
619 self.contains_one_anchor(file_id)
619 self.contains_one_anchor(file_id)
620 diffblock = doc.cssselect('[data-f-path="%s"]' % filename)
620 diffblock = doc.cssselect('[data-f-path="%s"]' % filename)
621 assert len(diffblock) == 2
621 assert len(diffblock) == 2
622 assert len(diffblock[0].cssselect('a[href="#%s"]' % file_id)) == 1
622 assert len(diffblock[0].cssselect('a[href="#%s"]' % file_id)) == 1
623
623
624 def contains_change_summary(self, files_changed, inserted, deleted):
624 def contains_change_summary(self, files_changed, inserted, deleted):
625 template = (
625 template = (
626 "{files_changed} file{plural} changed: "
626 '{files_changed} file{plural} changed: '
627 "{inserted} inserted, {deleted} deleted")
627 '<span class="op-added">{inserted} inserted</span>, <span class="op-deleted">{deleted} deleted</span>')
628 self.response.mustcontain(template.format(
628 self.response.mustcontain(template.format(
629 files_changed=files_changed,
629 files_changed=files_changed,
630 plural="s" if files_changed > 1 else "",
630 plural="s" if files_changed > 1 else "",
631 inserted=inserted,
631 inserted=inserted,
632 deleted=deleted))
632 deleted=deleted))
633
633
634 def contains_commits(self, commits, ancestors=None):
634 def contains_commits(self, commits, ancestors=None):
635 response = self.response
635 response = self.response
636
636
637 for commit in commits:
637 for commit in commits:
638 # Expecting to see the commit message in an element which
638 # Expecting to see the commit message in an element which
639 # has the ID "c-{commit.raw_id}"
639 # has the ID "c-{commit.raw_id}"
640 self.element_contains('#c-' + commit.raw_id, commit.message)
640 self.element_contains('#c-' + commit.raw_id, commit.message)
641 self.contains_one_link(
641 self.contains_one_link(
642 'r%s:%s' % (commit.idx, commit.short_id),
642 'r%s:%s' % (commit.idx, commit.short_id),
643 self._commit_url(commit))
643 self._commit_url(commit))
644 if ancestors:
644 if ancestors:
645 response.mustcontain('Ancestor')
645 response.mustcontain('Ancestor')
646 for ancestor in ancestors:
646 for ancestor in ancestors:
647 self.contains_one_link(
647 self.contains_one_link(
648 ancestor.short_id, self._commit_url(ancestor))
648 ancestor.short_id, self._commit_url(ancestor))
649
649
650 def _commit_url(self, commit):
650 def _commit_url(self, commit):
651 return '/%s/changeset/%s' % (commit.repository.name, commit.raw_id)
651 return '/%s/changeset/%s' % (commit.repository.name, commit.raw_id)
652
652
653 def swap_is_hidden(self):
653 def swap_is_hidden(self):
654 assert '<a id="btn-swap"' not in self.response.text
654 assert '<a id="btn-swap"' not in self.response.text
655
655
656 def swap_is_visible(self):
656 def swap_is_visible(self):
657 assert '<a id="btn-swap"' in self.response.text
657 assert '<a id="btn-swap"' in self.response.text
658
658
659 def target_source_are_disabled(self):
659 def target_source_are_disabled(self):
660 response = self.response
660 response = self.response
661 response.mustcontain("var enable_fields = false;")
661 response.mustcontain("var enable_fields = false;")
662 response.mustcontain('.select2("enable", enable_fields)')
662 response.mustcontain('.select2("enable", enable_fields)')
663
663
664 def target_source_are_enabled(self):
664 def target_source_are_enabled(self):
665 response = self.response
665 response = self.response
666 response.mustcontain("var enable_fields = true;")
666 response.mustcontain("var enable_fields = true;")
@@ -1,259 +1,265 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2019 RhodeCode GmbH
3 # Copyright (C) 2010-2019 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 pytest
21 import pytest
22
22
23 from rhodecode.apps.repository.tests.test_repo_compare import ComparePage
23 from rhodecode.lib.vcs import nodes
24 from rhodecode.lib.vcs import nodes
24 from rhodecode.lib.vcs.backends.base import EmptyCommit
25 from rhodecode.lib.vcs.backends.base import EmptyCommit
25 from rhodecode.tests.fixture import Fixture
26 from rhodecode.tests.fixture import Fixture
26 from rhodecode.tests.utils import commit_change
27 from rhodecode.tests.utils import commit_change
27
28
28 fixture = Fixture()
29 fixture = Fixture()
29
30
30
31
31 def route_path(name, params=None, **kwargs):
32 def route_path(name, params=None, **kwargs):
32 import urllib
33 import urllib
33
34
34 base_url = {
35 base_url = {
35 'repo_compare_select': '/{repo_name}/compare',
36 'repo_compare_select': '/{repo_name}/compare',
36 'repo_compare': '/{repo_name}/compare/{source_ref_type}@{source_ref}...{target_ref_type}@{target_ref}',
37 'repo_compare': '/{repo_name}/compare/{source_ref_type}@{source_ref}...{target_ref_type}@{target_ref}',
37 }[name].format(**kwargs)
38 }[name].format(**kwargs)
38
39
39 if params:
40 if params:
40 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
41 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
41 return base_url
42 return base_url
42
43
43
44
44 @pytest.mark.usefixtures("autologin_user", "app")
45 @pytest.mark.usefixtures("autologin_user", "app")
45 class TestSideBySideDiff(object):
46 class TestSideBySideDiff(object):
46
47
47 def test_diff_sidebyside_single_commit(self, app, backend):
48 def test_diff_sidebyside_single_commit(self, app, backend):
48 commit_id_range = {
49 commit_id_range = {
49 'hg': {
50 'hg': {
50 'commits': ['25d7e49c18b159446cadfa506a5cf8ad1cb04067',
51 'commits': ['25d7e49c18b159446cadfa506a5cf8ad1cb04067',
51 '603d6c72c46d953420c89d36372f08d9f305f5dd'],
52 '603d6c72c46d953420c89d36372f08d9f305f5dd'],
52 'changes': '21 files changed: 943 inserted, 288 deleted'
53 'changes': (21, 943, 288),
53 },
54 },
54 'git': {
55 'git': {
55 'commits': ['6fc9270775aaf5544c1deb014f4ddd60c952fcbb',
56 'commits': ['6fc9270775aaf5544c1deb014f4ddd60c952fcbb',
56 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'],
57 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'],
57 'changes': '20 files changed: 941 inserted, 286 deleted'
58 'changes': (20, 941, 286),
58 },
59 },
59
60
60 'svn': {
61 'svn': {
61 'commits': ['336',
62 'commits': ['336',
62 '337'],
63 '337'],
63 'changes': '21 files changed: 943 inserted, 288 deleted'
64 'changes': (21, 943, 288),
64 },
65 },
65 }
66 }
66
67
67 commit_info = commit_id_range[backend.alias]
68 commit_info = commit_id_range[backend.alias]
68 commit2, commit1 = commit_info['commits']
69 commit2, commit1 = commit_info['commits']
69 file_changes = commit_info['changes']
70 file_changes = commit_info['changes']
70
71
71 response = self.app.get(route_path(
72 response = self.app.get(route_path(
72 'repo_compare',
73 'repo_compare',
73 repo_name=backend.repo_name,
74 repo_name=backend.repo_name,
74 source_ref_type='rev',
75 source_ref_type='rev',
75 source_ref=commit2,
76 source_ref=commit2,
76 target_repo=backend.repo_name,
77 target_repo=backend.repo_name,
77 target_ref_type='rev',
78 target_ref_type='rev',
78 target_ref=commit1,
79 target_ref=commit1,
79 params=dict(target_repo=backend.repo_name, diffmode='sidebyside')
80 params=dict(target_repo=backend.repo_name, diffmode='sidebyside')
80 ))
81 ))
81
82
82 response.mustcontain(file_changes)
83 compare_page = ComparePage(response)
84 compare_page.contains_change_summary(*file_changes)
83 response.mustcontain('Expand 1 commit')
85 response.mustcontain('Expand 1 commit')
84
86
85 def test_diff_sidebyside_two_commits(self, app, backend):
87 def test_diff_sidebyside_two_commits(self, app, backend):
86 commit_id_range = {
88 commit_id_range = {
87 'hg': {
89 'hg': {
88 'commits': ['4fdd71e9427417b2e904e0464c634fdee85ec5a7',
90 'commits': ['4fdd71e9427417b2e904e0464c634fdee85ec5a7',
89 '603d6c72c46d953420c89d36372f08d9f305f5dd'],
91 '603d6c72c46d953420c89d36372f08d9f305f5dd'],
90 'changes': '32 files changed: 1165 inserted, 308 deleted'
92 'changes': (32, 1165, 308),
91 },
93 },
92 'git': {
94 'git': {
93 'commits': ['f5fbf9cfd5f1f1be146f6d3b38bcd791a7480c13',
95 'commits': ['f5fbf9cfd5f1f1be146f6d3b38bcd791a7480c13',
94 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'],
96 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'],
95 'changes': '31 files changed: 1163 inserted, 306 deleted'
97 'changes': (31, 1163, 306),
96 },
98 },
97
99
98 'svn': {
100 'svn': {
99 'commits': ['335',
101 'commits': ['335',
100 '337'],
102 '337'],
101 'changes': '32 files changed: 1179 inserted, 310 deleted'
103 'changes': (32, 1179, 310),
102 },
104 },
103 }
105 }
104
106
105 commit_info = commit_id_range[backend.alias]
107 commit_info = commit_id_range[backend.alias]
106 commit2, commit1 = commit_info['commits']
108 commit2, commit1 = commit_info['commits']
107 file_changes = commit_info['changes']
109 file_changes = commit_info['changes']
108
110
109 response = self.app.get(route_path(
111 response = self.app.get(route_path(
110 'repo_compare',
112 'repo_compare',
111 repo_name=backend.repo_name,
113 repo_name=backend.repo_name,
112 source_ref_type='rev',
114 source_ref_type='rev',
113 source_ref=commit2,
115 source_ref=commit2,
114 target_repo=backend.repo_name,
116 target_repo=backend.repo_name,
115 target_ref_type='rev',
117 target_ref_type='rev',
116 target_ref=commit1,
118 target_ref=commit1,
117 params=dict(target_repo=backend.repo_name, diffmode='sidebyside')
119 params=dict(target_repo=backend.repo_name, diffmode='sidebyside')
118 ))
120 ))
119
121
120 response.mustcontain(file_changes)
122 compare_page = ComparePage(response)
123 compare_page.contains_change_summary(*file_changes)
124
121 response.mustcontain('Expand 2 commits')
125 response.mustcontain('Expand 2 commits')
122
126
123 @pytest.mark.xfail(reason='GIT does not handle empty commit compare correct (missing 1 commit)')
127 @pytest.mark.xfail(reason='GIT does not handle empty commit compare correct (missing 1 commit)')
124 def test_diff_side_by_side_from_0_commit(self, app, backend, backend_stub):
128 def test_diff_side_by_side_from_0_commit(self, app, backend, backend_stub):
125 f_path = 'test_sidebyside_file.py'
129 f_path = 'test_sidebyside_file.py'
126 commit1_content = 'content-25d7e49c18b159446c\n'
130 commit1_content = 'content-25d7e49c18b159446c\n'
127 commit2_content = 'content-603d6c72c46d953420\n'
131 commit2_content = 'content-603d6c72c46d953420\n'
128 repo = backend.create_repo()
132 repo = backend.create_repo()
129
133
130 commit1 = commit_change(
134 commit1 = commit_change(
131 repo.repo_name, filename=f_path, content=commit1_content,
135 repo.repo_name, filename=f_path, content=commit1_content,
132 message='A', vcs_type=backend.alias, parent=None, newfile=True)
136 message='A', vcs_type=backend.alias, parent=None, newfile=True)
133
137
134 commit2 = commit_change(
138 commit2 = commit_change(
135 repo.repo_name, filename=f_path, content=commit2_content,
139 repo.repo_name, filename=f_path, content=commit2_content,
136 message='B, child of A', vcs_type=backend.alias, parent=commit1)
140 message='B, child of A', vcs_type=backend.alias, parent=commit1)
137
141
138 response = self.app.get(route_path(
142 response = self.app.get(route_path(
139 'repo_compare',
143 'repo_compare',
140 repo_name=repo.repo_name,
144 repo_name=repo.repo_name,
141 source_ref_type='rev',
145 source_ref_type='rev',
142 source_ref=EmptyCommit().raw_id,
146 source_ref=EmptyCommit().raw_id,
143 target_ref_type='rev',
147 target_ref_type='rev',
144 target_ref=commit2.raw_id,
148 target_ref=commit2.raw_id,
145 params=dict(diffmode='sidebyside')
149 params=dict(diffmode='sidebyside')
146 ))
150 ))
147
151
148 response.mustcontain('Expand 2 commits')
152 response.mustcontain('Expand 2 commits')
149 response.mustcontain('123 file changed')
153 response.mustcontain('123 file changed')
150
154
151 response.mustcontain(
155 response.mustcontain(
152 'r%s:%s...r%s:%s' % (
156 'r%s:%s...r%s:%s' % (
153 commit1.idx, commit1.short_id, commit2.idx, commit2.short_id))
157 commit1.idx, commit1.short_id, commit2.idx, commit2.short_id))
154
158
155 response.mustcontain('<strong>{}</strong>'.format(f_path))
159 response.mustcontain(f_path)
156
160
157 @pytest.mark.xfail(reason='GIT does not handle empty commit compare correct (missing 1 commit)')
161 @pytest.mark.xfail(reason='GIT does not handle empty commit compare correct (missing 1 commit)')
158 def test_diff_side_by_side_from_0_commit_with_file_filter(self, app, backend, backend_stub):
162 def test_diff_side_by_side_from_0_commit_with_file_filter(self, app, backend, backend_stub):
159 f_path = 'test_sidebyside_file.py'
163 f_path = 'test_sidebyside_file.py'
160 commit1_content = 'content-25d7e49c18b159446c\n'
164 commit1_content = 'content-25d7e49c18b159446c\n'
161 commit2_content = 'content-603d6c72c46d953420\n'
165 commit2_content = 'content-603d6c72c46d953420\n'
162 repo = backend.create_repo()
166 repo = backend.create_repo()
163
167
164 commit1 = commit_change(
168 commit1 = commit_change(
165 repo.repo_name, filename=f_path, content=commit1_content,
169 repo.repo_name, filename=f_path, content=commit1_content,
166 message='A', vcs_type=backend.alias, parent=None, newfile=True)
170 message='A', vcs_type=backend.alias, parent=None, newfile=True)
167
171
168 commit2 = commit_change(
172 commit2 = commit_change(
169 repo.repo_name, filename=f_path, content=commit2_content,
173 repo.repo_name, filename=f_path, content=commit2_content,
170 message='B, child of A', vcs_type=backend.alias, parent=commit1)
174 message='B, child of A', vcs_type=backend.alias, parent=commit1)
171
175
172 response = self.app.get(route_path(
176 response = self.app.get(route_path(
173 'repo_compare',
177 'repo_compare',
174 repo_name=repo.repo_name,
178 repo_name=repo.repo_name,
175 source_ref_type='rev',
179 source_ref_type='rev',
176 source_ref=EmptyCommit().raw_id,
180 source_ref=EmptyCommit().raw_id,
177 target_ref_type='rev',
181 target_ref_type='rev',
178 target_ref=commit2.raw_id,
182 target_ref=commit2.raw_id,
179 params=dict(f_path=f_path, target_repo=repo.repo_name, diffmode='sidebyside')
183 params=dict(f_path=f_path, target_repo=repo.repo_name, diffmode='sidebyside')
180 ))
184 ))
181
185
182 response.mustcontain('Expand 2 commits')
186 response.mustcontain('Expand 2 commits')
183 response.mustcontain('1 file changed')
187 response.mustcontain('1 file changed')
184
188
185 response.mustcontain(
189 response.mustcontain(
186 'r%s:%s...r%s:%s' % (
190 'r%s:%s...r%s:%s' % (
187 commit1.idx, commit1.short_id, commit2.idx, commit2.short_id))
191 commit1.idx, commit1.short_id, commit2.idx, commit2.short_id))
188
192
189 response.mustcontain('<strong>{}</strong>'.format(f_path))
193 response.mustcontain(f_path)
190
194
191 def test_diff_side_by_side_with_empty_file(self, app, backend, backend_stub):
195 def test_diff_side_by_side_with_empty_file(self, app, backend, backend_stub):
192 commits = [
196 commits = [
193 {'message': 'First commit'},
197 {'message': 'First commit'},
194 {'message': 'Second commit'},
198 {'message': 'Second commit'},
195 {'message': 'Commit with binary',
199 {'message': 'Commit with binary',
196 'added': [nodes.FileNode('file.empty', content='')]},
200 'added': [nodes.FileNode('file.empty', content='')]},
197 ]
201 ]
198 f_path = 'file.empty'
202 f_path = 'file.empty'
199 repo = backend.create_repo(commits=commits)
203 repo = backend.create_repo(commits=commits)
200 commit1 = repo.get_commit(commit_idx=0)
204 commit1 = repo.get_commit(commit_idx=0)
201 commit2 = repo.get_commit(commit_idx=1)
205 commit2 = repo.get_commit(commit_idx=1)
202 commit3 = repo.get_commit(commit_idx=2)
206 commit3 = repo.get_commit(commit_idx=2)
203
207
204 response = self.app.get(route_path(
208 response = self.app.get(route_path(
205 'repo_compare',
209 'repo_compare',
206 repo_name=repo.repo_name,
210 repo_name=repo.repo_name,
207 source_ref_type='rev',
211 source_ref_type='rev',
208 source_ref=commit1.raw_id,
212 source_ref=commit1.raw_id,
209 target_ref_type='rev',
213 target_ref_type='rev',
210 target_ref=commit3.raw_id,
214 target_ref=commit3.raw_id,
211 params=dict(f_path=f_path, target_repo=repo.repo_name, diffmode='sidebyside')
215 params=dict(f_path=f_path, target_repo=repo.repo_name, diffmode='sidebyside')
212 ))
216 ))
213
217
214 response.mustcontain('Expand 2 commits')
218 response.mustcontain('Expand 2 commits')
215 response.mustcontain('1 file changed')
219 response.mustcontain('1 file changed')
216
220
217 response.mustcontain(
221 response.mustcontain(
218 'r%s:%s...r%s:%s' % (
222 'r%s:%s...r%s:%s' % (
219 commit2.idx, commit2.short_id, commit3.idx, commit3.short_id))
223 commit2.idx, commit2.short_id, commit3.idx, commit3.short_id))
220
224
221 response.mustcontain('<strong>{}</strong>'.format(f_path))
225 response.mustcontain(f_path)
222
226
223 def test_diff_sidebyside_two_commits_with_file_filter(self, app, backend):
227 def test_diff_sidebyside_two_commits_with_file_filter(self, app, backend):
224 commit_id_range = {
228 commit_id_range = {
225 'hg': {
229 'hg': {
226 'commits': ['4fdd71e9427417b2e904e0464c634fdee85ec5a7',
230 'commits': ['4fdd71e9427417b2e904e0464c634fdee85ec5a7',
227 '603d6c72c46d953420c89d36372f08d9f305f5dd'],
231 '603d6c72c46d953420c89d36372f08d9f305f5dd'],
228 'changes': '1 file changed: 3 inserted, 3 deleted'
232 'changes': (1, 3, 3)
229 },
233 },
230 'git': {
234 'git': {
231 'commits': ['f5fbf9cfd5f1f1be146f6d3b38bcd791a7480c13',
235 'commits': ['f5fbf9cfd5f1f1be146f6d3b38bcd791a7480c13',
232 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'],
236 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'],
233 'changes': '1 file changed: 3 inserted, 3 deleted'
237 'changes': (1, 3, 3)
234 },
238 },
235
239
236 'svn': {
240 'svn': {
237 'commits': ['335',
241 'commits': ['335',
238 '337'],
242 '337'],
239 'changes': '1 file changed: 3 inserted, 3 deleted'
243 'changes': (1, 3, 3)
240 },
244 },
241 }
245 }
242 f_path = 'docs/conf.py'
246 f_path = 'docs/conf.py'
243
247
244 commit_info = commit_id_range[backend.alias]
248 commit_info = commit_id_range[backend.alias]
245 commit2, commit1 = commit_info['commits']
249 commit2, commit1 = commit_info['commits']
246 file_changes = commit_info['changes']
250 file_changes = commit_info['changes']
247
251
248 response = self.app.get(route_path(
252 response = self.app.get(route_path(
249 'repo_compare',
253 'repo_compare',
250 repo_name=backend.repo_name,
254 repo_name=backend.repo_name,
251 source_ref_type='rev',
255 source_ref_type='rev',
252 source_ref=commit2,
256 source_ref=commit2,
253 target_ref_type='rev',
257 target_ref_type='rev',
254 target_ref=commit1,
258 target_ref=commit1,
255 params=dict(f_path=f_path, target_repo=backend.repo_name, diffmode='sidebyside')
259 params=dict(f_path=f_path, target_repo=backend.repo_name, diffmode='sidebyside')
256 ))
260 ))
257
261
258 response.mustcontain('Expand 2 commits')
262 response.mustcontain('Expand 2 commits')
259 response.mustcontain(file_changes)
263
264 compare_page = ComparePage(response)
265 compare_page.contains_change_summary(*file_changes)
@@ -1,1066 +1,1070 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2019 RhodeCode GmbH
3 # Copyright (C) 2010-2019 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.apps.repository.tests.test_repo_compare import ComparePage
26 from rhodecode.apps.repository.views.repo_files import RepoFilesView
27 from rhodecode.apps.repository.views.repo_files import RepoFilesView
27 from rhodecode.lib import helpers as h
28 from rhodecode.lib import helpers as h
28 from rhodecode.lib.compat import OrderedDict
29 from rhodecode.lib.compat import OrderedDict
29 from rhodecode.lib.ext_json import json
30 from rhodecode.lib.ext_json import json
30 from rhodecode.lib.vcs import nodes
31 from rhodecode.lib.vcs import nodes
31
32
32 from rhodecode.lib.vcs.conf import settings
33 from rhodecode.lib.vcs.conf import settings
33 from rhodecode.tests import assert_session_flash
34 from rhodecode.tests import assert_session_flash
34 from rhodecode.tests.fixture import Fixture
35 from rhodecode.tests.fixture import Fixture
35 from rhodecode.model.db import Session
36 from rhodecode.model.db import Session
36
37
37 fixture = Fixture()
38 fixture = Fixture()
38
39
39
40
40 def get_node_history(backend_type):
41 def get_node_history(backend_type):
41 return {
42 return {
42 'hg': json.loads(fixture.load_resource('hg_node_history_response.json')),
43 'hg': json.loads(fixture.load_resource('hg_node_history_response.json')),
43 'git': json.loads(fixture.load_resource('git_node_history_response.json')),
44 'git': json.loads(fixture.load_resource('git_node_history_response.json')),
44 'svn': json.loads(fixture.load_resource('svn_node_history_response.json')),
45 'svn': json.loads(fixture.load_resource('svn_node_history_response.json')),
45 }[backend_type]
46 }[backend_type]
46
47
47
48
48 def route_path(name, params=None, **kwargs):
49 def route_path(name, params=None, **kwargs):
49 import urllib
50 import urllib
50
51
51 base_url = {
52 base_url = {
52 'repo_summary': '/{repo_name}',
53 'repo_summary': '/{repo_name}',
53 'repo_archivefile': '/{repo_name}/archive/{fname}',
54 'repo_archivefile': '/{repo_name}/archive/{fname}',
54 'repo_files_diff': '/{repo_name}/diff/{f_path}',
55 'repo_files_diff': '/{repo_name}/diff/{f_path}',
55 'repo_files_diff_2way_redirect': '/{repo_name}/diff-2way/{f_path}',
56 'repo_files_diff_2way_redirect': '/{repo_name}/diff-2way/{f_path}',
56 'repo_files': '/{repo_name}/files/{commit_id}/{f_path}',
57 'repo_files': '/{repo_name}/files/{commit_id}/{f_path}',
57 'repo_files:default_path': '/{repo_name}/files/{commit_id}/',
58 'repo_files:default_path': '/{repo_name}/files/{commit_id}/',
58 'repo_files:default_commit': '/{repo_name}/files',
59 'repo_files:default_commit': '/{repo_name}/files',
59 'repo_files:rendered': '/{repo_name}/render/{commit_id}/{f_path}',
60 'repo_files:rendered': '/{repo_name}/render/{commit_id}/{f_path}',
60 'repo_files:annotated': '/{repo_name}/annotate/{commit_id}/{f_path}',
61 'repo_files:annotated': '/{repo_name}/annotate/{commit_id}/{f_path}',
61 'repo_files:annotated_previous': '/{repo_name}/annotate-previous/{commit_id}/{f_path}',
62 'repo_files:annotated_previous': '/{repo_name}/annotate-previous/{commit_id}/{f_path}',
62 'repo_files_nodelist': '/{repo_name}/nodelist/{commit_id}/{f_path}',
63 'repo_files_nodelist': '/{repo_name}/nodelist/{commit_id}/{f_path}',
63 'repo_file_raw': '/{repo_name}/raw/{commit_id}/{f_path}',
64 'repo_file_raw': '/{repo_name}/raw/{commit_id}/{f_path}',
64 'repo_file_download': '/{repo_name}/download/{commit_id}/{f_path}',
65 'repo_file_download': '/{repo_name}/download/{commit_id}/{f_path}',
65 'repo_file_history': '/{repo_name}/history/{commit_id}/{f_path}',
66 'repo_file_history': '/{repo_name}/history/{commit_id}/{f_path}',
66 'repo_file_authors': '/{repo_name}/authors/{commit_id}/{f_path}',
67 'repo_file_authors': '/{repo_name}/authors/{commit_id}/{f_path}',
67 'repo_files_remove_file': '/{repo_name}/remove_file/{commit_id}/{f_path}',
68 'repo_files_remove_file': '/{repo_name}/remove_file/{commit_id}/{f_path}',
68 'repo_files_delete_file': '/{repo_name}/delete_file/{commit_id}/{f_path}',
69 'repo_files_delete_file': '/{repo_name}/delete_file/{commit_id}/{f_path}',
69 'repo_files_edit_file': '/{repo_name}/edit_file/{commit_id}/{f_path}',
70 'repo_files_edit_file': '/{repo_name}/edit_file/{commit_id}/{f_path}',
70 'repo_files_update_file': '/{repo_name}/update_file/{commit_id}/{f_path}',
71 'repo_files_update_file': '/{repo_name}/update_file/{commit_id}/{f_path}',
71 'repo_files_add_file': '/{repo_name}/add_file/{commit_id}/{f_path}',
72 'repo_files_add_file': '/{repo_name}/add_file/{commit_id}/{f_path}',
72 'repo_files_create_file': '/{repo_name}/create_file/{commit_id}/{f_path}',
73 'repo_files_create_file': '/{repo_name}/create_file/{commit_id}/{f_path}',
73 'repo_nodetree_full': '/{repo_name}/nodetree_full/{commit_id}/{f_path}',
74 'repo_nodetree_full': '/{repo_name}/nodetree_full/{commit_id}/{f_path}',
74 'repo_nodetree_full:default_path': '/{repo_name}/nodetree_full/{commit_id}/',
75 'repo_nodetree_full:default_path': '/{repo_name}/nodetree_full/{commit_id}/',
75 }[name].format(**kwargs)
76 }[name].format(**kwargs)
76
77
77 if params:
78 if params:
78 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
79 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
79 return base_url
80 return base_url
80
81
81
82
82 def assert_files_in_response(response, files, params):
83 def assert_files_in_response(response, files, params):
83 template = (
84 template = (
84 'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"')
85 'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"')
85 _assert_items_in_response(response, files, template, params)
86 _assert_items_in_response(response, files, template, params)
86
87
87
88
88 def assert_dirs_in_response(response, dirs, params):
89 def assert_dirs_in_response(response, dirs, params):
89 template = (
90 template = (
90 'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"')
91 'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"')
91 _assert_items_in_response(response, dirs, template, params)
92 _assert_items_in_response(response, dirs, template, params)
92
93
93
94
94 def _assert_items_in_response(response, items, template, params):
95 def _assert_items_in_response(response, items, template, params):
95 for item in items:
96 for item in items:
96 item_params = {'name': item}
97 item_params = {'name': item}
97 item_params.update(params)
98 item_params.update(params)
98 response.mustcontain(template % item_params)
99 response.mustcontain(template % item_params)
99
100
100
101
101 def assert_timeago_in_response(response, items, params):
102 def assert_timeago_in_response(response, items, params):
102 for item in items:
103 for item in items:
103 response.mustcontain(h.age_component(params['date']))
104 response.mustcontain(h.age_component(params['date']))
104
105
105
106
106 @pytest.mark.usefixtures("app")
107 @pytest.mark.usefixtures("app")
107 class TestFilesViews(object):
108 class TestFilesViews(object):
108
109
109 def test_show_files(self, backend):
110 def test_show_files(self, backend):
110 response = self.app.get(
111 response = self.app.get(
111 route_path('repo_files',
112 route_path('repo_files',
112 repo_name=backend.repo_name,
113 repo_name=backend.repo_name,
113 commit_id='tip', f_path='/'))
114 commit_id='tip', f_path='/'))
114 commit = backend.repo.get_commit()
115 commit = backend.repo.get_commit()
115
116
116 params = {
117 params = {
117 'repo_name': backend.repo_name,
118 'repo_name': backend.repo_name,
118 'commit_id': commit.raw_id,
119 'commit_id': commit.raw_id,
119 'date': commit.date
120 'date': commit.date
120 }
121 }
121 assert_dirs_in_response(response, ['docs', 'vcs'], params)
122 assert_dirs_in_response(response, ['docs', 'vcs'], params)
122 files = [
123 files = [
123 '.gitignore',
124 '.gitignore',
124 '.hgignore',
125 '.hgignore',
125 '.hgtags',
126 '.hgtags',
126 # TODO: missing in Git
127 # TODO: missing in Git
127 # '.travis.yml',
128 # '.travis.yml',
128 'MANIFEST.in',
129 'MANIFEST.in',
129 'README.rst',
130 'README.rst',
130 # TODO: File is missing in svn repository
131 # TODO: File is missing in svn repository
131 # 'run_test_and_report.sh',
132 # 'run_test_and_report.sh',
132 'setup.cfg',
133 'setup.cfg',
133 'setup.py',
134 'setup.py',
134 'test_and_report.sh',
135 'test_and_report.sh',
135 'tox.ini',
136 'tox.ini',
136 ]
137 ]
137 assert_files_in_response(response, files, params)
138 assert_files_in_response(response, files, params)
138 assert_timeago_in_response(response, files, params)
139 assert_timeago_in_response(response, files, params)
139
140
140 def test_show_files_links_submodules_with_absolute_url(self, backend_hg):
141 def test_show_files_links_submodules_with_absolute_url(self, backend_hg):
141 repo = backend_hg['subrepos']
142 repo = backend_hg['subrepos']
142 response = self.app.get(
143 response = self.app.get(
143 route_path('repo_files',
144 route_path('repo_files',
144 repo_name=repo.repo_name,
145 repo_name=repo.repo_name,
145 commit_id='tip', f_path='/'))
146 commit_id='tip', f_path='/'))
146 assert_response = response.assert_response()
147 assert_response = response.assert_response()
147 assert_response.contains_one_link(
148 assert_response.contains_one_link(
148 'absolute-path @ 000000000000', 'http://example.com/absolute-path')
149 'absolute-path @ 000000000000', 'http://example.com/absolute-path')
149
150
150 def test_show_files_links_submodules_with_absolute_url_subpaths(
151 def test_show_files_links_submodules_with_absolute_url_subpaths(
151 self, backend_hg):
152 self, backend_hg):
152 repo = backend_hg['subrepos']
153 repo = backend_hg['subrepos']
153 response = self.app.get(
154 response = self.app.get(
154 route_path('repo_files',
155 route_path('repo_files',
155 repo_name=repo.repo_name,
156 repo_name=repo.repo_name,
156 commit_id='tip', f_path='/'))
157 commit_id='tip', f_path='/'))
157 assert_response = response.assert_response()
158 assert_response = response.assert_response()
158 assert_response.contains_one_link(
159 assert_response.contains_one_link(
159 'subpaths-path @ 000000000000',
160 'subpaths-path @ 000000000000',
160 'http://sub-base.example.com/subpaths-path')
161 'http://sub-base.example.com/subpaths-path')
161
162
162 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
163 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
163 def test_files_menu(self, backend):
164 def test_files_menu(self, backend):
164 new_branch = "temp_branch_name"
165 new_branch = "temp_branch_name"
165 commits = [
166 commits = [
166 {'message': 'a'},
167 {'message': 'a'},
167 {'message': 'b', 'branch': new_branch}
168 {'message': 'b', 'branch': new_branch}
168 ]
169 ]
169 backend.create_repo(commits)
170 backend.create_repo(commits)
170 backend.repo.landing_rev = "branch:%s" % new_branch
171 backend.repo.landing_rev = "branch:%s" % new_branch
171 Session().commit()
172 Session().commit()
172
173
173 # get response based on tip and not new commit
174 # get response based on tip and not new commit
174 response = self.app.get(
175 response = self.app.get(
175 route_path('repo_files',
176 route_path('repo_files',
176 repo_name=backend.repo_name,
177 repo_name=backend.repo_name,
177 commit_id='tip', f_path='/'))
178 commit_id='tip', f_path='/'))
178
179
179 # make sure Files menu url is not tip but new commit
180 # make sure Files menu url is not tip but new commit
180 landing_rev = backend.repo.landing_rev[1]
181 landing_rev = backend.repo.landing_rev[1]
181 files_url = route_path('repo_files:default_path',
182 files_url = route_path('repo_files:default_path',
182 repo_name=backend.repo_name,
183 repo_name=backend.repo_name,
183 commit_id=landing_rev)
184 commit_id=landing_rev)
184
185
185 assert landing_rev != 'tip'
186 assert landing_rev != 'tip'
186 response.mustcontain(
187 response.mustcontain(
187 '<li class="active"><a class="menulink" href="%s">' % files_url)
188 '<li class="active"><a class="menulink" href="%s">' % files_url)
188
189
189 def test_show_files_commit(self, backend):
190 def test_show_files_commit(self, backend):
190 commit = backend.repo.get_commit(commit_idx=32)
191 commit = backend.repo.get_commit(commit_idx=32)
191
192
192 response = self.app.get(
193 response = self.app.get(
193 route_path('repo_files',
194 route_path('repo_files',
194 repo_name=backend.repo_name,
195 repo_name=backend.repo_name,
195 commit_id=commit.raw_id, f_path='/'))
196 commit_id=commit.raw_id, f_path='/'))
196
197
197 dirs = ['docs', 'tests']
198 dirs = ['docs', 'tests']
198 files = ['README.rst']
199 files = ['README.rst']
199 params = {
200 params = {
200 'repo_name': backend.repo_name,
201 'repo_name': backend.repo_name,
201 'commit_id': commit.raw_id,
202 'commit_id': commit.raw_id,
202 }
203 }
203 assert_dirs_in_response(response, dirs, params)
204 assert_dirs_in_response(response, dirs, params)
204 assert_files_in_response(response, files, params)
205 assert_files_in_response(response, files, params)
205
206
206 def test_show_files_different_branch(self, backend):
207 def test_show_files_different_branch(self, backend):
207 branches = dict(
208 branches = dict(
208 hg=(150, ['git']),
209 hg=(150, ['git']),
209 # TODO: Git test repository does not contain other branches
210 # TODO: Git test repository does not contain other branches
210 git=(633, ['master']),
211 git=(633, ['master']),
211 # TODO: Branch support in Subversion
212 # TODO: Branch support in Subversion
212 svn=(150, [])
213 svn=(150, [])
213 )
214 )
214 idx, branches = branches[backend.alias]
215 idx, branches = branches[backend.alias]
215 commit = backend.repo.get_commit(commit_idx=idx)
216 commit = backend.repo.get_commit(commit_idx=idx)
216 response = self.app.get(
217 response = self.app.get(
217 route_path('repo_files',
218 route_path('repo_files',
218 repo_name=backend.repo_name,
219 repo_name=backend.repo_name,
219 commit_id=commit.raw_id, f_path='/'))
220 commit_id=commit.raw_id, f_path='/'))
220
221
221 assert_response = response.assert_response()
222 assert_response = response.assert_response()
222 for branch in branches:
223 for branch in branches:
223 assert_response.element_contains('.tags .branchtag', branch)
224 assert_response.element_contains('.tags .branchtag', branch)
224
225
225 def test_show_files_paging(self, backend):
226 def test_show_files_paging(self, backend):
226 repo = backend.repo
227 repo = backend.repo
227 indexes = [73, 92, 109, 1, 0]
228 indexes = [73, 92, 109, 1, 0]
228 idx_map = [(rev, repo.get_commit(commit_idx=rev).raw_id)
229 idx_map = [(rev, repo.get_commit(commit_idx=rev).raw_id)
229 for rev in indexes]
230 for rev in indexes]
230
231
231 for idx in idx_map:
232 for idx in idx_map:
232 response = self.app.get(
233 response = self.app.get(
233 route_path('repo_files',
234 route_path('repo_files',
234 repo_name=backend.repo_name,
235 repo_name=backend.repo_name,
235 commit_id=idx[1], f_path='/'))
236 commit_id=idx[1], f_path='/'))
236
237
237 response.mustcontain("""r%s:%s""" % (idx[0], idx[1][:8]))
238 response.mustcontain("""r%s:%s""" % (idx[0], idx[1][:8]))
238
239
239 def test_file_source(self, backend):
240 def test_file_source(self, backend):
240 commit = backend.repo.get_commit(commit_idx=167)
241 commit = backend.repo.get_commit(commit_idx=167)
241 response = self.app.get(
242 response = self.app.get(
242 route_path('repo_files',
243 route_path('repo_files',
243 repo_name=backend.repo_name,
244 repo_name=backend.repo_name,
244 commit_id=commit.raw_id, f_path='vcs/nodes.py'))
245 commit_id=commit.raw_id, f_path='vcs/nodes.py'))
245
246
246 msgbox = """<div class="commit">%s</div>"""
247 msgbox = """<div class="commit">%s</div>"""
247 response.mustcontain(msgbox % (commit.message, ))
248 response.mustcontain(msgbox % (commit.message, ))
248
249
249 assert_response = response.assert_response()
250 assert_response = response.assert_response()
250 if commit.branch:
251 if commit.branch:
251 assert_response.element_contains(
252 assert_response.element_contains(
252 '.tags.tags-main .branchtag', commit.branch)
253 '.tags.tags-main .branchtag', commit.branch)
253 if commit.tags:
254 if commit.tags:
254 for tag in commit.tags:
255 for tag in commit.tags:
255 assert_response.element_contains('.tags.tags-main .tagtag', tag)
256 assert_response.element_contains('.tags.tags-main .tagtag', tag)
256
257
257 def test_file_source_annotated(self, backend):
258 def test_file_source_annotated(self, backend):
258 response = self.app.get(
259 response = self.app.get(
259 route_path('repo_files:annotated',
260 route_path('repo_files:annotated',
260 repo_name=backend.repo_name,
261 repo_name=backend.repo_name,
261 commit_id='tip', f_path='vcs/nodes.py'))
262 commit_id='tip', f_path='vcs/nodes.py'))
262 expected_commits = {
263 expected_commits = {
263 'hg': 'r356',
264 'hg': 'r356',
264 'git': 'r345',
265 'git': 'r345',
265 'svn': 'r208',
266 'svn': 'r208',
266 }
267 }
267 response.mustcontain(expected_commits[backend.alias])
268 response.mustcontain(expected_commits[backend.alias])
268
269
269 def test_file_source_authors(self, backend):
270 def test_file_source_authors(self, backend):
270 response = self.app.get(
271 response = self.app.get(
271 route_path('repo_file_authors',
272 route_path('repo_file_authors',
272 repo_name=backend.repo_name,
273 repo_name=backend.repo_name,
273 commit_id='tip', f_path='vcs/nodes.py'))
274 commit_id='tip', f_path='vcs/nodes.py'))
274 expected_authors = {
275 expected_authors = {
275 'hg': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
276 'hg': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
276 'git': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
277 'git': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
277 'svn': ('marcin', 'lukasz'),
278 'svn': ('marcin', 'lukasz'),
278 }
279 }
279
280
280 for author in expected_authors[backend.alias]:
281 for author in expected_authors[backend.alias]:
281 response.mustcontain(author)
282 response.mustcontain(author)
282
283
283 def test_file_source_authors_with_annotation(self, backend):
284 def test_file_source_authors_with_annotation(self, backend):
284 response = self.app.get(
285 response = self.app.get(
285 route_path('repo_file_authors',
286 route_path('repo_file_authors',
286 repo_name=backend.repo_name,
287 repo_name=backend.repo_name,
287 commit_id='tip', f_path='vcs/nodes.py',
288 commit_id='tip', f_path='vcs/nodes.py',
288 params=dict(annotate=1)))
289 params=dict(annotate=1)))
289 expected_authors = {
290 expected_authors = {
290 'hg': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
291 'hg': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
291 'git': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
292 'git': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
292 'svn': ('marcin', 'lukasz'),
293 'svn': ('marcin', 'lukasz'),
293 }
294 }
294
295
295 for author in expected_authors[backend.alias]:
296 for author in expected_authors[backend.alias]:
296 response.mustcontain(author)
297 response.mustcontain(author)
297
298
298 def test_file_source_history(self, backend, xhr_header):
299 def test_file_source_history(self, backend, xhr_header):
299 response = self.app.get(
300 response = self.app.get(
300 route_path('repo_file_history',
301 route_path('repo_file_history',
301 repo_name=backend.repo_name,
302 repo_name=backend.repo_name,
302 commit_id='tip', f_path='vcs/nodes.py'),
303 commit_id='tip', f_path='vcs/nodes.py'),
303 extra_environ=xhr_header)
304 extra_environ=xhr_header)
304 assert get_node_history(backend.alias) == json.loads(response.body)
305 assert get_node_history(backend.alias) == json.loads(response.body)
305
306
306 def test_file_source_history_svn(self, backend_svn, xhr_header):
307 def test_file_source_history_svn(self, backend_svn, xhr_header):
307 simple_repo = backend_svn['svn-simple-layout']
308 simple_repo = backend_svn['svn-simple-layout']
308 response = self.app.get(
309 response = self.app.get(
309 route_path('repo_file_history',
310 route_path('repo_file_history',
310 repo_name=simple_repo.repo_name,
311 repo_name=simple_repo.repo_name,
311 commit_id='tip', f_path='trunk/example.py'),
312 commit_id='tip', f_path='trunk/example.py'),
312 extra_environ=xhr_header)
313 extra_environ=xhr_header)
313
314
314 expected_data = json.loads(
315 expected_data = json.loads(
315 fixture.load_resource('svn_node_history_branches.json'))
316 fixture.load_resource('svn_node_history_branches.json'))
316
317
317 assert expected_data == response.json
318 assert expected_data == response.json
318
319
319 def test_file_source_history_with_annotation(self, backend, xhr_header):
320 def test_file_source_history_with_annotation(self, backend, xhr_header):
320 response = self.app.get(
321 response = self.app.get(
321 route_path('repo_file_history',
322 route_path('repo_file_history',
322 repo_name=backend.repo_name,
323 repo_name=backend.repo_name,
323 commit_id='tip', f_path='vcs/nodes.py',
324 commit_id='tip', f_path='vcs/nodes.py',
324 params=dict(annotate=1)),
325 params=dict(annotate=1)),
325
326
326 extra_environ=xhr_header)
327 extra_environ=xhr_header)
327 assert get_node_history(backend.alias) == json.loads(response.body)
328 assert get_node_history(backend.alias) == json.loads(response.body)
328
329
329 def test_tree_search_top_level(self, backend, xhr_header):
330 def test_tree_search_top_level(self, backend, xhr_header):
330 commit = backend.repo.get_commit(commit_idx=173)
331 commit = backend.repo.get_commit(commit_idx=173)
331 response = self.app.get(
332 response = self.app.get(
332 route_path('repo_files_nodelist',
333 route_path('repo_files_nodelist',
333 repo_name=backend.repo_name,
334 repo_name=backend.repo_name,
334 commit_id=commit.raw_id, f_path='/'),
335 commit_id=commit.raw_id, f_path='/'),
335 extra_environ=xhr_header)
336 extra_environ=xhr_header)
336 assert 'nodes' in response.json
337 assert 'nodes' in response.json
337 assert {'name': 'docs', 'type': 'dir'} in response.json['nodes']
338 assert {'name': 'docs', 'type': 'dir'} in response.json['nodes']
338
339
339 def test_tree_search_missing_xhr(self, backend):
340 def test_tree_search_missing_xhr(self, backend):
340 self.app.get(
341 self.app.get(
341 route_path('repo_files_nodelist',
342 route_path('repo_files_nodelist',
342 repo_name=backend.repo_name,
343 repo_name=backend.repo_name,
343 commit_id='tip', f_path='/'),
344 commit_id='tip', f_path='/'),
344 status=404)
345 status=404)
345
346
346 def test_tree_search_at_path(self, backend, xhr_header):
347 def test_tree_search_at_path(self, backend, xhr_header):
347 commit = backend.repo.get_commit(commit_idx=173)
348 commit = backend.repo.get_commit(commit_idx=173)
348 response = self.app.get(
349 response = self.app.get(
349 route_path('repo_files_nodelist',
350 route_path('repo_files_nodelist',
350 repo_name=backend.repo_name,
351 repo_name=backend.repo_name,
351 commit_id=commit.raw_id, f_path='/docs'),
352 commit_id=commit.raw_id, f_path='/docs'),
352 extra_environ=xhr_header)
353 extra_environ=xhr_header)
353 assert 'nodes' in response.json
354 assert 'nodes' in response.json
354 nodes = response.json['nodes']
355 nodes = response.json['nodes']
355 assert {'name': 'docs/api', 'type': 'dir'} in nodes
356 assert {'name': 'docs/api', 'type': 'dir'} in nodes
356 assert {'name': 'docs/index.rst', 'type': 'file'} in nodes
357 assert {'name': 'docs/index.rst', 'type': 'file'} in nodes
357
358
358 def test_tree_search_at_path_2nd_level(self, backend, xhr_header):
359 def test_tree_search_at_path_2nd_level(self, backend, xhr_header):
359 commit = backend.repo.get_commit(commit_idx=173)
360 commit = backend.repo.get_commit(commit_idx=173)
360 response = self.app.get(
361 response = self.app.get(
361 route_path('repo_files_nodelist',
362 route_path('repo_files_nodelist',
362 repo_name=backend.repo_name,
363 repo_name=backend.repo_name,
363 commit_id=commit.raw_id, f_path='/docs/api'),
364 commit_id=commit.raw_id, f_path='/docs/api'),
364 extra_environ=xhr_header)
365 extra_environ=xhr_header)
365 assert 'nodes' in response.json
366 assert 'nodes' in response.json
366 nodes = response.json['nodes']
367 nodes = response.json['nodes']
367 assert {'name': 'docs/api/index.rst', 'type': 'file'} in nodes
368 assert {'name': 'docs/api/index.rst', 'type': 'file'} in nodes
368
369
369 def test_tree_search_at_path_missing_xhr(self, backend):
370 def test_tree_search_at_path_missing_xhr(self, backend):
370 self.app.get(
371 self.app.get(
371 route_path('repo_files_nodelist',
372 route_path('repo_files_nodelist',
372 repo_name=backend.repo_name,
373 repo_name=backend.repo_name,
373 commit_id='tip', f_path='/docs'),
374 commit_id='tip', f_path='/docs'),
374 status=404)
375 status=404)
375
376
376 def test_nodetree(self, backend, xhr_header):
377 def test_nodetree(self, backend, xhr_header):
377 commit = backend.repo.get_commit(commit_idx=173)
378 commit = backend.repo.get_commit(commit_idx=173)
378 response = self.app.get(
379 response = self.app.get(
379 route_path('repo_nodetree_full',
380 route_path('repo_nodetree_full',
380 repo_name=backend.repo_name,
381 repo_name=backend.repo_name,
381 commit_id=commit.raw_id, f_path='/'),
382 commit_id=commit.raw_id, f_path='/'),
382 extra_environ=xhr_header)
383 extra_environ=xhr_header)
383
384
384 assert_response = response.assert_response()
385 assert_response = response.assert_response()
385
386
386 for attr in ['data-commit-id', 'data-date', 'data-author']:
387 for attr in ['data-commit-id', 'data-date', 'data-author']:
387 elements = assert_response.get_elements('[{}]'.format(attr))
388 elements = assert_response.get_elements('[{}]'.format(attr))
388 assert len(elements) > 1
389 assert len(elements) > 1
389
390
390 for element in elements:
391 for element in elements:
391 assert element.get(attr)
392 assert element.get(attr)
392
393
393 def test_nodetree_if_file(self, backend, xhr_header):
394 def test_nodetree_if_file(self, backend, xhr_header):
394 commit = backend.repo.get_commit(commit_idx=173)
395 commit = backend.repo.get_commit(commit_idx=173)
395 response = self.app.get(
396 response = self.app.get(
396 route_path('repo_nodetree_full',
397 route_path('repo_nodetree_full',
397 repo_name=backend.repo_name,
398 repo_name=backend.repo_name,
398 commit_id=commit.raw_id, f_path='README.rst'),
399 commit_id=commit.raw_id, f_path='README.rst'),
399 extra_environ=xhr_header)
400 extra_environ=xhr_header)
400 assert response.body == ''
401 assert response.body == ''
401
402
402 def test_nodetree_wrong_path(self, backend, xhr_header):
403 def test_nodetree_wrong_path(self, backend, xhr_header):
403 commit = backend.repo.get_commit(commit_idx=173)
404 commit = backend.repo.get_commit(commit_idx=173)
404 response = self.app.get(
405 response = self.app.get(
405 route_path('repo_nodetree_full',
406 route_path('repo_nodetree_full',
406 repo_name=backend.repo_name,
407 repo_name=backend.repo_name,
407 commit_id=commit.raw_id, f_path='/dont-exist'),
408 commit_id=commit.raw_id, f_path='/dont-exist'),
408 extra_environ=xhr_header)
409 extra_environ=xhr_header)
409
410
410 err = 'error: There is no file nor ' \
411 err = 'error: There is no file nor ' \
411 'directory at the given path'
412 'directory at the given path'
412 assert err in response.body
413 assert err in response.body
413
414
414 def test_nodetree_missing_xhr(self, backend):
415 def test_nodetree_missing_xhr(self, backend):
415 self.app.get(
416 self.app.get(
416 route_path('repo_nodetree_full',
417 route_path('repo_nodetree_full',
417 repo_name=backend.repo_name,
418 repo_name=backend.repo_name,
418 commit_id='tip', f_path='/'),
419 commit_id='tip', f_path='/'),
419 status=404)
420 status=404)
420
421
421
422
422 @pytest.mark.usefixtures("app", "autologin_user")
423 @pytest.mark.usefixtures("app", "autologin_user")
423 class TestRawFileHandling(object):
424 class TestRawFileHandling(object):
424
425
425 def test_download_file(self, backend):
426 def test_download_file(self, backend):
426 commit = backend.repo.get_commit(commit_idx=173)
427 commit = backend.repo.get_commit(commit_idx=173)
427 response = self.app.get(
428 response = self.app.get(
428 route_path('repo_file_download',
429 route_path('repo_file_download',
429 repo_name=backend.repo_name,
430 repo_name=backend.repo_name,
430 commit_id=commit.raw_id, f_path='vcs/nodes.py'),)
431 commit_id=commit.raw_id, f_path='vcs/nodes.py'),)
431
432
432 assert response.content_disposition == 'attachment; filename="nodes.py"; filename*=UTF-8\'\'nodes.py'
433 assert response.content_disposition == 'attachment; filename="nodes.py"; filename*=UTF-8\'\'nodes.py'
433 assert response.content_type == "text/x-python"
434 assert response.content_type == "text/x-python"
434
435
435 def test_download_file_wrong_cs(self, backend):
436 def test_download_file_wrong_cs(self, backend):
436 raw_id = u'ERRORce30c96924232dffcd24178a07ffeb5dfc'
437 raw_id = u'ERRORce30c96924232dffcd24178a07ffeb5dfc'
437
438
438 response = self.app.get(
439 response = self.app.get(
439 route_path('repo_file_download',
440 route_path('repo_file_download',
440 repo_name=backend.repo_name,
441 repo_name=backend.repo_name,
441 commit_id=raw_id, f_path='vcs/nodes.svg'),
442 commit_id=raw_id, f_path='vcs/nodes.svg'),
442 status=404)
443 status=404)
443
444
444 msg = """No such commit exists for this repository"""
445 msg = """No such commit exists for this repository"""
445 response.mustcontain(msg)
446 response.mustcontain(msg)
446
447
447 def test_download_file_wrong_f_path(self, backend):
448 def test_download_file_wrong_f_path(self, backend):
448 commit = backend.repo.get_commit(commit_idx=173)
449 commit = backend.repo.get_commit(commit_idx=173)
449 f_path = 'vcs/ERRORnodes.py'
450 f_path = 'vcs/ERRORnodes.py'
450
451
451 response = self.app.get(
452 response = self.app.get(
452 route_path('repo_file_download',
453 route_path('repo_file_download',
453 repo_name=backend.repo_name,
454 repo_name=backend.repo_name,
454 commit_id=commit.raw_id, f_path=f_path),
455 commit_id=commit.raw_id, f_path=f_path),
455 status=404)
456 status=404)
456
457
457 msg = (
458 msg = (
458 "There is no file nor directory at the given path: "
459 "There is no file nor directory at the given path: "
459 "`%s` at commit %s" % (f_path, commit.short_id))
460 "`%s` at commit %s" % (f_path, commit.short_id))
460 response.mustcontain(msg)
461 response.mustcontain(msg)
461
462
462 def test_file_raw(self, backend):
463 def test_file_raw(self, backend):
463 commit = backend.repo.get_commit(commit_idx=173)
464 commit = backend.repo.get_commit(commit_idx=173)
464 response = self.app.get(
465 response = self.app.get(
465 route_path('repo_file_raw',
466 route_path('repo_file_raw',
466 repo_name=backend.repo_name,
467 repo_name=backend.repo_name,
467 commit_id=commit.raw_id, f_path='vcs/nodes.py'),)
468 commit_id=commit.raw_id, f_path='vcs/nodes.py'),)
468
469
469 assert response.content_type == "text/plain"
470 assert response.content_type == "text/plain"
470
471
471 def test_file_raw_binary(self, backend):
472 def test_file_raw_binary(self, backend):
472 commit = backend.repo.get_commit()
473 commit = backend.repo.get_commit()
473 response = self.app.get(
474 response = self.app.get(
474 route_path('repo_file_raw',
475 route_path('repo_file_raw',
475 repo_name=backend.repo_name,
476 repo_name=backend.repo_name,
476 commit_id=commit.raw_id,
477 commit_id=commit.raw_id,
477 f_path='docs/theme/ADC/static/breadcrumb_background.png'),)
478 f_path='docs/theme/ADC/static/breadcrumb_background.png'),)
478
479
479 assert response.content_disposition == 'inline'
480 assert response.content_disposition == 'inline'
480
481
481 def test_raw_file_wrong_cs(self, backend):
482 def test_raw_file_wrong_cs(self, backend):
482 raw_id = u'ERRORcce30c96924232dffcd24178a07ffeb5dfc'
483 raw_id = u'ERRORcce30c96924232dffcd24178a07ffeb5dfc'
483
484
484 response = self.app.get(
485 response = self.app.get(
485 route_path('repo_file_raw',
486 route_path('repo_file_raw',
486 repo_name=backend.repo_name,
487 repo_name=backend.repo_name,
487 commit_id=raw_id, f_path='vcs/nodes.svg'),
488 commit_id=raw_id, f_path='vcs/nodes.svg'),
488 status=404)
489 status=404)
489
490
490 msg = """No such commit exists for this repository"""
491 msg = """No such commit exists for this repository"""
491 response.mustcontain(msg)
492 response.mustcontain(msg)
492
493
493 def test_raw_wrong_f_path(self, backend):
494 def test_raw_wrong_f_path(self, backend):
494 commit = backend.repo.get_commit(commit_idx=173)
495 commit = backend.repo.get_commit(commit_idx=173)
495 f_path = 'vcs/ERRORnodes.py'
496 f_path = 'vcs/ERRORnodes.py'
496 response = self.app.get(
497 response = self.app.get(
497 route_path('repo_file_raw',
498 route_path('repo_file_raw',
498 repo_name=backend.repo_name,
499 repo_name=backend.repo_name,
499 commit_id=commit.raw_id, f_path=f_path),
500 commit_id=commit.raw_id, f_path=f_path),
500 status=404)
501 status=404)
501
502
502 msg = (
503 msg = (
503 "There is no file nor directory at the given path: "
504 "There is no file nor directory at the given path: "
504 "`%s` at commit %s" % (f_path, commit.short_id))
505 "`%s` at commit %s" % (f_path, commit.short_id))
505 response.mustcontain(msg)
506 response.mustcontain(msg)
506
507
507 def test_raw_svg_should_not_be_rendered(self, backend):
508 def test_raw_svg_should_not_be_rendered(self, backend):
508 backend.create_repo()
509 backend.create_repo()
509 backend.ensure_file("xss.svg")
510 backend.ensure_file("xss.svg")
510 response = self.app.get(
511 response = self.app.get(
511 route_path('repo_file_raw',
512 route_path('repo_file_raw',
512 repo_name=backend.repo_name,
513 repo_name=backend.repo_name,
513 commit_id='tip', f_path='xss.svg'),)
514 commit_id='tip', f_path='xss.svg'),)
514 # If the content type is image/svg+xml then it allows to render HTML
515 # If the content type is image/svg+xml then it allows to render HTML
515 # and malicious SVG.
516 # and malicious SVG.
516 assert response.content_type == "text/plain"
517 assert response.content_type == "text/plain"
517
518
518
519
519 @pytest.mark.usefixtures("app")
520 @pytest.mark.usefixtures("app")
520 class TestRepositoryArchival(object):
521 class TestRepositoryArchival(object):
521
522
522 def test_archival(self, backend):
523 def test_archival(self, backend):
523 backend.enable_downloads()
524 backend.enable_downloads()
524 commit = backend.repo.get_commit(commit_idx=173)
525 commit = backend.repo.get_commit(commit_idx=173)
525 for a_type, content_type, extension in settings.ARCHIVE_SPECS:
526 for a_type, content_type, extension in settings.ARCHIVE_SPECS:
526
527
527 short = commit.short_id + extension
528 short = commit.short_id + extension
528 fname = commit.raw_id + extension
529 fname = commit.raw_id + extension
529 filename = '%s-%s' % (backend.repo_name, short)
530 filename = '%s-%s' % (backend.repo_name, short)
530 response = self.app.get(
531 response = self.app.get(
531 route_path('repo_archivefile',
532 route_path('repo_archivefile',
532 repo_name=backend.repo_name,
533 repo_name=backend.repo_name,
533 fname=fname))
534 fname=fname))
534
535
535 assert response.status == '200 OK'
536 assert response.status == '200 OK'
536 headers = [
537 headers = [
537 ('Content-Disposition', 'attachment; filename=%s' % filename),
538 ('Content-Disposition', 'attachment; filename=%s' % filename),
538 ('Content-Type', '%s' % content_type),
539 ('Content-Type', '%s' % content_type),
539 ]
540 ]
540
541
541 for header in headers:
542 for header in headers:
542 assert header in response.headers.items()
543 assert header in response.headers.items()
543
544
544 @pytest.mark.parametrize('arch_ext',[
545 @pytest.mark.parametrize('arch_ext',[
545 'tar', 'rar', 'x', '..ax', '.zipz', 'tar.gz.tar'])
546 'tar', 'rar', 'x', '..ax', '.zipz', 'tar.gz.tar'])
546 def test_archival_wrong_ext(self, backend, arch_ext):
547 def test_archival_wrong_ext(self, backend, arch_ext):
547 backend.enable_downloads()
548 backend.enable_downloads()
548 commit = backend.repo.get_commit(commit_idx=173)
549 commit = backend.repo.get_commit(commit_idx=173)
549
550
550 fname = commit.raw_id + '.' + arch_ext
551 fname = commit.raw_id + '.' + arch_ext
551
552
552 response = self.app.get(
553 response = self.app.get(
553 route_path('repo_archivefile',
554 route_path('repo_archivefile',
554 repo_name=backend.repo_name,
555 repo_name=backend.repo_name,
555 fname=fname))
556 fname=fname))
556 response.mustcontain(
557 response.mustcontain(
557 'Unknown archive type for: `{}`'.format(fname))
558 'Unknown archive type for: `{}`'.format(fname))
558
559
559 @pytest.mark.parametrize('commit_id', [
560 @pytest.mark.parametrize('commit_id', [
560 '00x000000', 'tar', 'wrong', '@$@$42413232', '232dffcd'])
561 '00x000000', 'tar', 'wrong', '@$@$42413232', '232dffcd'])
561 def test_archival_wrong_commit_id(self, backend, commit_id):
562 def test_archival_wrong_commit_id(self, backend, commit_id):
562 backend.enable_downloads()
563 backend.enable_downloads()
563 fname = '%s.zip' % commit_id
564 fname = '%s.zip' % commit_id
564
565
565 response = self.app.get(
566 response = self.app.get(
566 route_path('repo_archivefile',
567 route_path('repo_archivefile',
567 repo_name=backend.repo_name,
568 repo_name=backend.repo_name,
568 fname=fname))
569 fname=fname))
569 response.mustcontain('Unknown commit_id')
570 response.mustcontain('Unknown commit_id')
570
571
571
572
572 @pytest.mark.usefixtures("app")
573 @pytest.mark.usefixtures("app")
573 class TestFilesDiff(object):
574 class TestFilesDiff(object):
574
575
575 @pytest.mark.parametrize("diff", ['diff', 'download', 'raw'])
576 @pytest.mark.parametrize("diff", ['diff', 'download', 'raw'])
576 def test_file_full_diff(self, backend, diff):
577 def test_file_full_diff(self, backend, diff):
577 commit1 = backend.repo.get_commit(commit_idx=-1)
578 commit1 = backend.repo.get_commit(commit_idx=-1)
578 commit2 = backend.repo.get_commit(commit_idx=-2)
579 commit2 = backend.repo.get_commit(commit_idx=-2)
579
580
580 response = self.app.get(
581 response = self.app.get(
581 route_path('repo_files_diff',
582 route_path('repo_files_diff',
582 repo_name=backend.repo_name,
583 repo_name=backend.repo_name,
583 f_path='README'),
584 f_path='README'),
584 params={
585 params={
585 'diff1': commit2.raw_id,
586 'diff1': commit2.raw_id,
586 'diff2': commit1.raw_id,
587 'diff2': commit1.raw_id,
587 'fulldiff': '1',
588 'fulldiff': '1',
588 'diff': diff,
589 'diff': diff,
589 })
590 })
590
591
591 if diff == 'diff':
592 if diff == 'diff':
592 # use redirect since this is OLD view redirecting to compare page
593 # use redirect since this is OLD view redirecting to compare page
593 response = response.follow()
594 response = response.follow()
594
595
595 # It's a symlink to README.rst
596 # It's a symlink to README.rst
596 response.mustcontain('README.rst')
597 response.mustcontain('README.rst')
597 response.mustcontain('No newline at end of file')
598 response.mustcontain('No newline at end of file')
598
599
599 def test_file_binary_diff(self, backend):
600 def test_file_binary_diff(self, backend):
600 commits = [
601 commits = [
601 {'message': 'First commit'},
602 {'message': 'First commit'},
602 {'message': 'Commit with binary',
603 {'message': 'Commit with binary',
603 'added': [nodes.FileNode('file.bin', content='\0BINARY\0')]},
604 'added': [nodes.FileNode('file.bin', content='\0BINARY\0')]},
604 ]
605 ]
605 repo = backend.create_repo(commits=commits)
606 repo = backend.create_repo(commits=commits)
606
607
607 response = self.app.get(
608 response = self.app.get(
608 route_path('repo_files_diff',
609 route_path('repo_files_diff',
609 repo_name=backend.repo_name,
610 repo_name=backend.repo_name,
610 f_path='file.bin'),
611 f_path='file.bin'),
611 params={
612 params={
612 'diff1': repo.get_commit(commit_idx=0).raw_id,
613 'diff1': repo.get_commit(commit_idx=0).raw_id,
613 'diff2': repo.get_commit(commit_idx=1).raw_id,
614 'diff2': repo.get_commit(commit_idx=1).raw_id,
614 'fulldiff': '1',
615 'fulldiff': '1',
615 'diff': 'diff',
616 'diff': 'diff',
616 })
617 })
617 # use redirect since this is OLD view redirecting to compare page
618 # use redirect since this is OLD view redirecting to compare page
618 response = response.follow()
619 response = response.follow()
619 response.mustcontain('Expand 1 commit')
620 response.mustcontain('Expand 1 commit')
620 response.mustcontain('1 file changed: 0 inserted, 0 deleted')
621 file_changes = (1, 0, 0)
622
623 compare_page = ComparePage(response)
624 compare_page.contains_change_summary(*file_changes)
621
625
622 if backend.alias == 'svn':
626 if backend.alias == 'svn':
623 response.mustcontain('new file 10644')
627 response.mustcontain('new file 10644')
624 # TODO(marcink): SVN doesn't yet detect binary changes
628 # TODO(marcink): SVN doesn't yet detect binary changes
625 else:
629 else:
626 response.mustcontain('new file 100644')
630 response.mustcontain('new file 100644')
627 response.mustcontain('binary diff hidden')
631 response.mustcontain('binary diff hidden')
628
632
629 def test_diff_2way(self, backend):
633 def test_diff_2way(self, backend):
630 commit1 = backend.repo.get_commit(commit_idx=-1)
634 commit1 = backend.repo.get_commit(commit_idx=-1)
631 commit2 = backend.repo.get_commit(commit_idx=-2)
635 commit2 = backend.repo.get_commit(commit_idx=-2)
632 response = self.app.get(
636 response = self.app.get(
633 route_path('repo_files_diff_2way_redirect',
637 route_path('repo_files_diff_2way_redirect',
634 repo_name=backend.repo_name,
638 repo_name=backend.repo_name,
635 f_path='README'),
639 f_path='README'),
636 params={
640 params={
637 'diff1': commit2.raw_id,
641 'diff1': commit2.raw_id,
638 'diff2': commit1.raw_id,
642 'diff2': commit1.raw_id,
639 })
643 })
640 # use redirect since this is OLD view redirecting to compare page
644 # use redirect since this is OLD view redirecting to compare page
641 response = response.follow()
645 response = response.follow()
642
646
643 # It's a symlink to README.rst
647 # It's a symlink to README.rst
644 response.mustcontain('README.rst')
648 response.mustcontain('README.rst')
645 response.mustcontain('No newline at end of file')
649 response.mustcontain('No newline at end of file')
646
650
647 def test_requires_one_commit_id(self, backend, autologin_user):
651 def test_requires_one_commit_id(self, backend, autologin_user):
648 response = self.app.get(
652 response = self.app.get(
649 route_path('repo_files_diff',
653 route_path('repo_files_diff',
650 repo_name=backend.repo_name,
654 repo_name=backend.repo_name,
651 f_path='README.rst'),
655 f_path='README.rst'),
652 status=400)
656 status=400)
653 response.mustcontain(
657 response.mustcontain(
654 'Need query parameter', 'diff1', 'diff2', 'to generate a diff.')
658 'Need query parameter', 'diff1', 'diff2', 'to generate a diff.')
655
659
656 def test_returns_no_files_if_file_does_not_exist(self, vcsbackend):
660 def test_returns_no_files_if_file_does_not_exist(self, vcsbackend):
657 repo = vcsbackend.repo
661 repo = vcsbackend.repo
658 response = self.app.get(
662 response = self.app.get(
659 route_path('repo_files_diff',
663 route_path('repo_files_diff',
660 repo_name=repo.name,
664 repo_name=repo.name,
661 f_path='does-not-exist-in-any-commit'),
665 f_path='does-not-exist-in-any-commit'),
662 params={
666 params={
663 'diff1': repo[0].raw_id,
667 'diff1': repo[0].raw_id,
664 'diff2': repo[1].raw_id
668 'diff2': repo[1].raw_id
665 })
669 })
666
670
667 response = response.follow()
671 response = response.follow()
668 response.mustcontain('No files')
672 response.mustcontain('No files')
669
673
670 def test_returns_redirect_if_file_not_changed(self, backend):
674 def test_returns_redirect_if_file_not_changed(self, backend):
671 commit = backend.repo.get_commit(commit_idx=-1)
675 commit = backend.repo.get_commit(commit_idx=-1)
672 response = self.app.get(
676 response = self.app.get(
673 route_path('repo_files_diff_2way_redirect',
677 route_path('repo_files_diff_2way_redirect',
674 repo_name=backend.repo_name,
678 repo_name=backend.repo_name,
675 f_path='README'),
679 f_path='README'),
676 params={
680 params={
677 'diff1': commit.raw_id,
681 'diff1': commit.raw_id,
678 'diff2': commit.raw_id,
682 'diff2': commit.raw_id,
679 })
683 })
680
684
681 response = response.follow()
685 response = response.follow()
682 response.mustcontain('No files')
686 response.mustcontain('No files')
683 response.mustcontain('No commits in this compare')
687 response.mustcontain('No commits in this compare')
684
688
685 def test_supports_diff_to_different_path_svn(self, backend_svn):
689 def test_supports_diff_to_different_path_svn(self, backend_svn):
686 #TODO: check this case
690 #TODO: check this case
687 return
691 return
688
692
689 repo = backend_svn['svn-simple-layout'].scm_instance()
693 repo = backend_svn['svn-simple-layout'].scm_instance()
690 commit_id_1 = '24'
694 commit_id_1 = '24'
691 commit_id_2 = '26'
695 commit_id_2 = '26'
692
696
693 response = self.app.get(
697 response = self.app.get(
694 route_path('repo_files_diff',
698 route_path('repo_files_diff',
695 repo_name=backend_svn.repo_name,
699 repo_name=backend_svn.repo_name,
696 f_path='trunk/example.py'),
700 f_path='trunk/example.py'),
697 params={
701 params={
698 'diff1': 'tags/v0.2/example.py@' + commit_id_1,
702 'diff1': 'tags/v0.2/example.py@' + commit_id_1,
699 'diff2': commit_id_2,
703 'diff2': commit_id_2,
700 })
704 })
701
705
702 response = response.follow()
706 response = response.follow()
703 response.mustcontain(
707 response.mustcontain(
704 # diff contains this
708 # diff contains this
705 "Will print out a useful message on invocation.")
709 "Will print out a useful message on invocation.")
706
710
707 # Note: Expecting that we indicate the user what's being compared
711 # Note: Expecting that we indicate the user what's being compared
708 response.mustcontain("trunk/example.py")
712 response.mustcontain("trunk/example.py")
709 response.mustcontain("tags/v0.2/example.py")
713 response.mustcontain("tags/v0.2/example.py")
710
714
711 def test_show_rev_redirects_to_svn_path(self, backend_svn):
715 def test_show_rev_redirects_to_svn_path(self, backend_svn):
712 #TODO: check this case
716 #TODO: check this case
713 return
717 return
714
718
715 repo = backend_svn['svn-simple-layout'].scm_instance()
719 repo = backend_svn['svn-simple-layout'].scm_instance()
716 commit_id = repo[-1].raw_id
720 commit_id = repo[-1].raw_id
717
721
718 response = self.app.get(
722 response = self.app.get(
719 route_path('repo_files_diff',
723 route_path('repo_files_diff',
720 repo_name=backend_svn.repo_name,
724 repo_name=backend_svn.repo_name,
721 f_path='trunk/example.py'),
725 f_path='trunk/example.py'),
722 params={
726 params={
723 'diff1': 'branches/argparse/example.py@' + commit_id,
727 'diff1': 'branches/argparse/example.py@' + commit_id,
724 'diff2': commit_id,
728 'diff2': commit_id,
725 },
729 },
726 status=302)
730 status=302)
727 response = response.follow()
731 response = response.follow()
728 assert response.headers['Location'].endswith(
732 assert response.headers['Location'].endswith(
729 'svn-svn-simple-layout/files/26/branches/argparse/example.py')
733 'svn-svn-simple-layout/files/26/branches/argparse/example.py')
730
734
731 def test_show_rev_and_annotate_redirects_to_svn_path(self, backend_svn):
735 def test_show_rev_and_annotate_redirects_to_svn_path(self, backend_svn):
732 #TODO: check this case
736 #TODO: check this case
733 return
737 return
734
738
735 repo = backend_svn['svn-simple-layout'].scm_instance()
739 repo = backend_svn['svn-simple-layout'].scm_instance()
736 commit_id = repo[-1].raw_id
740 commit_id = repo[-1].raw_id
737 response = self.app.get(
741 response = self.app.get(
738 route_path('repo_files_diff',
742 route_path('repo_files_diff',
739 repo_name=backend_svn.repo_name,
743 repo_name=backend_svn.repo_name,
740 f_path='trunk/example.py'),
744 f_path='trunk/example.py'),
741 params={
745 params={
742 'diff1': 'branches/argparse/example.py@' + commit_id,
746 'diff1': 'branches/argparse/example.py@' + commit_id,
743 'diff2': commit_id,
747 'diff2': commit_id,
744 'show_rev': 'Show at Revision',
748 'show_rev': 'Show at Revision',
745 'annotate': 'true',
749 'annotate': 'true',
746 },
750 },
747 status=302)
751 status=302)
748 response = response.follow()
752 response = response.follow()
749 assert response.headers['Location'].endswith(
753 assert response.headers['Location'].endswith(
750 'svn-svn-simple-layout/annotate/26/branches/argparse/example.py')
754 'svn-svn-simple-layout/annotate/26/branches/argparse/example.py')
751
755
752
756
753 @pytest.mark.usefixtures("app", "autologin_user")
757 @pytest.mark.usefixtures("app", "autologin_user")
754 class TestModifyFilesWithWebInterface(object):
758 class TestModifyFilesWithWebInterface(object):
755
759
756 def test_add_file_view(self, backend):
760 def test_add_file_view(self, backend):
757 self.app.get(
761 self.app.get(
758 route_path('repo_files_add_file',
762 route_path('repo_files_add_file',
759 repo_name=backend.repo_name,
763 repo_name=backend.repo_name,
760 commit_id='tip', f_path='/')
764 commit_id='tip', f_path='/')
761 )
765 )
762
766
763 @pytest.mark.xfail_backends("svn", reason="Depends on online editing")
767 @pytest.mark.xfail_backends("svn", reason="Depends on online editing")
764 def test_add_file_into_repo_missing_content(self, backend, csrf_token):
768 def test_add_file_into_repo_missing_content(self, backend, csrf_token):
765 backend.create_repo()
769 backend.create_repo()
766 filename = 'init.py'
770 filename = 'init.py'
767 response = self.app.post(
771 response = self.app.post(
768 route_path('repo_files_create_file',
772 route_path('repo_files_create_file',
769 repo_name=backend.repo_name,
773 repo_name=backend.repo_name,
770 commit_id='tip', f_path='/'),
774 commit_id='tip', f_path='/'),
771 params={
775 params={
772 'content': "",
776 'content': "",
773 'filename': filename,
777 'filename': filename,
774 'csrf_token': csrf_token,
778 'csrf_token': csrf_token,
775 },
779 },
776 status=302)
780 status=302)
777 expected_msg = 'Successfully committed new file `{}`'.format(os.path.join(filename))
781 expected_msg = 'Successfully committed new file `{}`'.format(os.path.join(filename))
778 assert_session_flash(response, expected_msg)
782 assert_session_flash(response, expected_msg)
779
783
780 def test_add_file_into_repo_missing_filename(self, backend, csrf_token):
784 def test_add_file_into_repo_missing_filename(self, backend, csrf_token):
781 commit_id = backend.repo.get_commit().raw_id
785 commit_id = backend.repo.get_commit().raw_id
782 response = self.app.post(
786 response = self.app.post(
783 route_path('repo_files_create_file',
787 route_path('repo_files_create_file',
784 repo_name=backend.repo_name,
788 repo_name=backend.repo_name,
785 commit_id=commit_id, f_path='/'),
789 commit_id=commit_id, f_path='/'),
786 params={
790 params={
787 'content': "foo",
791 'content': "foo",
788 'csrf_token': csrf_token,
792 'csrf_token': csrf_token,
789 },
793 },
790 status=302)
794 status=302)
791
795
792 assert_session_flash(response, 'No filename specified')
796 assert_session_flash(response, 'No filename specified')
793
797
794 def test_add_file_into_repo_errors_and_no_commits(
798 def test_add_file_into_repo_errors_and_no_commits(
795 self, backend, csrf_token):
799 self, backend, csrf_token):
796 repo = backend.create_repo()
800 repo = backend.create_repo()
797 # Create a file with no filename, it will display an error but
801 # Create a file with no filename, it will display an error but
798 # the repo has no commits yet
802 # the repo has no commits yet
799 response = self.app.post(
803 response = self.app.post(
800 route_path('repo_files_create_file',
804 route_path('repo_files_create_file',
801 repo_name=repo.repo_name,
805 repo_name=repo.repo_name,
802 commit_id='tip', f_path='/'),
806 commit_id='tip', f_path='/'),
803 params={
807 params={
804 'content': "foo",
808 'content': "foo",
805 'csrf_token': csrf_token,
809 'csrf_token': csrf_token,
806 },
810 },
807 status=302)
811 status=302)
808
812
809 assert_session_flash(response, 'No filename specified')
813 assert_session_flash(response, 'No filename specified')
810
814
811 # Not allowed, redirect to the summary
815 # Not allowed, redirect to the summary
812 redirected = response.follow()
816 redirected = response.follow()
813 summary_url = h.route_path('repo_summary', repo_name=repo.repo_name)
817 summary_url = h.route_path('repo_summary', repo_name=repo.repo_name)
814
818
815 # As there are no commits, displays the summary page with the error of
819 # As there are no commits, displays the summary page with the error of
816 # creating a file with no filename
820 # creating a file with no filename
817
821
818 assert redirected.request.path == summary_url
822 assert redirected.request.path == summary_url
819
823
820 @pytest.mark.parametrize("filename, clean_filename", [
824 @pytest.mark.parametrize("filename, clean_filename", [
821 ('/abs/foo', 'abs/foo'),
825 ('/abs/foo', 'abs/foo'),
822 ('../rel/foo', 'rel/foo'),
826 ('../rel/foo', 'rel/foo'),
823 ('file/../foo/foo', 'file/foo/foo'),
827 ('file/../foo/foo', 'file/foo/foo'),
824 ])
828 ])
825 def test_add_file_into_repo_bad_filenames(self, filename, clean_filename, backend, csrf_token):
829 def test_add_file_into_repo_bad_filenames(self, filename, clean_filename, backend, csrf_token):
826 repo = backend.create_repo()
830 repo = backend.create_repo()
827 commit_id = repo.get_commit().raw_id
831 commit_id = repo.get_commit().raw_id
828
832
829 response = self.app.post(
833 response = self.app.post(
830 route_path('repo_files_create_file',
834 route_path('repo_files_create_file',
831 repo_name=repo.repo_name,
835 repo_name=repo.repo_name,
832 commit_id=commit_id, f_path='/'),
836 commit_id=commit_id, f_path='/'),
833 params={
837 params={
834 'content': "foo",
838 'content': "foo",
835 'filename': filename,
839 'filename': filename,
836 'csrf_token': csrf_token,
840 'csrf_token': csrf_token,
837 },
841 },
838 status=302)
842 status=302)
839
843
840 expected_msg = 'Successfully committed new file `{}`'.format(clean_filename)
844 expected_msg = 'Successfully committed new file `{}`'.format(clean_filename)
841 assert_session_flash(response, expected_msg)
845 assert_session_flash(response, expected_msg)
842
846
843 @pytest.mark.parametrize("cnt, filename, content", [
847 @pytest.mark.parametrize("cnt, filename, content", [
844 (1, 'foo.txt', "Content"),
848 (1, 'foo.txt', "Content"),
845 (2, 'dir/foo.rst', "Content"),
849 (2, 'dir/foo.rst', "Content"),
846 (3, 'dir/foo-second.rst', "Content"),
850 (3, 'dir/foo-second.rst', "Content"),
847 (4, 'rel/dir/foo.bar', "Content"),
851 (4, 'rel/dir/foo.bar', "Content"),
848 ])
852 ])
849 def test_add_file_into_empty_repo(self, cnt, filename, content, backend, csrf_token):
853 def test_add_file_into_empty_repo(self, cnt, filename, content, backend, csrf_token):
850 repo = backend.create_repo()
854 repo = backend.create_repo()
851 commit_id = repo.get_commit().raw_id
855 commit_id = repo.get_commit().raw_id
852 response = self.app.post(
856 response = self.app.post(
853 route_path('repo_files_create_file',
857 route_path('repo_files_create_file',
854 repo_name=repo.repo_name,
858 repo_name=repo.repo_name,
855 commit_id=commit_id, f_path='/'),
859 commit_id=commit_id, f_path='/'),
856 params={
860 params={
857 'content': content,
861 'content': content,
858 'filename': filename,
862 'filename': filename,
859 'csrf_token': csrf_token,
863 'csrf_token': csrf_token,
860 },
864 },
861 status=302)
865 status=302)
862
866
863 expected_msg = 'Successfully committed new file `{}`'.format(filename)
867 expected_msg = 'Successfully committed new file `{}`'.format(filename)
864 assert_session_flash(response, expected_msg)
868 assert_session_flash(response, expected_msg)
865
869
866 def test_edit_file_view(self, backend):
870 def test_edit_file_view(self, backend):
867 response = self.app.get(
871 response = self.app.get(
868 route_path('repo_files_edit_file',
872 route_path('repo_files_edit_file',
869 repo_name=backend.repo_name,
873 repo_name=backend.repo_name,
870 commit_id=backend.default_head_id,
874 commit_id=backend.default_head_id,
871 f_path='vcs/nodes.py'),
875 f_path='vcs/nodes.py'),
872 status=200)
876 status=200)
873 response.mustcontain("Module holding everything related to vcs nodes.")
877 response.mustcontain("Module holding everything related to vcs nodes.")
874
878
875 def test_edit_file_view_not_on_branch(self, backend):
879 def test_edit_file_view_not_on_branch(self, backend):
876 repo = backend.create_repo()
880 repo = backend.create_repo()
877 backend.ensure_file("vcs/nodes.py")
881 backend.ensure_file("vcs/nodes.py")
878
882
879 response = self.app.get(
883 response = self.app.get(
880 route_path('repo_files_edit_file',
884 route_path('repo_files_edit_file',
881 repo_name=repo.repo_name,
885 repo_name=repo.repo_name,
882 commit_id='tip',
886 commit_id='tip',
883 f_path='vcs/nodes.py'),
887 f_path='vcs/nodes.py'),
884 status=302)
888 status=302)
885 assert_session_flash(
889 assert_session_flash(
886 response, 'Cannot modify file. Given commit `tip` is not head of a branch.')
890 response, 'Cannot modify file. Given commit `tip` is not head of a branch.')
887
891
888 def test_edit_file_view_commit_changes(self, backend, csrf_token):
892 def test_edit_file_view_commit_changes(self, backend, csrf_token):
889 repo = backend.create_repo()
893 repo = backend.create_repo()
890 backend.ensure_file("vcs/nodes.py", content="print 'hello'")
894 backend.ensure_file("vcs/nodes.py", content="print 'hello'")
891
895
892 response = self.app.post(
896 response = self.app.post(
893 route_path('repo_files_update_file',
897 route_path('repo_files_update_file',
894 repo_name=repo.repo_name,
898 repo_name=repo.repo_name,
895 commit_id=backend.default_head_id,
899 commit_id=backend.default_head_id,
896 f_path='vcs/nodes.py'),
900 f_path='vcs/nodes.py'),
897 params={
901 params={
898 'content': "print 'hello world'",
902 'content': "print 'hello world'",
899 'message': 'I committed',
903 'message': 'I committed',
900 'filename': "vcs/nodes.py",
904 'filename': "vcs/nodes.py",
901 'csrf_token': csrf_token,
905 'csrf_token': csrf_token,
902 },
906 },
903 status=302)
907 status=302)
904 assert_session_flash(
908 assert_session_flash(
905 response, 'Successfully committed changes to file `vcs/nodes.py`')
909 response, 'Successfully committed changes to file `vcs/nodes.py`')
906 tip = repo.get_commit(commit_idx=-1)
910 tip = repo.get_commit(commit_idx=-1)
907 assert tip.message == 'I committed'
911 assert tip.message == 'I committed'
908
912
909 def test_edit_file_view_commit_changes_default_message(self, backend,
913 def test_edit_file_view_commit_changes_default_message(self, backend,
910 csrf_token):
914 csrf_token):
911 repo = backend.create_repo()
915 repo = backend.create_repo()
912 backend.ensure_file("vcs/nodes.py", content="print 'hello'")
916 backend.ensure_file("vcs/nodes.py", content="print 'hello'")
913
917
914 commit_id = (
918 commit_id = (
915 backend.default_branch_name or
919 backend.default_branch_name or
916 backend.repo.scm_instance().commit_ids[-1])
920 backend.repo.scm_instance().commit_ids[-1])
917
921
918 response = self.app.post(
922 response = self.app.post(
919 route_path('repo_files_update_file',
923 route_path('repo_files_update_file',
920 repo_name=repo.repo_name,
924 repo_name=repo.repo_name,
921 commit_id=commit_id,
925 commit_id=commit_id,
922 f_path='vcs/nodes.py'),
926 f_path='vcs/nodes.py'),
923 params={
927 params={
924 'content': "print 'hello world'",
928 'content': "print 'hello world'",
925 'message': '',
929 'message': '',
926 'filename': "vcs/nodes.py",
930 'filename': "vcs/nodes.py",
927 'csrf_token': csrf_token,
931 'csrf_token': csrf_token,
928 },
932 },
929 status=302)
933 status=302)
930 assert_session_flash(
934 assert_session_flash(
931 response, 'Successfully committed changes to file `vcs/nodes.py`')
935 response, 'Successfully committed changes to file `vcs/nodes.py`')
932 tip = repo.get_commit(commit_idx=-1)
936 tip = repo.get_commit(commit_idx=-1)
933 assert tip.message == 'Edited file vcs/nodes.py via RhodeCode Enterprise'
937 assert tip.message == 'Edited file vcs/nodes.py via RhodeCode Enterprise'
934
938
935 def test_delete_file_view(self, backend):
939 def test_delete_file_view(self, backend):
936 self.app.get(
940 self.app.get(
937 route_path('repo_files_remove_file',
941 route_path('repo_files_remove_file',
938 repo_name=backend.repo_name,
942 repo_name=backend.repo_name,
939 commit_id=backend.default_head_id,
943 commit_id=backend.default_head_id,
940 f_path='vcs/nodes.py'),
944 f_path='vcs/nodes.py'),
941 status=200)
945 status=200)
942
946
943 def test_delete_file_view_not_on_branch(self, backend):
947 def test_delete_file_view_not_on_branch(self, backend):
944 repo = backend.create_repo()
948 repo = backend.create_repo()
945 backend.ensure_file('vcs/nodes.py')
949 backend.ensure_file('vcs/nodes.py')
946
950
947 response = self.app.get(
951 response = self.app.get(
948 route_path('repo_files_remove_file',
952 route_path('repo_files_remove_file',
949 repo_name=repo.repo_name,
953 repo_name=repo.repo_name,
950 commit_id='tip',
954 commit_id='tip',
951 f_path='vcs/nodes.py'),
955 f_path='vcs/nodes.py'),
952 status=302)
956 status=302)
953 assert_session_flash(
957 assert_session_flash(
954 response, 'Cannot modify file. Given commit `tip` is not head of a branch.')
958 response, 'Cannot modify file. Given commit `tip` is not head of a branch.')
955
959
956 def test_delete_file_view_commit_changes(self, backend, csrf_token):
960 def test_delete_file_view_commit_changes(self, backend, csrf_token):
957 repo = backend.create_repo()
961 repo = backend.create_repo()
958 backend.ensure_file("vcs/nodes.py")
962 backend.ensure_file("vcs/nodes.py")
959
963
960 response = self.app.post(
964 response = self.app.post(
961 route_path('repo_files_delete_file',
965 route_path('repo_files_delete_file',
962 repo_name=repo.repo_name,
966 repo_name=repo.repo_name,
963 commit_id=backend.default_head_id,
967 commit_id=backend.default_head_id,
964 f_path='vcs/nodes.py'),
968 f_path='vcs/nodes.py'),
965 params={
969 params={
966 'message': 'i commited',
970 'message': 'i commited',
967 'csrf_token': csrf_token,
971 'csrf_token': csrf_token,
968 },
972 },
969 status=302)
973 status=302)
970 assert_session_flash(
974 assert_session_flash(
971 response, 'Successfully deleted file `vcs/nodes.py`')
975 response, 'Successfully deleted file `vcs/nodes.py`')
972
976
973
977
974 @pytest.mark.usefixtures("app")
978 @pytest.mark.usefixtures("app")
975 class TestFilesViewOtherCases(object):
979 class TestFilesViewOtherCases(object):
976
980
977 def test_access_empty_repo_redirect_to_summary_with_alert_write_perms(
981 def test_access_empty_repo_redirect_to_summary_with_alert_write_perms(
978 self, backend_stub, autologin_regular_user, user_regular,
982 self, backend_stub, autologin_regular_user, user_regular,
979 user_util):
983 user_util):
980
984
981 repo = backend_stub.create_repo()
985 repo = backend_stub.create_repo()
982 user_util.grant_user_permission_to_repo(
986 user_util.grant_user_permission_to_repo(
983 repo, user_regular, 'repository.write')
987 repo, user_regular, 'repository.write')
984 response = self.app.get(
988 response = self.app.get(
985 route_path('repo_files',
989 route_path('repo_files',
986 repo_name=repo.repo_name,
990 repo_name=repo.repo_name,
987 commit_id='tip', f_path='/'))
991 commit_id='tip', f_path='/'))
988
992
989 repo_file_add_url = route_path(
993 repo_file_add_url = route_path(
990 'repo_files_add_file',
994 'repo_files_add_file',
991 repo_name=repo.repo_name,
995 repo_name=repo.repo_name,
992 commit_id=0, f_path='')
996 commit_id=0, f_path='')
993
997
994 assert_session_flash(
998 assert_session_flash(
995 response,
999 response,
996 'There are no files yet. <a class="alert-link" '
1000 'There are no files yet. <a class="alert-link" '
997 'href="{}">Click here to add a new file.</a>'
1001 'href="{}">Click here to add a new file.</a>'
998 .format(repo_file_add_url))
1002 .format(repo_file_add_url))
999
1003
1000 def test_access_empty_repo_redirect_to_summary_with_alert_no_write_perms(
1004 def test_access_empty_repo_redirect_to_summary_with_alert_no_write_perms(
1001 self, backend_stub, autologin_regular_user):
1005 self, backend_stub, autologin_regular_user):
1002 repo = backend_stub.create_repo()
1006 repo = backend_stub.create_repo()
1003 # init session for anon user
1007 # init session for anon user
1004 route_path('repo_summary', repo_name=repo.repo_name)
1008 route_path('repo_summary', repo_name=repo.repo_name)
1005
1009
1006 repo_file_add_url = route_path(
1010 repo_file_add_url = route_path(
1007 'repo_files_add_file',
1011 'repo_files_add_file',
1008 repo_name=repo.repo_name,
1012 repo_name=repo.repo_name,
1009 commit_id=0, f_path='')
1013 commit_id=0, f_path='')
1010
1014
1011 response = self.app.get(
1015 response = self.app.get(
1012 route_path('repo_files',
1016 route_path('repo_files',
1013 repo_name=repo.repo_name,
1017 repo_name=repo.repo_name,
1014 commit_id='tip', f_path='/'))
1018 commit_id='tip', f_path='/'))
1015
1019
1016 assert_session_flash(response, no_=repo_file_add_url)
1020 assert_session_flash(response, no_=repo_file_add_url)
1017
1021
1018 @pytest.mark.parametrize('file_node', [
1022 @pytest.mark.parametrize('file_node', [
1019 'archive/file.zip',
1023 'archive/file.zip',
1020 'diff/my-file.txt',
1024 'diff/my-file.txt',
1021 'render.py',
1025 'render.py',
1022 'render',
1026 'render',
1023 'remove_file',
1027 'remove_file',
1024 'remove_file/to-delete.txt',
1028 'remove_file/to-delete.txt',
1025 ])
1029 ])
1026 def test_file_names_equal_to_routes_parts(self, backend, file_node):
1030 def test_file_names_equal_to_routes_parts(self, backend, file_node):
1027 backend.create_repo()
1031 backend.create_repo()
1028 backend.ensure_file(file_node)
1032 backend.ensure_file(file_node)
1029
1033
1030 self.app.get(
1034 self.app.get(
1031 route_path('repo_files',
1035 route_path('repo_files',
1032 repo_name=backend.repo_name,
1036 repo_name=backend.repo_name,
1033 commit_id='tip', f_path=file_node),
1037 commit_id='tip', f_path=file_node),
1034 status=200)
1038 status=200)
1035
1039
1036
1040
1037 class TestAdjustFilePathForSvn(object):
1041 class TestAdjustFilePathForSvn(object):
1038 """
1042 """
1039 SVN specific adjustments of node history in RepoFilesView.
1043 SVN specific adjustments of node history in RepoFilesView.
1040 """
1044 """
1041
1045
1042 def test_returns_path_relative_to_matched_reference(self):
1046 def test_returns_path_relative_to_matched_reference(self):
1043 repo = self._repo(branches=['trunk'])
1047 repo = self._repo(branches=['trunk'])
1044 self.assert_file_adjustment('trunk/file', 'file', repo)
1048 self.assert_file_adjustment('trunk/file', 'file', repo)
1045
1049
1046 def test_does_not_modify_file_if_no_reference_matches(self):
1050 def test_does_not_modify_file_if_no_reference_matches(self):
1047 repo = self._repo(branches=['trunk'])
1051 repo = self._repo(branches=['trunk'])
1048 self.assert_file_adjustment('notes/file', 'notes/file', repo)
1052 self.assert_file_adjustment('notes/file', 'notes/file', repo)
1049
1053
1050 def test_does_not_adjust_partial_directory_names(self):
1054 def test_does_not_adjust_partial_directory_names(self):
1051 repo = self._repo(branches=['trun'])
1055 repo = self._repo(branches=['trun'])
1052 self.assert_file_adjustment('trunk/file', 'trunk/file', repo)
1056 self.assert_file_adjustment('trunk/file', 'trunk/file', repo)
1053
1057
1054 def test_is_robust_to_patterns_which_prefix_other_patterns(self):
1058 def test_is_robust_to_patterns_which_prefix_other_patterns(self):
1055 repo = self._repo(branches=['trunk', 'trunk/new', 'trunk/old'])
1059 repo = self._repo(branches=['trunk', 'trunk/new', 'trunk/old'])
1056 self.assert_file_adjustment('trunk/new/file', 'file', repo)
1060 self.assert_file_adjustment('trunk/new/file', 'file', repo)
1057
1061
1058 def assert_file_adjustment(self, f_path, expected, repo):
1062 def assert_file_adjustment(self, f_path, expected, repo):
1059 result = RepoFilesView.adjust_file_path_for_svn(f_path, repo)
1063 result = RepoFilesView.adjust_file_path_for_svn(f_path, repo)
1060 assert result == expected
1064 assert result == expected
1061
1065
1062 def _repo(self, branches=None):
1066 def _repo(self, branches=None):
1063 repo = mock.Mock()
1067 repo = mock.Mock()
1064 repo.branches = OrderedDict((name, '0') for name in branches or [])
1068 repo.branches = OrderedDict((name, '0') for name in branches or [])
1065 repo.tags = {}
1069 repo.tags = {}
1066 return repo
1070 return repo
General Comments 0
You need to be logged in to leave comments. Login now