##// END OF EJS Templates
tests: fixed compare page and related tests....
marcink -
r3773:a77b6fa8 new-ui
parent child Browse files
Show More
@@ -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
98
98 # Verify test fixture setup
99 # Verify test fixture setup
99 # This does not work for git
100 # This does not work for git
100 if backend.alias != 'git':
101 if backend.alias != 'git':
101 assert 5 == len(fork.scm_instance().commit_ids)
102 assert 5 == len(fork.scm_instance().commit_ids)
102 assert 2 == len(origin_repo.commit_ids)
103 assert 2 == len(origin_repo.commit_ids)
103
104
104 # Comparing the revisions
105 # Comparing the revisions
105 response = self.app.get(
106 response = self.app.get(
106 route_path('repo_compare',
107 route_path('repo_compare',
107 repo_name=origin.repo_name,
108 repo_name=origin.repo_name,
108 source_ref_type="rev", source_ref=commit3.raw_id,
109 source_ref_type="rev", source_ref=commit3.raw_id,
109 target_ref_type="rev", target_ref=commit4.raw_id,
110 target_ref_type="rev", target_ref=commit4.raw_id,
110 params=dict(merge='1', target_repo=fork.repo_name)
111 params=dict(merge='1', target_repo=fork.repo_name)
111 ))
112 ))
112
113
113 compare_page = ComparePage(response)
114 compare_page = ComparePage(response)
114 compare_page.contains_commits([commit4])
115 compare_page.contains_commits([commit4])
115
116
116 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
117 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
117 def test_compare_forks_on_branch_extra_commits(self, backend):
118 def test_compare_forks_on_branch_extra_commits(self, backend):
118 repo1 = backend.create_repo()
119 repo1 = backend.create_repo()
119
120
120 # commit something !
121 # commit something !
121 commit0 = commit_change(
122 commit0 = commit_change(
122 repo1.repo_name, filename='file1', content='line1\n',
123 repo1.repo_name, filename='file1', content='line1\n',
123 message='commit1', vcs_type=backend.alias, parent=None,
124 message='commit1', vcs_type=backend.alias, parent=None,
124 newfile=True)
125 newfile=True)
125
126
126 # fork this repo
127 # fork this repo
127 repo2 = backend.create_fork()
128 repo2 = backend.create_fork()
128
129
129 # add two extra commit into fork
130 # add two extra commit into fork
130 commit1 = commit_change(
131 commit1 = commit_change(
131 repo2.repo_name, filename='file1', content='line1\nline2\n',
132 repo2.repo_name, filename='file1', content='line1\nline2\n',
132 message='commit2', vcs_type=backend.alias, parent=commit0)
133 message='commit2', vcs_type=backend.alias, parent=commit0)
133
134
134 commit2 = commit_change(
135 commit2 = commit_change(
135 repo2.repo_name, filename='file1', content='line1\nline2\nline3\n',
136 repo2.repo_name, filename='file1', content='line1\nline2\nline3\n',
136 message='commit3', vcs_type=backend.alias, parent=commit1)
137 message='commit3', vcs_type=backend.alias, parent=commit1)
137
138
138 commit_id1 = repo1.scm_instance().DEFAULT_BRANCH_NAME
139 commit_id1 = repo1.scm_instance().DEFAULT_BRANCH_NAME
139 commit_id2 = repo2.scm_instance().DEFAULT_BRANCH_NAME
140 commit_id2 = repo2.scm_instance().DEFAULT_BRANCH_NAME
140
141
141 response = self.app.get(
142 response = self.app.get(
142 route_path('repo_compare',
143 route_path('repo_compare',
143 repo_name=repo1.repo_name,
144 repo_name=repo1.repo_name,
144 source_ref_type="branch", source_ref=commit_id2,
145 source_ref_type="branch", source_ref=commit_id2,
145 target_ref_type="branch", target_ref=commit_id1,
146 target_ref_type="branch", target_ref=commit_id1,
146 params=dict(merge='1', target_repo=repo2.repo_name)
147 params=dict(merge='1', target_repo=repo2.repo_name)
147 ))
148 ))
148
149
149 response.mustcontain('%s@%s' % (repo1.repo_name, commit_id2))
150 response.mustcontain('%s@%s' % (repo1.repo_name, commit_id2))
150 response.mustcontain('%s@%s' % (repo2.repo_name, commit_id1))
151 response.mustcontain('%s@%s' % (repo2.repo_name, commit_id1))
151
152
152 compare_page = ComparePage(response)
153 compare_page = ComparePage(response)
153 compare_page.contains_change_summary(1, 2, 0)
154 compare_page.contains_change_summary(1, 2, 0)
154 compare_page.contains_commits([commit1, commit2])
155 compare_page.contains_commits([commit1, commit2])
155
156
156 anchor = 'a_c-{}-826e8142e6ba'.format(commit0.short_id)
157 anchor = 'a_c-{}-826e8142e6ba'.format(commit0.short_id)
157 compare_page.contains_file_links_and_anchors([('file1', anchor), ])
158 compare_page.contains_file_links_and_anchors([('file1', anchor), ])
158
159
159 # 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
160 # it is then a preview mode
161 # it is then a preview mode
161 compare_page.swap_is_hidden()
162 compare_page.swap_is_hidden()
162 compare_page.target_source_are_disabled()
163 compare_page.target_source_are_disabled()
163
164
164 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
165 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
165 def test_compare_forks_on_branch_extra_commits_origin_has_incomming(
166 def test_compare_forks_on_branch_extra_commits_origin_has_incomming(self, backend):
166 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 "{inserted} inserted, {deleted} deleted")
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,183 +1,259 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.lib.vcs import nodes
23 from rhodecode.lib.vcs import nodes
24 from rhodecode.lib.vcs.backends.base import EmptyCommit
24 from rhodecode.tests.fixture import Fixture
25 from rhodecode.tests.fixture import Fixture
25 from rhodecode.tests.utils import commit_change
26 from rhodecode.tests.utils import commit_change
26
27
27 fixture = Fixture()
28 fixture = Fixture()
28
29
29
30
30 def route_path(name, params=None, **kwargs):
31 def route_path(name, params=None, **kwargs):
31 import urllib
32 import urllib
32
33
33 base_url = {
34 base_url = {
34 'repo_compare_select': '/{repo_name}/compare',
35 'repo_compare_select': '/{repo_name}/compare',
35 'repo_compare': '/{repo_name}/compare/{source_ref_type}@{source_ref}...{target_ref_type}@{target_ref}',
36 'repo_compare': '/{repo_name}/compare/{source_ref_type}@{source_ref}...{target_ref_type}@{target_ref}',
36 }[name].format(**kwargs)
37 }[name].format(**kwargs)
37
38
38 if params:
39 if params:
39 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
40 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
40 return base_url
41 return base_url
41
42
42
43
43 @pytest.mark.usefixtures("autologin_user", "app")
44 @pytest.mark.usefixtures("autologin_user", "app")
44 class TestSideBySideDiff(object):
45 class TestSideBySideDiff(object):
45
46
46 def test_diff_side_by_side(self, app, backend, backend_stub):
47 def test_diff_sidebyside_single_commit(self, app, backend):
47 f_path = 'test_sidebyside_file.py'
48 commit1_content = 'content-25d7e49c18b159446c\n'
49 commit2_content = 'content-603d6c72c46d953420\n'
50 repo = backend.create_repo()
51
52 commit1 = commit_change(
53 repo.repo_name, filename=f_path, content=commit1_content,
54 message='A', vcs_type=backend.alias, parent=None, newfile=True)
55
56 commit2 = commit_change(
57 repo.repo_name, filename=f_path, content=commit2_content,
58 message='B, child of A', vcs_type=backend.alias, parent=commit1)
59
60 response = self.app.get(route_path(
61 'repo_compare',
62 repo_name=repo.repo_name,
63 source_ref_type='rev',
64 source_ref=commit1.raw_id,
65 target_ref_type='rev',
66 target_ref=commit2.raw_id,
67 params=dict(f_path=f_path, target_repo=repo.repo_name, diffmode='sidebyside')
68 ))
69
70 response.mustcontain('Expand 1 commit')
71 response.mustcontain('1 file changed')
72
73 response.mustcontain(
74 'r%s:%s...r%s:%s' % (
75 commit1.idx, commit1.short_id, commit2.idx, commit2.short_id))
76
77 response.mustcontain('<strong>{}</strong>'.format(f_path))
78
79 def test_diff_side_by_side_with_empty_file(self, app, backend, backend_stub):
80 commits = [
81 {'message': 'First commit'},
82 {'message': 'Commit with binary',
83 'added': [nodes.FileNode('file.empty', content='')]},
84 ]
85 f_path = 'file.empty'
86 repo = backend.create_repo(commits=commits)
87 commit1 = repo.get_commit(commit_idx=0)
88 commit2 = repo.get_commit(commit_idx=1)
89
90 response = self.app.get(route_path(
91 'repo_compare',
92 repo_name=repo.repo_name,
93 source_ref_type='rev',
94 source_ref=commit1.raw_id,
95 target_ref_type='rev',
96 target_ref=commit2.raw_id,
97 params=dict(f_path=f_path, target_repo=repo.repo_name, diffmode='sidebyside')
98 ))
99
100 response.mustcontain('Expand 1 commit')
101 response.mustcontain('1 file changed')
102
103 response.mustcontain(
104 'r%s:%s...r%s:%s' % (
105 commit1.idx, commit1.short_id, commit2.idx, commit2.short_id))
106
107 response.mustcontain('<strong>{}</strong>'.format(f_path))
108
109 def test_diff_sidebyside_two_commits(self, app, backend):
110 commit_id_range = {
48 commit_id_range = {
111 'hg': {
49 'hg': {
112 'commits': ['25d7e49c18b159446cadfa506a5cf8ad1cb04067',
50 'commits': ['25d7e49c18b159446cadfa506a5cf8ad1cb04067',
113 '603d6c72c46d953420c89d36372f08d9f305f5dd'],
51 '603d6c72c46d953420c89d36372f08d9f305f5dd'],
114 'changes': '21 files changed: 943 inserted, 288 deleted'
52 'changes': '21 files changed: 943 inserted, 288 deleted'
115 },
53 },
116 'git': {
54 'git': {
117 'commits': ['6fc9270775aaf5544c1deb014f4ddd60c952fcbb',
55 'commits': ['6fc9270775aaf5544c1deb014f4ddd60c952fcbb',
118 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'],
56 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'],
119 'changes': '21 files changed: 943 inserted, 288 deleted'
57 'changes': '21 files changed: 943 inserted, 288 deleted'
120 },
58 },
121
59
122 'svn': {
60 'svn': {
123 'commits': ['336',
61 'commits': ['336',
124 '337'],
62 '337'],
125 'changes': '21 files changed: 943 inserted, 288 deleted'
63 'changes': '21 files changed: 943 inserted, 288 deleted'
126 },
64 },
127 }
65 }
128
66
129 commit_info = commit_id_range[backend.alias]
67 commit_info = commit_id_range[backend.alias]
130 commit2, commit1 = commit_info['commits']
68 commit2, commit1 = commit_info['commits']
131 file_changes = commit_info['changes']
69 file_changes = commit_info['changes']
132
70
133 response = self.app.get(route_path(
71 response = self.app.get(route_path(
134 'repo_compare',
72 'repo_compare',
135 repo_name=backend.repo_name,
73 repo_name=backend.repo_name,
136 source_ref_type='rev',
74 source_ref_type='rev',
137 source_ref=commit2,
75 source_ref=commit2,
138 target_repo=backend.repo_name,
76 target_repo=backend.repo_name,
139 target_ref_type='rev',
77 target_ref_type='rev',
140 target_ref=commit1,
78 target_ref=commit1,
141 params=dict(target_repo=backend.repo_name, diffmode='sidebyside')
79 params=dict(target_repo=backend.repo_name, diffmode='sidebyside')
142 ))
80 ))
143
81
82 response.mustcontain(file_changes)
144 response.mustcontain('Expand 1 commit')
83 response.mustcontain('Expand 1 commit')
145 response.mustcontain(file_changes)
146
84
147 def test_diff_sidebyside_two_commits_single_file(self, app, backend):
85 def test_diff_sidebyside_two_commits(self, app, backend):
148 commit_id_range = {
86 commit_id_range = {
149 'hg': {
87 'hg': {
150 'commits': ['25d7e49c18b159446cadfa506a5cf8ad1cb04067',
88 'commits': ['4fdd71e9427417b2e904e0464c634fdee85ec5a7',
151 '603d6c72c46d953420c89d36372f08d9f305f5dd'],
89 '603d6c72c46d953420c89d36372f08d9f305f5dd'],
152 'changes': '1 file changed: 1 inserted, 1 deleted'
90 'changes': '32 files changed: 1165 inserted, 308 deleted'
153 },
91 },
154 'git': {
92 'git': {
155 'commits': ['6fc9270775aaf5544c1deb014f4ddd60c952fcbb',
93 'commits': ['f5fbf9cfd5f1f1be146f6d3b38bcd791a7480c13',
156 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'],
94 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'],
157 'changes': '1 file changed: 1 inserted, 1 deleted'
95 'changes': '32 files changed: 1165 inserted, 308 deleted'
158 },
96 },
159
97
160 'svn': {
98 'svn': {
161 'commits': ['336',
99 'commits': ['335',
162 '337'],
100 '337'],
163 'changes': '1 file changed: 1 inserted, 1 deleted'
101 'changes': '32 files changed: 1179 inserted, 310 deleted'
102 },
103 }
104
105 commit_info = commit_id_range[backend.alias]
106 commit2, commit1 = commit_info['commits']
107 file_changes = commit_info['changes']
108
109 response = self.app.get(route_path(
110 'repo_compare',
111 repo_name=backend.repo_name,
112 source_ref_type='rev',
113 source_ref=commit2,
114 target_repo=backend.repo_name,
115 target_ref_type='rev',
116 target_ref=commit1,
117 params=dict(target_repo=backend.repo_name, diffmode='sidebyside')
118 ))
119
120 response.mustcontain(file_changes)
121 response.mustcontain('Expand 2 commits')
122
123 @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):
125 f_path = 'test_sidebyside_file.py'
126 commit1_content = 'content-25d7e49c18b159446c\n'
127 commit2_content = 'content-603d6c72c46d953420\n'
128 repo = backend.create_repo()
129
130 commit1 = commit_change(
131 repo.repo_name, filename=f_path, content=commit1_content,
132 message='A', vcs_type=backend.alias, parent=None, newfile=True)
133
134 commit2 = commit_change(
135 repo.repo_name, filename=f_path, content=commit2_content,
136 message='B, child of A', vcs_type=backend.alias, parent=commit1)
137
138 response = self.app.get(route_path(
139 'repo_compare',
140 repo_name=repo.repo_name,
141 source_ref_type='rev',
142 source_ref=EmptyCommit().raw_id,
143 target_ref_type='rev',
144 target_ref=commit2.raw_id,
145 params=dict(diffmode='sidebyside')
146 ))
147
148 response.mustcontain('Expand 2 commits')
149 response.mustcontain('123 file changed')
150
151 response.mustcontain(
152 'r%s:%s...r%s:%s' % (
153 commit1.idx, commit1.short_id, commit2.idx, commit2.short_id))
154
155 response.mustcontain('<strong>{}</strong>'.format(f_path))
156
157 @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):
159 f_path = 'test_sidebyside_file.py'
160 commit1_content = 'content-25d7e49c18b159446c\n'
161 commit2_content = 'content-603d6c72c46d953420\n'
162 repo = backend.create_repo()
163
164 commit1 = commit_change(
165 repo.repo_name, filename=f_path, content=commit1_content,
166 message='A', vcs_type=backend.alias, parent=None, newfile=True)
167
168 commit2 = commit_change(
169 repo.repo_name, filename=f_path, content=commit2_content,
170 message='B, child of A', vcs_type=backend.alias, parent=commit1)
171
172 response = self.app.get(route_path(
173 'repo_compare',
174 repo_name=repo.repo_name,
175 source_ref_type='rev',
176 source_ref=EmptyCommit().raw_id,
177 target_ref_type='rev',
178 target_ref=commit2.raw_id,
179 params=dict(f_path=f_path, target_repo=repo.repo_name, diffmode='sidebyside')
180 ))
181
182 response.mustcontain('Expand 2 commits')
183 response.mustcontain('1 file changed')
184
185 response.mustcontain(
186 'r%s:%s...r%s:%s' % (
187 commit1.idx, commit1.short_id, commit2.idx, commit2.short_id))
188
189 response.mustcontain('<strong>{}</strong>'.format(f_path))
190
191 def test_diff_side_by_side_with_empty_file(self, app, backend, backend_stub):
192 commits = [
193 {'message': 'First commit'},
194 {'message': 'Second commit'},
195 {'message': 'Commit with binary',
196 'added': [nodes.FileNode('file.empty', content='')]},
197 ]
198 f_path = 'file.empty'
199 repo = backend.create_repo(commits=commits)
200 commit1 = repo.get_commit(commit_idx=0)
201 commit2 = repo.get_commit(commit_idx=1)
202 commit3 = repo.get_commit(commit_idx=2)
203
204 response = self.app.get(route_path(
205 'repo_compare',
206 repo_name=repo.repo_name,
207 source_ref_type='rev',
208 source_ref=commit1.raw_id,
209 target_ref_type='rev',
210 target_ref=commit3.raw_id,
211 params=dict(f_path=f_path, target_repo=repo.repo_name, diffmode='sidebyside')
212 ))
213
214 response.mustcontain('Expand 2 commits')
215 response.mustcontain('1 file changed')
216
217 response.mustcontain(
218 'r%s:%s...r%s:%s' % (
219 commit2.idx, commit2.short_id, commit3.idx, commit3.short_id))
220
221 response.mustcontain('<strong>{}</strong>'.format(f_path))
222
223 def test_diff_sidebyside_two_commits_with_file_filter(self, app, backend):
224 commit_id_range = {
225 'hg': {
226 'commits': ['4fdd71e9427417b2e904e0464c634fdee85ec5a7',
227 '603d6c72c46d953420c89d36372f08d9f305f5dd'],
228 'changes': '1 file changed: 3 inserted, 3 deleted'
229 },
230 'git': {
231 'commits': ['f5fbf9cfd5f1f1be146f6d3b38bcd791a7480c13',
232 '03fa803d7e9fb14daa9a3089e0d1494eda75d986'],
233 'changes': '1 file changed: 3 inserted, 3 deleted'
234 },
235
236 'svn': {
237 'commits': ['335',
238 '337'],
239 'changes': '1 file changed: 3 inserted, 3 deleted'
164 },
240 },
165 }
241 }
166 f_path = 'docs/conf.py'
242 f_path = 'docs/conf.py'
167
243
168 commit_info = commit_id_range[backend.alias]
244 commit_info = commit_id_range[backend.alias]
169 commit2, commit1 = commit_info['commits']
245 commit2, commit1 = commit_info['commits']
170 file_changes = commit_info['changes']
246 file_changes = commit_info['changes']
171
247
172 response = self.app.get(route_path(
248 response = self.app.get(route_path(
173 'repo_compare',
249 'repo_compare',
174 repo_name=backend.repo_name,
250 repo_name=backend.repo_name,
175 source_ref_type='rev',
251 source_ref_type='rev',
176 source_ref=commit2,
252 source_ref=commit2,
177 target_ref_type='rev',
253 target_ref_type='rev',
178 target_ref=commit1,
254 target_ref=commit1,
179 params=dict(f_path=f_path, target_repo=backend.repo_name, diffmode='sidebyside')
255 params=dict(f_path=f_path, target_repo=backend.repo_name, diffmode='sidebyside')
180 ))
256 ))
181
257
182 response.mustcontain('Expand 1 commit')
258 response.mustcontain('Expand 2 commits')
183 response.mustcontain(file_changes)
259 response.mustcontain(file_changes)
@@ -1,317 +1,311 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2019 RhodeCode GmbH
3 # Copyright (C) 2012-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
21
22 import logging
22 import logging
23
23
24 from pyramid.httpexceptions import HTTPBadRequest, HTTPNotFound, HTTPFound
24 from pyramid.httpexceptions import HTTPBadRequest, HTTPNotFound, HTTPFound
25 from pyramid.view import view_config
25 from pyramid.view import view_config
26 from pyramid.renderers import render
26 from pyramid.renderers import render
27 from pyramid.response import Response
27 from pyramid.response import Response
28
28
29 from rhodecode.apps._base import RepoAppView
29 from rhodecode.apps._base import RepoAppView
30
30
31 from rhodecode.lib import helpers as h
31 from rhodecode.lib import helpers as h
32 from rhodecode.lib import diffs, codeblocks
32 from rhodecode.lib import diffs, codeblocks
33 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
33 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
34 from rhodecode.lib.utils import safe_str
34 from rhodecode.lib.utils import safe_str
35 from rhodecode.lib.utils2 import safe_unicode, str2bool
35 from rhodecode.lib.utils2 import safe_unicode, str2bool
36 from rhodecode.lib.view_utils import parse_path_ref, get_commit_from_ref_name
36 from rhodecode.lib.view_utils import parse_path_ref, get_commit_from_ref_name
37 from rhodecode.lib.vcs.exceptions import (
37 from rhodecode.lib.vcs.exceptions import (
38 EmptyRepositoryError, RepositoryError, RepositoryRequirementError,
38 EmptyRepositoryError, RepositoryError, RepositoryRequirementError,
39 NodeDoesNotExistError)
39 NodeDoesNotExistError)
40 from rhodecode.model.db import Repository, ChangesetStatus
40 from rhodecode.model.db import Repository, ChangesetStatus
41
41
42 log = logging.getLogger(__name__)
42 log = logging.getLogger(__name__)
43
43
44
44
45 class RepoCompareView(RepoAppView):
45 class RepoCompareView(RepoAppView):
46 def load_default_context(self):
46 def load_default_context(self):
47 c = self._get_local_tmpl_context(include_app_defaults=True)
47 c = self._get_local_tmpl_context(include_app_defaults=True)
48 c.rhodecode_repo = self.rhodecode_vcs_repo
48 c.rhodecode_repo = self.rhodecode_vcs_repo
49 return c
49 return c
50
50
51 def _get_commit_or_redirect(
51 def _get_commit_or_redirect(
52 self, ref, ref_type, repo, redirect_after=True, partial=False):
52 self, ref, ref_type, repo, redirect_after=True, partial=False):
53 """
53 """
54 This is a safe way to get a commit. If an error occurs it
54 This is a safe way to get a commit. If an error occurs it
55 redirects to a commit with a proper message. If partial is set
55 redirects to a commit with a proper message. If partial is set
56 then it does not do redirect raise and throws an exception instead.
56 then it does not do redirect raise and throws an exception instead.
57 """
57 """
58 _ = self.request.translate
58 _ = self.request.translate
59 try:
59 try:
60 return get_commit_from_ref_name(repo, safe_str(ref), ref_type)
60 return get_commit_from_ref_name(repo, safe_str(ref), ref_type)
61 except EmptyRepositoryError:
61 except EmptyRepositoryError:
62 if not redirect_after:
62 if not redirect_after:
63 return repo.scm_instance().EMPTY_COMMIT
63 return repo.scm_instance().EMPTY_COMMIT
64 h.flash(h.literal(_('There are no commits yet')),
64 h.flash(h.literal(_('There are no commits yet')),
65 category='warning')
65 category='warning')
66 if not partial:
66 if not partial:
67 raise HTTPFound(
67 raise HTTPFound(
68 h.route_path('repo_summary', repo_name=repo.repo_name))
68 h.route_path('repo_summary', repo_name=repo.repo_name))
69 raise HTTPBadRequest()
69 raise HTTPBadRequest()
70
70
71 except RepositoryError as e:
71 except RepositoryError as e:
72 log.exception(safe_str(e))
72 log.exception(safe_str(e))
73 h.flash(safe_str(h.escape(e)), category='warning')
73 h.flash(safe_str(h.escape(e)), category='warning')
74 if not partial:
74 if not partial:
75 raise HTTPFound(
75 raise HTTPFound(
76 h.route_path('repo_summary', repo_name=repo.repo_name))
76 h.route_path('repo_summary', repo_name=repo.repo_name))
77 raise HTTPBadRequest()
77 raise HTTPBadRequest()
78
78
79 @LoginRequired()
79 @LoginRequired()
80 @HasRepoPermissionAnyDecorator(
80 @HasRepoPermissionAnyDecorator(
81 'repository.read', 'repository.write', 'repository.admin')
81 'repository.read', 'repository.write', 'repository.admin')
82 @view_config(
82 @view_config(
83 route_name='repo_compare_select', request_method='GET',
83 route_name='repo_compare_select', request_method='GET',
84 renderer='rhodecode:templates/compare/compare_diff.mako')
84 renderer='rhodecode:templates/compare/compare_diff.mako')
85 def compare_select(self):
85 def compare_select(self):
86 _ = self.request.translate
86 _ = self.request.translate
87 c = self.load_default_context()
87 c = self.load_default_context()
88
88
89 source_repo = self.db_repo_name
89 source_repo = self.db_repo_name
90 target_repo = self.request.GET.get('target_repo', source_repo)
90 target_repo = self.request.GET.get('target_repo', source_repo)
91 c.source_repo = Repository.get_by_repo_name(source_repo)
91 c.source_repo = Repository.get_by_repo_name(source_repo)
92 c.target_repo = Repository.get_by_repo_name(target_repo)
92 c.target_repo = Repository.get_by_repo_name(target_repo)
93
93
94 if c.source_repo is None or c.target_repo is None:
94 if c.source_repo is None or c.target_repo is None:
95 raise HTTPNotFound()
95 raise HTTPNotFound()
96
96
97 c.compare_home = True
97 c.compare_home = True
98 c.commit_ranges = []
98 c.commit_ranges = []
99 c.collapse_all_commits = False
99 c.collapse_all_commits = False
100 c.diffset = None
100 c.diffset = None
101 c.limited_diff = False
101 c.limited_diff = False
102 c.source_ref = c.target_ref = _('Select commit')
102 c.source_ref = c.target_ref = _('Select commit')
103 c.source_ref_type = ""
103 c.source_ref_type = ""
104 c.target_ref_type = ""
104 c.target_ref_type = ""
105 c.commit_statuses = ChangesetStatus.STATUSES
105 c.commit_statuses = ChangesetStatus.STATUSES
106 c.preview_mode = False
106 c.preview_mode = False
107 c.file_path = None
107 c.file_path = None
108
108
109 return self._get_template_context(c)
109 return self._get_template_context(c)
110
110
111 @LoginRequired()
111 @LoginRequired()
112 @HasRepoPermissionAnyDecorator(
112 @HasRepoPermissionAnyDecorator(
113 'repository.read', 'repository.write', 'repository.admin')
113 'repository.read', 'repository.write', 'repository.admin')
114 @view_config(
114 @view_config(
115 route_name='repo_compare', request_method='GET',
115 route_name='repo_compare', request_method='GET',
116 renderer=None)
116 renderer=None)
117 def compare(self):
117 def compare(self):
118 _ = self.request.translate
118 _ = self.request.translate
119 c = self.load_default_context()
119 c = self.load_default_context()
120
120
121 source_ref_type = self.request.matchdict['source_ref_type']
121 source_ref_type = self.request.matchdict['source_ref_type']
122 source_ref = self.request.matchdict['source_ref']
122 source_ref = self.request.matchdict['source_ref']
123 target_ref_type = self.request.matchdict['target_ref_type']
123 target_ref_type = self.request.matchdict['target_ref_type']
124 target_ref = self.request.matchdict['target_ref']
124 target_ref = self.request.matchdict['target_ref']
125
125
126 # source_ref will be evaluated in source_repo
126 # source_ref will be evaluated in source_repo
127 source_repo_name = self.db_repo_name
127 source_repo_name = self.db_repo_name
128 source_path, source_id = parse_path_ref(source_ref)
128 source_path, source_id = parse_path_ref(source_ref)
129
129
130 # target_ref will be evaluated in target_repo
130 # target_ref will be evaluated in target_repo
131 target_repo_name = self.request.GET.get('target_repo', source_repo_name)
131 target_repo_name = self.request.GET.get('target_repo', source_repo_name)
132 target_path, target_id = parse_path_ref(
132 target_path, target_id = parse_path_ref(
133 target_ref, default_path=self.request.GET.get('f_path', ''))
133 target_ref, default_path=self.request.GET.get('f_path', ''))
134
134
135 # if merge is True
135 # if merge is True
136 # Show what changes since the shared ancestor commit of target/source
136 # Show what changes since the shared ancestor commit of target/source
137 # the source would get if it was merged with target. Only commits
137 # the source would get if it was merged with target. Only commits
138 # which are in target but not in source will be shown.
138 # which are in target but not in source will be shown.
139 merge = str2bool(self.request.GET.get('merge'))
139 merge = str2bool(self.request.GET.get('merge'))
140 # if merge is False
140 # if merge is False
141 # Show a raw diff of source/target refs even if no ancestor exists
141 # Show a raw diff of source/target refs even if no ancestor exists
142
142
143 # c.fulldiff disables cut_off_limit
143 # c.fulldiff disables cut_off_limit
144 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
144 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
145
145
146 # fetch global flags of ignore ws or context lines
146 # fetch global flags of ignore ws or context lines
147 diff_context = diffs.get_diff_context(self.request)
147 diff_context = diffs.get_diff_context(self.request)
148 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
148 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
149
149
150 c.file_path = target_path
150 c.file_path = target_path
151 c.commit_statuses = ChangesetStatus.STATUSES
151 c.commit_statuses = ChangesetStatus.STATUSES
152
152
153 # if partial, returns just compare_commits.html (commits log)
153 # if partial, returns just compare_commits.html (commits log)
154 partial = self.request.is_xhr
154 partial = self.request.is_xhr
155
155
156 # swap url for compare_diff page
156 # swap url for compare_diff page
157 c.swap_url = h.route_path(
157 c.swap_url = h.route_path(
158 'repo_compare',
158 'repo_compare',
159 repo_name=target_repo_name,
159 repo_name=target_repo_name,
160 source_ref_type=target_ref_type,
160 source_ref_type=target_ref_type,
161 source_ref=target_ref,
161 source_ref=target_ref,
162 target_repo=source_repo_name,
162 target_repo=source_repo_name,
163 target_ref_type=source_ref_type,
163 target_ref_type=source_ref_type,
164 target_ref=source_ref,
164 target_ref=source_ref,
165 _query=dict(merge=merge and '1' or '', f_path=target_path))
165 _query=dict(merge=merge and '1' or '', f_path=target_path))
166
166
167 source_repo = Repository.get_by_repo_name(source_repo_name)
167 source_repo = Repository.get_by_repo_name(source_repo_name)
168 target_repo = Repository.get_by_repo_name(target_repo_name)
168 target_repo = Repository.get_by_repo_name(target_repo_name)
169
169
170 if source_repo is None:
170 if source_repo is None:
171 log.error('Could not find the source repo: {}'
171 log.error('Could not find the source repo: {}'
172 .format(source_repo_name))
172 .format(source_repo_name))
173 h.flash(_('Could not find the source repo: `{}`')
173 h.flash(_('Could not find the source repo: `{}`')
174 .format(h.escape(source_repo_name)), category='error')
174 .format(h.escape(source_repo_name)), category='error')
175 raise HTTPFound(
175 raise HTTPFound(
176 h.route_path('repo_compare_select', repo_name=self.db_repo_name))
176 h.route_path('repo_compare_select', repo_name=self.db_repo_name))
177
177
178 if target_repo is None:
178 if target_repo is None:
179 log.error('Could not find the target repo: {}'
179 log.error('Could not find the target repo: {}'
180 .format(source_repo_name))
180 .format(source_repo_name))
181 h.flash(_('Could not find the target repo: `{}`')
181 h.flash(_('Could not find the target repo: `{}`')
182 .format(h.escape(target_repo_name)), category='error')
182 .format(h.escape(target_repo_name)), category='error')
183 raise HTTPFound(
183 raise HTTPFound(
184 h.route_path('repo_compare_select', repo_name=self.db_repo_name))
184 h.route_path('repo_compare_select', repo_name=self.db_repo_name))
185
185
186 source_scm = source_repo.scm_instance()
186 source_scm = source_repo.scm_instance()
187 target_scm = target_repo.scm_instance()
187 target_scm = target_repo.scm_instance()
188
188
189 source_alias = source_scm.alias
189 source_alias = source_scm.alias
190 target_alias = target_scm.alias
190 target_alias = target_scm.alias
191 if source_alias != target_alias:
191 if source_alias != target_alias:
192 msg = _('The comparison of two different kinds of remote repos '
192 msg = _('The comparison of two different kinds of remote repos '
193 'is not available')
193 'is not available')
194 log.error(msg)
194 log.error(msg)
195 h.flash(msg, category='error')
195 h.flash(msg, category='error')
196 raise HTTPFound(
196 raise HTTPFound(
197 h.route_path('repo_compare_select', repo_name=self.db_repo_name))
197 h.route_path('repo_compare_select', repo_name=self.db_repo_name))
198
198
199 source_commit = self._get_commit_or_redirect(
199 source_commit = self._get_commit_or_redirect(
200 ref=source_id, ref_type=source_ref_type, repo=source_repo,
200 ref=source_id, ref_type=source_ref_type, repo=source_repo,
201 partial=partial)
201 partial=partial)
202 target_commit = self._get_commit_or_redirect(
202 target_commit = self._get_commit_or_redirect(
203 ref=target_id, ref_type=target_ref_type, repo=target_repo,
203 ref=target_id, ref_type=target_ref_type, repo=target_repo,
204 partial=partial)
204 partial=partial)
205
205
206 c.compare_home = False
206 c.compare_home = False
207 c.source_repo = source_repo
207 c.source_repo = source_repo
208 c.target_repo = target_repo
208 c.target_repo = target_repo
209 c.source_ref = source_ref
209 c.source_ref = source_ref
210 c.target_ref = target_ref
210 c.target_ref = target_ref
211 c.source_ref_type = source_ref_type
211 c.source_ref_type = source_ref_type
212 c.target_ref_type = target_ref_type
212 c.target_ref_type = target_ref_type
213
213
214 pre_load = ["author", "branch", "date", "message"]
214 pre_load = ["author", "branch", "date", "message"]
215 c.ancestor = None
215 c.ancestor = None
216
216
217 if c.file_path:
217 try:
218 if source_commit == target_commit:
218 c.commit_ranges = source_scm.compare(
219 c.commit_ranges = []
219 source_commit.raw_id, target_commit.raw_id,
220 else:
220 target_scm, merge, pre_load=pre_load) or []
221 c.commit_ranges = [source_commit, target_commit]
221 if merge:
222 else:
222 c.ancestor = source_scm.get_common_ancestor(
223 try:
223 source_commit.raw_id, target_commit.raw_id, target_scm)
224 c.commit_ranges = source_scm.compare(
224 except RepositoryRequirementError:
225 source_commit.raw_id, target_commit.raw_id,
225 msg = _('Could not compare repos with different '
226 target_scm, merge, pre_load=pre_load)
226 'large file settings')
227 if merge:
227 log.error(msg)
228 c.ancestor = source_scm.get_common_ancestor(
228 if partial:
229 source_commit.raw_id, target_commit.raw_id, target_scm)
229 return Response(msg)
230 except RepositoryRequirementError:
230 h.flash(msg, category='error')
231 msg = _('Could not compare repos with different '
231 raise HTTPFound(
232 'large file settings')
232 h.route_path('repo_compare_select',
233 log.error(msg)
233 repo_name=self.db_repo_name))
234 if partial:
235 return Response(msg)
236 h.flash(msg, category='error')
237 raise HTTPFound(
238 h.route_path('repo_compare_select',
239 repo_name=self.db_repo_name))
240
234
241 c.statuses = self.db_repo.statuses(
235 c.statuses = self.db_repo.statuses(
242 [x.raw_id for x in c.commit_ranges])
236 [x.raw_id for x in c.commit_ranges])
243
237
244 # auto collapse if we have more than limit
238 # auto collapse if we have more than limit
245 collapse_limit = diffs.DiffProcessor._collapse_commits_over
239 collapse_limit = diffs.DiffProcessor._collapse_commits_over
246 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
240 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
247
241
248 if partial: # for PR ajax commits loader
242 if partial: # for PR ajax commits loader
249 if not c.ancestor:
243 if not c.ancestor:
250 return Response('') # cannot merge if there is no ancestor
244 return Response('') # cannot merge if there is no ancestor
251
245
252 html = render(
246 html = render(
253 'rhodecode:templates/compare/compare_commits.mako',
247 'rhodecode:templates/compare/compare_commits.mako',
254 self._get_template_context(c), self.request)
248 self._get_template_context(c), self.request)
255 return Response(html)
249 return Response(html)
256
250
257 if c.ancestor:
251 if c.ancestor:
258 # case we want a simple diff without incoming commits,
252 # case we want a simple diff without incoming commits,
259 # previewing what will be merged.
253 # previewing what will be merged.
260 # Make the diff on target repo (which is known to have target_ref)
254 # Make the diff on target repo (which is known to have target_ref)
261 log.debug('Using ancestor %s as source_ref instead of %s',
255 log.debug('Using ancestor %s as source_ref instead of %s',
262 c.ancestor, source_ref)
256 c.ancestor, source_ref)
263 source_repo = target_repo
257 source_repo = target_repo
264 source_commit = target_repo.get_commit(commit_id=c.ancestor)
258 source_commit = target_repo.get_commit(commit_id=c.ancestor)
265
259
266 # diff_limit will cut off the whole diff if the limit is applied
260 # diff_limit will cut off the whole diff if the limit is applied
267 # otherwise it will just hide the big files from the front-end
261 # otherwise it will just hide the big files from the front-end
268 diff_limit = c.visual.cut_off_limit_diff
262 diff_limit = c.visual.cut_off_limit_diff
269 file_limit = c.visual.cut_off_limit_file
263 file_limit = c.visual.cut_off_limit_file
270
264
271 log.debug('calculating diff between '
265 log.debug('calculating diff between '
272 'source_ref:%s and target_ref:%s for repo `%s`',
266 'source_ref:%s and target_ref:%s for repo `%s`',
273 source_commit, target_commit,
267 source_commit, target_commit,
274 safe_unicode(source_repo.scm_instance().path))
268 safe_unicode(source_repo.scm_instance().path))
275
269
276 if source_commit.repository != target_commit.repository:
270 if source_commit.repository != target_commit.repository:
277 msg = _(
271 msg = _(
278 "Repositories unrelated. "
272 "Repositories unrelated. "
279 "Cannot compare commit %(commit1)s from repository %(repo1)s "
273 "Cannot compare commit %(commit1)s from repository %(repo1)s "
280 "with commit %(commit2)s from repository %(repo2)s.") % {
274 "with commit %(commit2)s from repository %(repo2)s.") % {
281 'commit1': h.show_id(source_commit),
275 'commit1': h.show_id(source_commit),
282 'repo1': source_repo.repo_name,
276 'repo1': source_repo.repo_name,
283 'commit2': h.show_id(target_commit),
277 'commit2': h.show_id(target_commit),
284 'repo2': target_repo.repo_name,
278 'repo2': target_repo.repo_name,
285 }
279 }
286 h.flash(msg, category='error')
280 h.flash(msg, category='error')
287 raise HTTPFound(
281 raise HTTPFound(
288 h.route_path('repo_compare_select',
282 h.route_path('repo_compare_select',
289 repo_name=self.db_repo_name))
283 repo_name=self.db_repo_name))
290
284
291 txt_diff = source_repo.scm_instance().get_diff(
285 txt_diff = source_repo.scm_instance().get_diff(
292 commit1=source_commit, commit2=target_commit,
286 commit1=source_commit, commit2=target_commit,
293 path=target_path, path1=source_path,
287 path=target_path, path1=source_path,
294 ignore_whitespace=hide_whitespace_changes, context=diff_context)
288 ignore_whitespace=hide_whitespace_changes, context=diff_context)
295
289
296 diff_processor = diffs.DiffProcessor(
290 diff_processor = diffs.DiffProcessor(
297 txt_diff, format='newdiff', diff_limit=diff_limit,
291 txt_diff, format='newdiff', diff_limit=diff_limit,
298 file_limit=file_limit, show_full_diff=c.fulldiff)
292 file_limit=file_limit, show_full_diff=c.fulldiff)
299 _parsed = diff_processor.prepare()
293 _parsed = diff_processor.prepare()
300
294
301 diffset = codeblocks.DiffSet(
295 diffset = codeblocks.DiffSet(
302 repo_name=source_repo.repo_name,
296 repo_name=source_repo.repo_name,
303 source_node_getter=codeblocks.diffset_node_getter(source_commit),
297 source_node_getter=codeblocks.diffset_node_getter(source_commit),
304 target_repo_name=self.db_repo_name,
298 target_repo_name=self.db_repo_name,
305 target_node_getter=codeblocks.diffset_node_getter(target_commit),
299 target_node_getter=codeblocks.diffset_node_getter(target_commit),
306 )
300 )
307 c.diffset = self.path_filter.render_patchset_filtered(
301 c.diffset = self.path_filter.render_patchset_filtered(
308 diffset, _parsed, source_ref, target_ref)
302 diffset, _parsed, source_ref, target_ref)
309
303
310 c.preview_mode = merge
304 c.preview_mode = merge
311 c.source_commit = source_commit
305 c.source_commit = source_commit
312 c.target_commit = target_commit
306 c.target_commit = target_commit
313
307
314 html = render(
308 html = render(
315 'rhodecode:templates/compare/compare_diff.mako',
309 'rhodecode:templates/compare/compare_diff.mako',
316 self._get_template_context(c), self.request)
310 self._get_template_context(c), self.request)
317 return Response(html) No newline at end of file
311 return Response(html)
@@ -1,1860 +1,1861 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2014-2019 RhodeCode GmbH
3 # Copyright (C) 2014-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 """
21 """
22 Base module for all VCS systems
22 Base module for all VCS systems
23 """
23 """
24 import os
24 import os
25 import re
25 import re
26 import time
26 import time
27 import shutil
27 import shutil
28 import datetime
28 import datetime
29 import fnmatch
29 import fnmatch
30 import itertools
30 import itertools
31 import logging
31 import logging
32 import collections
32 import collections
33 import warnings
33 import warnings
34
34
35 from zope.cachedescriptors.property import Lazy as LazyProperty
35 from zope.cachedescriptors.property import Lazy as LazyProperty
36 from zope.cachedescriptors.property import CachedProperty
36 from zope.cachedescriptors.property import CachedProperty
37
37
38 from pyramid import compat
38 from pyramid import compat
39
39
40 from rhodecode.translation import lazy_ugettext
40 from rhodecode.translation import lazy_ugettext
41 from rhodecode.lib.utils2 import safe_str, safe_unicode
41 from rhodecode.lib.utils2 import safe_str, safe_unicode
42 from rhodecode.lib.vcs import connection
42 from rhodecode.lib.vcs import connection
43 from rhodecode.lib.vcs.utils import author_name, author_email
43 from rhodecode.lib.vcs.utils import author_name, author_email
44 from rhodecode.lib.vcs.conf import settings
44 from rhodecode.lib.vcs.conf import settings
45 from rhodecode.lib.vcs.exceptions import (
45 from rhodecode.lib.vcs.exceptions import (
46 CommitError, EmptyRepositoryError, NodeAlreadyAddedError,
46 CommitError, EmptyRepositoryError, NodeAlreadyAddedError,
47 NodeAlreadyChangedError, NodeAlreadyExistsError, NodeAlreadyRemovedError,
47 NodeAlreadyChangedError, NodeAlreadyExistsError, NodeAlreadyRemovedError,
48 NodeDoesNotExistError, NodeNotChangedError, VCSError,
48 NodeDoesNotExistError, NodeNotChangedError, VCSError,
49 ImproperArchiveTypeError, BranchDoesNotExistError, CommitDoesNotExistError,
49 ImproperArchiveTypeError, BranchDoesNotExistError, CommitDoesNotExistError,
50 RepositoryError)
50 RepositoryError)
51
51
52
52
53 log = logging.getLogger(__name__)
53 log = logging.getLogger(__name__)
54
54
55
55
56 FILEMODE_DEFAULT = 0o100644
56 FILEMODE_DEFAULT = 0o100644
57 FILEMODE_EXECUTABLE = 0o100755
57 FILEMODE_EXECUTABLE = 0o100755
58 EMPTY_COMMIT_ID = '0' * 40
58
59
59 Reference = collections.namedtuple('Reference', ('type', 'name', 'commit_id'))
60 Reference = collections.namedtuple('Reference', ('type', 'name', 'commit_id'))
60
61
61
62
62 class MergeFailureReason(object):
63 class MergeFailureReason(object):
63 """
64 """
64 Enumeration with all the reasons why the server side merge could fail.
65 Enumeration with all the reasons why the server side merge could fail.
65
66
66 DO NOT change the number of the reasons, as they may be stored in the
67 DO NOT change the number of the reasons, as they may be stored in the
67 database.
68 database.
68
69
69 Changing the name of a reason is acceptable and encouraged to deprecate old
70 Changing the name of a reason is acceptable and encouraged to deprecate old
70 reasons.
71 reasons.
71 """
72 """
72
73
73 # Everything went well.
74 # Everything went well.
74 NONE = 0
75 NONE = 0
75
76
76 # An unexpected exception was raised. Check the logs for more details.
77 # An unexpected exception was raised. Check the logs for more details.
77 UNKNOWN = 1
78 UNKNOWN = 1
78
79
79 # The merge was not successful, there are conflicts.
80 # The merge was not successful, there are conflicts.
80 MERGE_FAILED = 2
81 MERGE_FAILED = 2
81
82
82 # The merge succeeded but we could not push it to the target repository.
83 # The merge succeeded but we could not push it to the target repository.
83 PUSH_FAILED = 3
84 PUSH_FAILED = 3
84
85
85 # The specified target is not a head in the target repository.
86 # The specified target is not a head in the target repository.
86 TARGET_IS_NOT_HEAD = 4
87 TARGET_IS_NOT_HEAD = 4
87
88
88 # The source repository contains more branches than the target. Pushing
89 # The source repository contains more branches than the target. Pushing
89 # the merge will create additional branches in the target.
90 # the merge will create additional branches in the target.
90 HG_SOURCE_HAS_MORE_BRANCHES = 5
91 HG_SOURCE_HAS_MORE_BRANCHES = 5
91
92
92 # The target reference has multiple heads. That does not allow to correctly
93 # The target reference has multiple heads. That does not allow to correctly
93 # identify the target location. This could only happen for mercurial
94 # identify the target location. This could only happen for mercurial
94 # branches.
95 # branches.
95 HG_TARGET_HAS_MULTIPLE_HEADS = 6
96 HG_TARGET_HAS_MULTIPLE_HEADS = 6
96
97
97 # The target repository is locked
98 # The target repository is locked
98 TARGET_IS_LOCKED = 7
99 TARGET_IS_LOCKED = 7
99
100
100 # Deprecated, use MISSING_TARGET_REF or MISSING_SOURCE_REF instead.
101 # Deprecated, use MISSING_TARGET_REF or MISSING_SOURCE_REF instead.
101 # A involved commit could not be found.
102 # A involved commit could not be found.
102 _DEPRECATED_MISSING_COMMIT = 8
103 _DEPRECATED_MISSING_COMMIT = 8
103
104
104 # The target repo reference is missing.
105 # The target repo reference is missing.
105 MISSING_TARGET_REF = 9
106 MISSING_TARGET_REF = 9
106
107
107 # The source repo reference is missing.
108 # The source repo reference is missing.
108 MISSING_SOURCE_REF = 10
109 MISSING_SOURCE_REF = 10
109
110
110 # The merge was not successful, there are conflicts related to sub
111 # The merge was not successful, there are conflicts related to sub
111 # repositories.
112 # repositories.
112 SUBREPO_MERGE_FAILED = 11
113 SUBREPO_MERGE_FAILED = 11
113
114
114
115
115 class UpdateFailureReason(object):
116 class UpdateFailureReason(object):
116 """
117 """
117 Enumeration with all the reasons why the pull request update could fail.
118 Enumeration with all the reasons why the pull request update could fail.
118
119
119 DO NOT change the number of the reasons, as they may be stored in the
120 DO NOT change the number of the reasons, as they may be stored in the
120 database.
121 database.
121
122
122 Changing the name of a reason is acceptable and encouraged to deprecate old
123 Changing the name of a reason is acceptable and encouraged to deprecate old
123 reasons.
124 reasons.
124 """
125 """
125
126
126 # Everything went well.
127 # Everything went well.
127 NONE = 0
128 NONE = 0
128
129
129 # An unexpected exception was raised. Check the logs for more details.
130 # An unexpected exception was raised. Check the logs for more details.
130 UNKNOWN = 1
131 UNKNOWN = 1
131
132
132 # The pull request is up to date.
133 # The pull request is up to date.
133 NO_CHANGE = 2
134 NO_CHANGE = 2
134
135
135 # The pull request has a reference type that is not supported for update.
136 # The pull request has a reference type that is not supported for update.
136 WRONG_REF_TYPE = 3
137 WRONG_REF_TYPE = 3
137
138
138 # Update failed because the target reference is missing.
139 # Update failed because the target reference is missing.
139 MISSING_TARGET_REF = 4
140 MISSING_TARGET_REF = 4
140
141
141 # Update failed because the source reference is missing.
142 # Update failed because the source reference is missing.
142 MISSING_SOURCE_REF = 5
143 MISSING_SOURCE_REF = 5
143
144
144
145
145 class MergeResponse(object):
146 class MergeResponse(object):
146
147
147 # uses .format(**metadata) for variables
148 # uses .format(**metadata) for variables
148 MERGE_STATUS_MESSAGES = {
149 MERGE_STATUS_MESSAGES = {
149 MergeFailureReason.NONE: lazy_ugettext(
150 MergeFailureReason.NONE: lazy_ugettext(
150 u'This pull request can be automatically merged.'),
151 u'This pull request can be automatically merged.'),
151 MergeFailureReason.UNKNOWN: lazy_ugettext(
152 MergeFailureReason.UNKNOWN: lazy_ugettext(
152 u'This pull request cannot be merged because of an unhandled exception. '
153 u'This pull request cannot be merged because of an unhandled exception. '
153 u'{exception}'),
154 u'{exception}'),
154 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
155 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
155 u'This pull request cannot be merged because of merge conflicts.'),
156 u'This pull request cannot be merged because of merge conflicts.'),
156 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
157 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
157 u'This pull request could not be merged because push to '
158 u'This pull request could not be merged because push to '
158 u'target:`{target}@{merge_commit}` failed.'),
159 u'target:`{target}@{merge_commit}` failed.'),
159 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
160 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
160 u'This pull request cannot be merged because the target '
161 u'This pull request cannot be merged because the target '
161 u'`{target_ref.name}` is not a head.'),
162 u'`{target_ref.name}` is not a head.'),
162 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
163 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
163 u'This pull request cannot be merged because the source contains '
164 u'This pull request cannot be merged because the source contains '
164 u'more branches than the target.'),
165 u'more branches than the target.'),
165 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
166 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
166 u'This pull request cannot be merged because the target `{target_ref.name}` '
167 u'This pull request cannot be merged because the target `{target_ref.name}` '
167 u'has multiple heads: `{heads}`.'),
168 u'has multiple heads: `{heads}`.'),
168 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
169 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
169 u'This pull request cannot be merged because the target repository is '
170 u'This pull request cannot be merged because the target repository is '
170 u'locked by {locked_by}.'),
171 u'locked by {locked_by}.'),
171
172
172 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
173 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
173 u'This pull request cannot be merged because the target '
174 u'This pull request cannot be merged because the target '
174 u'reference `{target_ref.name}` is missing.'),
175 u'reference `{target_ref.name}` is missing.'),
175 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
176 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
176 u'This pull request cannot be merged because the source '
177 u'This pull request cannot be merged because the source '
177 u'reference `{source_ref.name}` is missing.'),
178 u'reference `{source_ref.name}` is missing.'),
178 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
179 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
179 u'This pull request cannot be merged because of conflicts related '
180 u'This pull request cannot be merged because of conflicts related '
180 u'to sub repositories.'),
181 u'to sub repositories.'),
181
182
182 # Deprecations
183 # Deprecations
183 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
184 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
184 u'This pull request cannot be merged because the target or the '
185 u'This pull request cannot be merged because the target or the '
185 u'source reference is missing.'),
186 u'source reference is missing.'),
186
187
187 }
188 }
188
189
189 def __init__(self, possible, executed, merge_ref, failure_reason, metadata=None):
190 def __init__(self, possible, executed, merge_ref, failure_reason, metadata=None):
190 self.possible = possible
191 self.possible = possible
191 self.executed = executed
192 self.executed = executed
192 self.merge_ref = merge_ref
193 self.merge_ref = merge_ref
193 self.failure_reason = failure_reason
194 self.failure_reason = failure_reason
194 self.metadata = metadata or {}
195 self.metadata = metadata or {}
195
196
196 def __repr__(self):
197 def __repr__(self):
197 return '<MergeResponse:{} {}>'.format(self.label, self.failure_reason)
198 return '<MergeResponse:{} {}>'.format(self.label, self.failure_reason)
198
199
199 def __eq__(self, other):
200 def __eq__(self, other):
200 same_instance = isinstance(other, self.__class__)
201 same_instance = isinstance(other, self.__class__)
201 return same_instance \
202 return same_instance \
202 and self.possible == other.possible \
203 and self.possible == other.possible \
203 and self.executed == other.executed \
204 and self.executed == other.executed \
204 and self.failure_reason == other.failure_reason
205 and self.failure_reason == other.failure_reason
205
206
206 @property
207 @property
207 def label(self):
208 def label(self):
208 label_dict = dict((v, k) for k, v in MergeFailureReason.__dict__.items() if
209 label_dict = dict((v, k) for k, v in MergeFailureReason.__dict__.items() if
209 not k.startswith('_'))
210 not k.startswith('_'))
210 return label_dict.get(self.failure_reason)
211 return label_dict.get(self.failure_reason)
211
212
212 @property
213 @property
213 def merge_status_message(self):
214 def merge_status_message(self):
214 """
215 """
215 Return a human friendly error message for the given merge status code.
216 Return a human friendly error message for the given merge status code.
216 """
217 """
217 msg = safe_unicode(self.MERGE_STATUS_MESSAGES[self.failure_reason])
218 msg = safe_unicode(self.MERGE_STATUS_MESSAGES[self.failure_reason])
218 try:
219 try:
219 return msg.format(**self.metadata)
220 return msg.format(**self.metadata)
220 except Exception:
221 except Exception:
221 log.exception('Failed to format %s message', self)
222 log.exception('Failed to format %s message', self)
222 return msg
223 return msg
223
224
224 def asdict(self):
225 def asdict(self):
225 data = {}
226 data = {}
226 for k in ['possible', 'executed', 'merge_ref', 'failure_reason',
227 for k in ['possible', 'executed', 'merge_ref', 'failure_reason',
227 'merge_status_message']:
228 'merge_status_message']:
228 data[k] = getattr(self, k)
229 data[k] = getattr(self, k)
229 return data
230 return data
230
231
231
232
232 class BaseRepository(object):
233 class BaseRepository(object):
233 """
234 """
234 Base Repository for final backends
235 Base Repository for final backends
235
236
236 .. attribute:: DEFAULT_BRANCH_NAME
237 .. attribute:: DEFAULT_BRANCH_NAME
237
238
238 name of default branch (i.e. "trunk" for svn, "master" for git etc.
239 name of default branch (i.e. "trunk" for svn, "master" for git etc.
239
240
240 .. attribute:: commit_ids
241 .. attribute:: commit_ids
241
242
242 list of all available commit ids, in ascending order
243 list of all available commit ids, in ascending order
243
244
244 .. attribute:: path
245 .. attribute:: path
245
246
246 absolute path to the repository
247 absolute path to the repository
247
248
248 .. attribute:: bookmarks
249 .. attribute:: bookmarks
249
250
250 Mapping from name to :term:`Commit ID` of the bookmark. Empty in case
251 Mapping from name to :term:`Commit ID` of the bookmark. Empty in case
251 there are no bookmarks or the backend implementation does not support
252 there are no bookmarks or the backend implementation does not support
252 bookmarks.
253 bookmarks.
253
254
254 .. attribute:: tags
255 .. attribute:: tags
255
256
256 Mapping from name to :term:`Commit ID` of the tag.
257 Mapping from name to :term:`Commit ID` of the tag.
257
258
258 """
259 """
259
260
260 DEFAULT_BRANCH_NAME = None
261 DEFAULT_BRANCH_NAME = None
261 DEFAULT_CONTACT = u"Unknown"
262 DEFAULT_CONTACT = u"Unknown"
262 DEFAULT_DESCRIPTION = u"unknown"
263 DEFAULT_DESCRIPTION = u"unknown"
263 EMPTY_COMMIT_ID = '0' * 40
264 EMPTY_COMMIT_ID = '0' * 40
264
265
265 path = None
266 path = None
266 _commit_ids_ver = 0
267 _commit_ids_ver = 0
267
268
268 def __init__(self, repo_path, config=None, create=False, **kwargs):
269 def __init__(self, repo_path, config=None, create=False, **kwargs):
269 """
270 """
270 Initializes repository. Raises RepositoryError if repository could
271 Initializes repository. Raises RepositoryError if repository could
271 not be find at the given ``repo_path`` or directory at ``repo_path``
272 not be find at the given ``repo_path`` or directory at ``repo_path``
272 exists and ``create`` is set to True.
273 exists and ``create`` is set to True.
273
274
274 :param repo_path: local path of the repository
275 :param repo_path: local path of the repository
275 :param config: repository configuration
276 :param config: repository configuration
276 :param create=False: if set to True, would try to create repository.
277 :param create=False: if set to True, would try to create repository.
277 :param src_url=None: if set, should be proper url from which repository
278 :param src_url=None: if set, should be proper url from which repository
278 would be cloned; requires ``create`` parameter to be set to True -
279 would be cloned; requires ``create`` parameter to be set to True -
279 raises RepositoryError if src_url is set and create evaluates to
280 raises RepositoryError if src_url is set and create evaluates to
280 False
281 False
281 """
282 """
282 raise NotImplementedError
283 raise NotImplementedError
283
284
284 def __repr__(self):
285 def __repr__(self):
285 return '<%s at %s>' % (self.__class__.__name__, self.path)
286 return '<%s at %s>' % (self.__class__.__name__, self.path)
286
287
287 def __len__(self):
288 def __len__(self):
288 return self.count()
289 return self.count()
289
290
290 def __eq__(self, other):
291 def __eq__(self, other):
291 same_instance = isinstance(other, self.__class__)
292 same_instance = isinstance(other, self.__class__)
292 return same_instance and other.path == self.path
293 return same_instance and other.path == self.path
293
294
294 def __ne__(self, other):
295 def __ne__(self, other):
295 return not self.__eq__(other)
296 return not self.__eq__(other)
296
297
297 def get_create_shadow_cache_pr_path(self, db_repo):
298 def get_create_shadow_cache_pr_path(self, db_repo):
298 path = db_repo.cached_diffs_dir
299 path = db_repo.cached_diffs_dir
299 if not os.path.exists(path):
300 if not os.path.exists(path):
300 os.makedirs(path, 0o755)
301 os.makedirs(path, 0o755)
301 return path
302 return path
302
303
303 @classmethod
304 @classmethod
304 def get_default_config(cls, default=None):
305 def get_default_config(cls, default=None):
305 config = Config()
306 config = Config()
306 if default and isinstance(default, list):
307 if default and isinstance(default, list):
307 for section, key, val in default:
308 for section, key, val in default:
308 config.set(section, key, val)
309 config.set(section, key, val)
309 return config
310 return config
310
311
311 @LazyProperty
312 @LazyProperty
312 def _remote(self):
313 def _remote(self):
313 raise NotImplementedError
314 raise NotImplementedError
314
315
315 def _heads(self, branch=None):
316 def _heads(self, branch=None):
316 return []
317 return []
317
318
318 @LazyProperty
319 @LazyProperty
319 def EMPTY_COMMIT(self):
320 def EMPTY_COMMIT(self):
320 return EmptyCommit(self.EMPTY_COMMIT_ID)
321 return EmptyCommit(self.EMPTY_COMMIT_ID)
321
322
322 @LazyProperty
323 @LazyProperty
323 def alias(self):
324 def alias(self):
324 for k, v in settings.BACKENDS.items():
325 for k, v in settings.BACKENDS.items():
325 if v.split('.')[-1] == str(self.__class__.__name__):
326 if v.split('.')[-1] == str(self.__class__.__name__):
326 return k
327 return k
327
328
328 @LazyProperty
329 @LazyProperty
329 def name(self):
330 def name(self):
330 return safe_unicode(os.path.basename(self.path))
331 return safe_unicode(os.path.basename(self.path))
331
332
332 @LazyProperty
333 @LazyProperty
333 def description(self):
334 def description(self):
334 raise NotImplementedError
335 raise NotImplementedError
335
336
336 def refs(self):
337 def refs(self):
337 """
338 """
338 returns a `dict` with branches, bookmarks, tags, and closed_branches
339 returns a `dict` with branches, bookmarks, tags, and closed_branches
339 for this repository
340 for this repository
340 """
341 """
341 return dict(
342 return dict(
342 branches=self.branches,
343 branches=self.branches,
343 branches_closed=self.branches_closed,
344 branches_closed=self.branches_closed,
344 tags=self.tags,
345 tags=self.tags,
345 bookmarks=self.bookmarks
346 bookmarks=self.bookmarks
346 )
347 )
347
348
348 @LazyProperty
349 @LazyProperty
349 def branches(self):
350 def branches(self):
350 """
351 """
351 A `dict` which maps branch names to commit ids.
352 A `dict` which maps branch names to commit ids.
352 """
353 """
353 raise NotImplementedError
354 raise NotImplementedError
354
355
355 @LazyProperty
356 @LazyProperty
356 def branches_closed(self):
357 def branches_closed(self):
357 """
358 """
358 A `dict` which maps tags names to commit ids.
359 A `dict` which maps tags names to commit ids.
359 """
360 """
360 raise NotImplementedError
361 raise NotImplementedError
361
362
362 @LazyProperty
363 @LazyProperty
363 def bookmarks(self):
364 def bookmarks(self):
364 """
365 """
365 A `dict` which maps tags names to commit ids.
366 A `dict` which maps tags names to commit ids.
366 """
367 """
367 raise NotImplementedError
368 raise NotImplementedError
368
369
369 @LazyProperty
370 @LazyProperty
370 def tags(self):
371 def tags(self):
371 """
372 """
372 A `dict` which maps tags names to commit ids.
373 A `dict` which maps tags names to commit ids.
373 """
374 """
374 raise NotImplementedError
375 raise NotImplementedError
375
376
376 @LazyProperty
377 @LazyProperty
377 def size(self):
378 def size(self):
378 """
379 """
379 Returns combined size in bytes for all repository files
380 Returns combined size in bytes for all repository files
380 """
381 """
381 tip = self.get_commit()
382 tip = self.get_commit()
382 return tip.size
383 return tip.size
383
384
384 def size_at_commit(self, commit_id):
385 def size_at_commit(self, commit_id):
385 commit = self.get_commit(commit_id)
386 commit = self.get_commit(commit_id)
386 return commit.size
387 return commit.size
387
388
388 def is_empty(self):
389 def is_empty(self):
389 return self._remote.is_empty()
390 return self._remote.is_empty()
390
391
391 @staticmethod
392 @staticmethod
392 def check_url(url, config):
393 def check_url(url, config):
393 """
394 """
394 Function will check given url and try to verify if it's a valid
395 Function will check given url and try to verify if it's a valid
395 link.
396 link.
396 """
397 """
397 raise NotImplementedError
398 raise NotImplementedError
398
399
399 @staticmethod
400 @staticmethod
400 def is_valid_repository(path):
401 def is_valid_repository(path):
401 """
402 """
402 Check if given `path` contains a valid repository of this backend
403 Check if given `path` contains a valid repository of this backend
403 """
404 """
404 raise NotImplementedError
405 raise NotImplementedError
405
406
406 # ==========================================================================
407 # ==========================================================================
407 # COMMITS
408 # COMMITS
408 # ==========================================================================
409 # ==========================================================================
409
410
410 @CachedProperty('_commit_ids_ver')
411 @CachedProperty('_commit_ids_ver')
411 def commit_ids(self):
412 def commit_ids(self):
412 raise NotImplementedError
413 raise NotImplementedError
413
414
414 def append_commit_id(self, commit_id):
415 def append_commit_id(self, commit_id):
415 if commit_id not in self.commit_ids:
416 if commit_id not in self.commit_ids:
416 self._rebuild_cache(self.commit_ids + [commit_id])
417 self._rebuild_cache(self.commit_ids + [commit_id])
417 self._commit_ids_ver = time.time()
418 self._commit_ids_ver = time.time()
418
419
419 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None, translate_tag=None):
420 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None, translate_tag=None):
420 """
421 """
421 Returns instance of `BaseCommit` class. If `commit_id` and `commit_idx`
422 Returns instance of `BaseCommit` class. If `commit_id` and `commit_idx`
422 are both None, most recent commit is returned.
423 are both None, most recent commit is returned.
423
424
424 :param pre_load: Optional. List of commit attributes to load.
425 :param pre_load: Optional. List of commit attributes to load.
425
426
426 :raises ``EmptyRepositoryError``: if there are no commits
427 :raises ``EmptyRepositoryError``: if there are no commits
427 """
428 """
428 raise NotImplementedError
429 raise NotImplementedError
429
430
430 def __iter__(self):
431 def __iter__(self):
431 for commit_id in self.commit_ids:
432 for commit_id in self.commit_ids:
432 yield self.get_commit(commit_id=commit_id)
433 yield self.get_commit(commit_id=commit_id)
433
434
434 def get_commits(
435 def get_commits(
435 self, start_id=None, end_id=None, start_date=None, end_date=None,
436 self, start_id=None, end_id=None, start_date=None, end_date=None,
436 branch_name=None, show_hidden=False, pre_load=None, translate_tags=None):
437 branch_name=None, show_hidden=False, pre_load=None, translate_tags=None):
437 """
438 """
438 Returns iterator of `BaseCommit` objects from start to end
439 Returns iterator of `BaseCommit` objects from start to end
439 not inclusive. This should behave just like a list, ie. end is not
440 not inclusive. This should behave just like a list, ie. end is not
440 inclusive.
441 inclusive.
441
442
442 :param start_id: None or str, must be a valid commit id
443 :param start_id: None or str, must be a valid commit id
443 :param end_id: None or str, must be a valid commit id
444 :param end_id: None or str, must be a valid commit id
444 :param start_date:
445 :param start_date:
445 :param end_date:
446 :param end_date:
446 :param branch_name:
447 :param branch_name:
447 :param show_hidden:
448 :param show_hidden:
448 :param pre_load:
449 :param pre_load:
449 :param translate_tags:
450 :param translate_tags:
450 """
451 """
451 raise NotImplementedError
452 raise NotImplementedError
452
453
453 def __getitem__(self, key):
454 def __getitem__(self, key):
454 """
455 """
455 Allows index based access to the commit objects of this repository.
456 Allows index based access to the commit objects of this repository.
456 """
457 """
457 pre_load = ["author", "branch", "date", "message", "parents"]
458 pre_load = ["author", "branch", "date", "message", "parents"]
458 if isinstance(key, slice):
459 if isinstance(key, slice):
459 return self._get_range(key, pre_load)
460 return self._get_range(key, pre_load)
460 return self.get_commit(commit_idx=key, pre_load=pre_load)
461 return self.get_commit(commit_idx=key, pre_load=pre_load)
461
462
462 def _get_range(self, slice_obj, pre_load):
463 def _get_range(self, slice_obj, pre_load):
463 for commit_id in self.commit_ids.__getitem__(slice_obj):
464 for commit_id in self.commit_ids.__getitem__(slice_obj):
464 yield self.get_commit(commit_id=commit_id, pre_load=pre_load)
465 yield self.get_commit(commit_id=commit_id, pre_load=pre_load)
465
466
466 def count(self):
467 def count(self):
467 return len(self.commit_ids)
468 return len(self.commit_ids)
468
469
469 def tag(self, name, user, commit_id=None, message=None, date=None, **opts):
470 def tag(self, name, user, commit_id=None, message=None, date=None, **opts):
470 """
471 """
471 Creates and returns a tag for the given ``commit_id``.
472 Creates and returns a tag for the given ``commit_id``.
472
473
473 :param name: name for new tag
474 :param name: name for new tag
474 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
475 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
475 :param commit_id: commit id for which new tag would be created
476 :param commit_id: commit id for which new tag would be created
476 :param message: message of the tag's commit
477 :param message: message of the tag's commit
477 :param date: date of tag's commit
478 :param date: date of tag's commit
478
479
479 :raises TagAlreadyExistError: if tag with same name already exists
480 :raises TagAlreadyExistError: if tag with same name already exists
480 """
481 """
481 raise NotImplementedError
482 raise NotImplementedError
482
483
483 def remove_tag(self, name, user, message=None, date=None):
484 def remove_tag(self, name, user, message=None, date=None):
484 """
485 """
485 Removes tag with the given ``name``.
486 Removes tag with the given ``name``.
486
487
487 :param name: name of the tag to be removed
488 :param name: name of the tag to be removed
488 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
489 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
489 :param message: message of the tag's removal commit
490 :param message: message of the tag's removal commit
490 :param date: date of tag's removal commit
491 :param date: date of tag's removal commit
491
492
492 :raises TagDoesNotExistError: if tag with given name does not exists
493 :raises TagDoesNotExistError: if tag with given name does not exists
493 """
494 """
494 raise NotImplementedError
495 raise NotImplementedError
495
496
496 def get_diff(
497 def get_diff(
497 self, commit1, commit2, path=None, ignore_whitespace=False,
498 self, commit1, commit2, path=None, ignore_whitespace=False,
498 context=3, path1=None):
499 context=3, path1=None):
499 """
500 """
500 Returns (git like) *diff*, as plain text. Shows changes introduced by
501 Returns (git like) *diff*, as plain text. Shows changes introduced by
501 `commit2` since `commit1`.
502 `commit2` since `commit1`.
502
503
503 :param commit1: Entry point from which diff is shown. Can be
504 :param commit1: Entry point from which diff is shown. Can be
504 ``self.EMPTY_COMMIT`` - in this case, patch showing all
505 ``self.EMPTY_COMMIT`` - in this case, patch showing all
505 the changes since empty state of the repository until `commit2`
506 the changes since empty state of the repository until `commit2`
506 :param commit2: Until which commit changes should be shown.
507 :param commit2: Until which commit changes should be shown.
507 :param path: Can be set to a path of a file to create a diff of that
508 :param path: Can be set to a path of a file to create a diff of that
508 file. If `path1` is also set, this value is only associated to
509 file. If `path1` is also set, this value is only associated to
509 `commit2`.
510 `commit2`.
510 :param ignore_whitespace: If set to ``True``, would not show whitespace
511 :param ignore_whitespace: If set to ``True``, would not show whitespace
511 changes. Defaults to ``False``.
512 changes. Defaults to ``False``.
512 :param context: How many lines before/after changed lines should be
513 :param context: How many lines before/after changed lines should be
513 shown. Defaults to ``3``.
514 shown. Defaults to ``3``.
514 :param path1: Can be set to a path to associate with `commit1`. This
515 :param path1: Can be set to a path to associate with `commit1`. This
515 parameter works only for backends which support diff generation for
516 parameter works only for backends which support diff generation for
516 different paths. Other backends will raise a `ValueError` if `path1`
517 different paths. Other backends will raise a `ValueError` if `path1`
517 is set and has a different value than `path`.
518 is set and has a different value than `path`.
518 :param file_path: filter this diff by given path pattern
519 :param file_path: filter this diff by given path pattern
519 """
520 """
520 raise NotImplementedError
521 raise NotImplementedError
521
522
522 def strip(self, commit_id, branch=None):
523 def strip(self, commit_id, branch=None):
523 """
524 """
524 Strip given commit_id from the repository
525 Strip given commit_id from the repository
525 """
526 """
526 raise NotImplementedError
527 raise NotImplementedError
527
528
528 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
529 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
529 """
530 """
530 Return a latest common ancestor commit if one exists for this repo
531 Return a latest common ancestor commit if one exists for this repo
531 `commit_id1` vs `commit_id2` from `repo2`.
532 `commit_id1` vs `commit_id2` from `repo2`.
532
533
533 :param commit_id1: Commit it from this repository to use as a
534 :param commit_id1: Commit it from this repository to use as a
534 target for the comparison.
535 target for the comparison.
535 :param commit_id2: Source commit id to use for comparison.
536 :param commit_id2: Source commit id to use for comparison.
536 :param repo2: Source repository to use for comparison.
537 :param repo2: Source repository to use for comparison.
537 """
538 """
538 raise NotImplementedError
539 raise NotImplementedError
539
540
540 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
541 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
541 """
542 """
542 Compare this repository's revision `commit_id1` with `commit_id2`.
543 Compare this repository's revision `commit_id1` with `commit_id2`.
543
544
544 Returns a tuple(commits, ancestor) that would be merged from
545 Returns a tuple(commits, ancestor) that would be merged from
545 `commit_id2`. Doing a normal compare (``merge=False``), ``None``
546 `commit_id2`. Doing a normal compare (``merge=False``), ``None``
546 will be returned as ancestor.
547 will be returned as ancestor.
547
548
548 :param commit_id1: Commit it from this repository to use as a
549 :param commit_id1: Commit it from this repository to use as a
549 target for the comparison.
550 target for the comparison.
550 :param commit_id2: Source commit id to use for comparison.
551 :param commit_id2: Source commit id to use for comparison.
551 :param repo2: Source repository to use for comparison.
552 :param repo2: Source repository to use for comparison.
552 :param merge: If set to ``True`` will do a merge compare which also
553 :param merge: If set to ``True`` will do a merge compare which also
553 returns the common ancestor.
554 returns the common ancestor.
554 :param pre_load: Optional. List of commit attributes to load.
555 :param pre_load: Optional. List of commit attributes to load.
555 """
556 """
556 raise NotImplementedError
557 raise NotImplementedError
557
558
558 def merge(self, repo_id, workspace_id, target_ref, source_repo, source_ref,
559 def merge(self, repo_id, workspace_id, target_ref, source_repo, source_ref,
559 user_name='', user_email='', message='', dry_run=False,
560 user_name='', user_email='', message='', dry_run=False,
560 use_rebase=False, close_branch=False):
561 use_rebase=False, close_branch=False):
561 """
562 """
562 Merge the revisions specified in `source_ref` from `source_repo`
563 Merge the revisions specified in `source_ref` from `source_repo`
563 onto the `target_ref` of this repository.
564 onto the `target_ref` of this repository.
564
565
565 `source_ref` and `target_ref` are named tupls with the following
566 `source_ref` and `target_ref` are named tupls with the following
566 fields `type`, `name` and `commit_id`.
567 fields `type`, `name` and `commit_id`.
567
568
568 Returns a MergeResponse named tuple with the following fields
569 Returns a MergeResponse named tuple with the following fields
569 'possible', 'executed', 'source_commit', 'target_commit',
570 'possible', 'executed', 'source_commit', 'target_commit',
570 'merge_commit'.
571 'merge_commit'.
571
572
572 :param repo_id: `repo_id` target repo id.
573 :param repo_id: `repo_id` target repo id.
573 :param workspace_id: `workspace_id` unique identifier.
574 :param workspace_id: `workspace_id` unique identifier.
574 :param target_ref: `target_ref` points to the commit on top of which
575 :param target_ref: `target_ref` points to the commit on top of which
575 the `source_ref` should be merged.
576 the `source_ref` should be merged.
576 :param source_repo: The repository that contains the commits to be
577 :param source_repo: The repository that contains the commits to be
577 merged.
578 merged.
578 :param source_ref: `source_ref` points to the topmost commit from
579 :param source_ref: `source_ref` points to the topmost commit from
579 the `source_repo` which should be merged.
580 the `source_repo` which should be merged.
580 :param user_name: Merge commit `user_name`.
581 :param user_name: Merge commit `user_name`.
581 :param user_email: Merge commit `user_email`.
582 :param user_email: Merge commit `user_email`.
582 :param message: Merge commit `message`.
583 :param message: Merge commit `message`.
583 :param dry_run: If `True` the merge will not take place.
584 :param dry_run: If `True` the merge will not take place.
584 :param use_rebase: If `True` commits from the source will be rebased
585 :param use_rebase: If `True` commits from the source will be rebased
585 on top of the target instead of being merged.
586 on top of the target instead of being merged.
586 :param close_branch: If `True` branch will be close before merging it
587 :param close_branch: If `True` branch will be close before merging it
587 """
588 """
588 if dry_run:
589 if dry_run:
589 message = message or settings.MERGE_DRY_RUN_MESSAGE
590 message = message or settings.MERGE_DRY_RUN_MESSAGE
590 user_email = user_email or settings.MERGE_DRY_RUN_EMAIL
591 user_email = user_email or settings.MERGE_DRY_RUN_EMAIL
591 user_name = user_name or settings.MERGE_DRY_RUN_USER
592 user_name = user_name or settings.MERGE_DRY_RUN_USER
592 else:
593 else:
593 if not user_name:
594 if not user_name:
594 raise ValueError('user_name cannot be empty')
595 raise ValueError('user_name cannot be empty')
595 if not user_email:
596 if not user_email:
596 raise ValueError('user_email cannot be empty')
597 raise ValueError('user_email cannot be empty')
597 if not message:
598 if not message:
598 raise ValueError('message cannot be empty')
599 raise ValueError('message cannot be empty')
599
600
600 try:
601 try:
601 return self._merge_repo(
602 return self._merge_repo(
602 repo_id, workspace_id, target_ref, source_repo,
603 repo_id, workspace_id, target_ref, source_repo,
603 source_ref, message, user_name, user_email, dry_run=dry_run,
604 source_ref, message, user_name, user_email, dry_run=dry_run,
604 use_rebase=use_rebase, close_branch=close_branch)
605 use_rebase=use_rebase, close_branch=close_branch)
605 except RepositoryError as exc:
606 except RepositoryError as exc:
606 log.exception('Unexpected failure when running merge, dry-run=%s', dry_run)
607 log.exception('Unexpected failure when running merge, dry-run=%s', dry_run)
607 return MergeResponse(
608 return MergeResponse(
608 False, False, None, MergeFailureReason.UNKNOWN,
609 False, False, None, MergeFailureReason.UNKNOWN,
609 metadata={'exception': str(exc)})
610 metadata={'exception': str(exc)})
610
611
611 def _merge_repo(self, repo_id, workspace_id, target_ref,
612 def _merge_repo(self, repo_id, workspace_id, target_ref,
612 source_repo, source_ref, merge_message,
613 source_repo, source_ref, merge_message,
613 merger_name, merger_email, dry_run=False,
614 merger_name, merger_email, dry_run=False,
614 use_rebase=False, close_branch=False):
615 use_rebase=False, close_branch=False):
615 """Internal implementation of merge."""
616 """Internal implementation of merge."""
616 raise NotImplementedError
617 raise NotImplementedError
617
618
618 def _maybe_prepare_merge_workspace(
619 def _maybe_prepare_merge_workspace(
619 self, repo_id, workspace_id, target_ref, source_ref):
620 self, repo_id, workspace_id, target_ref, source_ref):
620 """
621 """
621 Create the merge workspace.
622 Create the merge workspace.
622
623
623 :param workspace_id: `workspace_id` unique identifier.
624 :param workspace_id: `workspace_id` unique identifier.
624 """
625 """
625 raise NotImplementedError
626 raise NotImplementedError
626
627
627 def _get_legacy_shadow_repository_path(self, workspace_id):
628 def _get_legacy_shadow_repository_path(self, workspace_id):
628 """
629 """
629 Legacy version that was used before. We still need it for
630 Legacy version that was used before. We still need it for
630 backward compat
631 backward compat
631 """
632 """
632 return os.path.join(
633 return os.path.join(
633 os.path.dirname(self.path),
634 os.path.dirname(self.path),
634 '.__shadow_%s_%s' % (os.path.basename(self.path), workspace_id))
635 '.__shadow_%s_%s' % (os.path.basename(self.path), workspace_id))
635
636
636 def _get_shadow_repository_path(self, repo_id, workspace_id):
637 def _get_shadow_repository_path(self, repo_id, workspace_id):
637 # The name of the shadow repository must start with '.', so it is
638 # The name of the shadow repository must start with '.', so it is
638 # skipped by 'rhodecode.lib.utils.get_filesystem_repos'.
639 # skipped by 'rhodecode.lib.utils.get_filesystem_repos'.
639 legacy_repository_path = self._get_legacy_shadow_repository_path(workspace_id)
640 legacy_repository_path = self._get_legacy_shadow_repository_path(workspace_id)
640 if os.path.exists(legacy_repository_path):
641 if os.path.exists(legacy_repository_path):
641 return legacy_repository_path
642 return legacy_repository_path
642 else:
643 else:
643 return os.path.join(
644 return os.path.join(
644 os.path.dirname(self.path),
645 os.path.dirname(self.path),
645 '.__shadow_repo_%s_%s' % (repo_id, workspace_id))
646 '.__shadow_repo_%s_%s' % (repo_id, workspace_id))
646
647
647 def cleanup_merge_workspace(self, repo_id, workspace_id):
648 def cleanup_merge_workspace(self, repo_id, workspace_id):
648 """
649 """
649 Remove merge workspace.
650 Remove merge workspace.
650
651
651 This function MUST not fail in case there is no workspace associated to
652 This function MUST not fail in case there is no workspace associated to
652 the given `workspace_id`.
653 the given `workspace_id`.
653
654
654 :param workspace_id: `workspace_id` unique identifier.
655 :param workspace_id: `workspace_id` unique identifier.
655 """
656 """
656 shadow_repository_path = self._get_shadow_repository_path(repo_id, workspace_id)
657 shadow_repository_path = self._get_shadow_repository_path(repo_id, workspace_id)
657 shadow_repository_path_del = '{}.{}.delete'.format(
658 shadow_repository_path_del = '{}.{}.delete'.format(
658 shadow_repository_path, time.time())
659 shadow_repository_path, time.time())
659
660
660 # move the shadow repo, so it never conflicts with the one used.
661 # move the shadow repo, so it never conflicts with the one used.
661 # we use this method because shutil.rmtree had some edge case problems
662 # we use this method because shutil.rmtree had some edge case problems
662 # removing symlinked repositories
663 # removing symlinked repositories
663 if not os.path.isdir(shadow_repository_path):
664 if not os.path.isdir(shadow_repository_path):
664 return
665 return
665
666
666 shutil.move(shadow_repository_path, shadow_repository_path_del)
667 shutil.move(shadow_repository_path, shadow_repository_path_del)
667 try:
668 try:
668 shutil.rmtree(shadow_repository_path_del, ignore_errors=False)
669 shutil.rmtree(shadow_repository_path_del, ignore_errors=False)
669 except Exception:
670 except Exception:
670 log.exception('Failed to gracefully remove shadow repo under %s',
671 log.exception('Failed to gracefully remove shadow repo under %s',
671 shadow_repository_path_del)
672 shadow_repository_path_del)
672 shutil.rmtree(shadow_repository_path_del, ignore_errors=True)
673 shutil.rmtree(shadow_repository_path_del, ignore_errors=True)
673
674
674 # ========== #
675 # ========== #
675 # COMMIT API #
676 # COMMIT API #
676 # ========== #
677 # ========== #
677
678
678 @LazyProperty
679 @LazyProperty
679 def in_memory_commit(self):
680 def in_memory_commit(self):
680 """
681 """
681 Returns :class:`InMemoryCommit` object for this repository.
682 Returns :class:`InMemoryCommit` object for this repository.
682 """
683 """
683 raise NotImplementedError
684 raise NotImplementedError
684
685
685 # ======================== #
686 # ======================== #
686 # UTILITIES FOR SUBCLASSES #
687 # UTILITIES FOR SUBCLASSES #
687 # ======================== #
688 # ======================== #
688
689
689 def _validate_diff_commits(self, commit1, commit2):
690 def _validate_diff_commits(self, commit1, commit2):
690 """
691 """
691 Validates that the given commits are related to this repository.
692 Validates that the given commits are related to this repository.
692
693
693 Intended as a utility for sub classes to have a consistent validation
694 Intended as a utility for sub classes to have a consistent validation
694 of input parameters in methods like :meth:`get_diff`.
695 of input parameters in methods like :meth:`get_diff`.
695 """
696 """
696 self._validate_commit(commit1)
697 self._validate_commit(commit1)
697 self._validate_commit(commit2)
698 self._validate_commit(commit2)
698 if (isinstance(commit1, EmptyCommit) and
699 if (isinstance(commit1, EmptyCommit) and
699 isinstance(commit2, EmptyCommit)):
700 isinstance(commit2, EmptyCommit)):
700 raise ValueError("Cannot compare two empty commits")
701 raise ValueError("Cannot compare two empty commits")
701
702
702 def _validate_commit(self, commit):
703 def _validate_commit(self, commit):
703 if not isinstance(commit, BaseCommit):
704 if not isinstance(commit, BaseCommit):
704 raise TypeError(
705 raise TypeError(
705 "%s is not of type BaseCommit" % repr(commit))
706 "%s is not of type BaseCommit" % repr(commit))
706 if commit.repository != self and not isinstance(commit, EmptyCommit):
707 if commit.repository != self and not isinstance(commit, EmptyCommit):
707 raise ValueError(
708 raise ValueError(
708 "Commit %s must be a valid commit from this repository %s, "
709 "Commit %s must be a valid commit from this repository %s, "
709 "related to this repository instead %s." %
710 "related to this repository instead %s." %
710 (commit, self, commit.repository))
711 (commit, self, commit.repository))
711
712
712 def _validate_commit_id(self, commit_id):
713 def _validate_commit_id(self, commit_id):
713 if not isinstance(commit_id, compat.string_types):
714 if not isinstance(commit_id, compat.string_types):
714 raise TypeError("commit_id must be a string value")
715 raise TypeError("commit_id must be a string value")
715
716
716 def _validate_commit_idx(self, commit_idx):
717 def _validate_commit_idx(self, commit_idx):
717 if not isinstance(commit_idx, (int, long)):
718 if not isinstance(commit_idx, (int, long)):
718 raise TypeError("commit_idx must be a numeric value")
719 raise TypeError("commit_idx must be a numeric value")
719
720
720 def _validate_branch_name(self, branch_name):
721 def _validate_branch_name(self, branch_name):
721 if branch_name and branch_name not in self.branches_all:
722 if branch_name and branch_name not in self.branches_all:
722 msg = ("Branch %s not found in %s" % (branch_name, self))
723 msg = ("Branch %s not found in %s" % (branch_name, self))
723 raise BranchDoesNotExistError(msg)
724 raise BranchDoesNotExistError(msg)
724
725
725 #
726 #
726 # Supporting deprecated API parts
727 # Supporting deprecated API parts
727 # TODO: johbo: consider to move this into a mixin
728 # TODO: johbo: consider to move this into a mixin
728 #
729 #
729
730
730 @property
731 @property
731 def EMPTY_CHANGESET(self):
732 def EMPTY_CHANGESET(self):
732 warnings.warn(
733 warnings.warn(
733 "Use EMPTY_COMMIT or EMPTY_COMMIT_ID instead", DeprecationWarning)
734 "Use EMPTY_COMMIT or EMPTY_COMMIT_ID instead", DeprecationWarning)
734 return self.EMPTY_COMMIT_ID
735 return self.EMPTY_COMMIT_ID
735
736
736 @property
737 @property
737 def revisions(self):
738 def revisions(self):
738 warnings.warn("Use commits attribute instead", DeprecationWarning)
739 warnings.warn("Use commits attribute instead", DeprecationWarning)
739 return self.commit_ids
740 return self.commit_ids
740
741
741 @revisions.setter
742 @revisions.setter
742 def revisions(self, value):
743 def revisions(self, value):
743 warnings.warn("Use commits attribute instead", DeprecationWarning)
744 warnings.warn("Use commits attribute instead", DeprecationWarning)
744 self.commit_ids = value
745 self.commit_ids = value
745
746
746 def get_changeset(self, revision=None, pre_load=None):
747 def get_changeset(self, revision=None, pre_load=None):
747 warnings.warn("Use get_commit instead", DeprecationWarning)
748 warnings.warn("Use get_commit instead", DeprecationWarning)
748 commit_id = None
749 commit_id = None
749 commit_idx = None
750 commit_idx = None
750 if isinstance(revision, compat.string_types):
751 if isinstance(revision, compat.string_types):
751 commit_id = revision
752 commit_id = revision
752 else:
753 else:
753 commit_idx = revision
754 commit_idx = revision
754 return self.get_commit(
755 return self.get_commit(
755 commit_id=commit_id, commit_idx=commit_idx, pre_load=pre_load)
756 commit_id=commit_id, commit_idx=commit_idx, pre_load=pre_load)
756
757
757 def get_changesets(
758 def get_changesets(
758 self, start=None, end=None, start_date=None, end_date=None,
759 self, start=None, end=None, start_date=None, end_date=None,
759 branch_name=None, pre_load=None):
760 branch_name=None, pre_load=None):
760 warnings.warn("Use get_commits instead", DeprecationWarning)
761 warnings.warn("Use get_commits instead", DeprecationWarning)
761 start_id = self._revision_to_commit(start)
762 start_id = self._revision_to_commit(start)
762 end_id = self._revision_to_commit(end)
763 end_id = self._revision_to_commit(end)
763 return self.get_commits(
764 return self.get_commits(
764 start_id=start_id, end_id=end_id, start_date=start_date,
765 start_id=start_id, end_id=end_id, start_date=start_date,
765 end_date=end_date, branch_name=branch_name, pre_load=pre_load)
766 end_date=end_date, branch_name=branch_name, pre_load=pre_load)
766
767
767 def _revision_to_commit(self, revision):
768 def _revision_to_commit(self, revision):
768 """
769 """
769 Translates a revision to a commit_id
770 Translates a revision to a commit_id
770
771
771 Helps to support the old changeset based API which allows to use
772 Helps to support the old changeset based API which allows to use
772 commit ids and commit indices interchangeable.
773 commit ids and commit indices interchangeable.
773 """
774 """
774 if revision is None:
775 if revision is None:
775 return revision
776 return revision
776
777
777 if isinstance(revision, compat.string_types):
778 if isinstance(revision, compat.string_types):
778 commit_id = revision
779 commit_id = revision
779 else:
780 else:
780 commit_id = self.commit_ids[revision]
781 commit_id = self.commit_ids[revision]
781 return commit_id
782 return commit_id
782
783
783 @property
784 @property
784 def in_memory_changeset(self):
785 def in_memory_changeset(self):
785 warnings.warn("Use in_memory_commit instead", DeprecationWarning)
786 warnings.warn("Use in_memory_commit instead", DeprecationWarning)
786 return self.in_memory_commit
787 return self.in_memory_commit
787
788
788 def get_path_permissions(self, username):
789 def get_path_permissions(self, username):
789 """
790 """
790 Returns a path permission checker or None if not supported
791 Returns a path permission checker or None if not supported
791
792
792 :param username: session user name
793 :param username: session user name
793 :return: an instance of BasePathPermissionChecker or None
794 :return: an instance of BasePathPermissionChecker or None
794 """
795 """
795 return None
796 return None
796
797
797 def install_hooks(self, force=False):
798 def install_hooks(self, force=False):
798 return self._remote.install_hooks(force)
799 return self._remote.install_hooks(force)
799
800
800 def get_hooks_info(self):
801 def get_hooks_info(self):
801 return self._remote.get_hooks_info()
802 return self._remote.get_hooks_info()
802
803
803
804
804 class BaseCommit(object):
805 class BaseCommit(object):
805 """
806 """
806 Each backend should implement it's commit representation.
807 Each backend should implement it's commit representation.
807
808
808 **Attributes**
809 **Attributes**
809
810
810 ``repository``
811 ``repository``
811 repository object within which commit exists
812 repository object within which commit exists
812
813
813 ``id``
814 ``id``
814 The commit id, may be ``raw_id`` or i.e. for mercurial's tip
815 The commit id, may be ``raw_id`` or i.e. for mercurial's tip
815 just ``tip``.
816 just ``tip``.
816
817
817 ``raw_id``
818 ``raw_id``
818 raw commit representation (i.e. full 40 length sha for git
819 raw commit representation (i.e. full 40 length sha for git
819 backend)
820 backend)
820
821
821 ``short_id``
822 ``short_id``
822 shortened (if apply) version of ``raw_id``; it would be simple
823 shortened (if apply) version of ``raw_id``; it would be simple
823 shortcut for ``raw_id[:12]`` for git/mercurial backends or same
824 shortcut for ``raw_id[:12]`` for git/mercurial backends or same
824 as ``raw_id`` for subversion
825 as ``raw_id`` for subversion
825
826
826 ``idx``
827 ``idx``
827 commit index
828 commit index
828
829
829 ``files``
830 ``files``
830 list of ``FileNode`` (``Node`` with NodeKind.FILE) objects
831 list of ``FileNode`` (``Node`` with NodeKind.FILE) objects
831
832
832 ``dirs``
833 ``dirs``
833 list of ``DirNode`` (``Node`` with NodeKind.DIR) objects
834 list of ``DirNode`` (``Node`` with NodeKind.DIR) objects
834
835
835 ``nodes``
836 ``nodes``
836 combined list of ``Node`` objects
837 combined list of ``Node`` objects
837
838
838 ``author``
839 ``author``
839 author of the commit, as unicode
840 author of the commit, as unicode
840
841
841 ``message``
842 ``message``
842 message of the commit, as unicode
843 message of the commit, as unicode
843
844
844 ``parents``
845 ``parents``
845 list of parent commits
846 list of parent commits
846
847
847 """
848 """
848
849
849 branch = None
850 branch = None
850 """
851 """
851 Depending on the backend this should be set to the branch name of the
852 Depending on the backend this should be set to the branch name of the
852 commit. Backends not supporting branches on commits should leave this
853 commit. Backends not supporting branches on commits should leave this
853 value as ``None``.
854 value as ``None``.
854 """
855 """
855
856
856 _ARCHIVE_PREFIX_TEMPLATE = b'{repo_name}-{short_id}'
857 _ARCHIVE_PREFIX_TEMPLATE = b'{repo_name}-{short_id}'
857 """
858 """
858 This template is used to generate a default prefix for repository archives
859 This template is used to generate a default prefix for repository archives
859 if no prefix has been specified.
860 if no prefix has been specified.
860 """
861 """
861
862
862 def __str__(self):
863 def __str__(self):
863 return '<%s at %s:%s>' % (
864 return '<%s at %s:%s>' % (
864 self.__class__.__name__, self.idx, self.short_id)
865 self.__class__.__name__, self.idx, self.short_id)
865
866
866 def __repr__(self):
867 def __repr__(self):
867 return self.__str__()
868 return self.__str__()
868
869
869 def __unicode__(self):
870 def __unicode__(self):
870 return u'%s:%s' % (self.idx, self.short_id)
871 return u'%s:%s' % (self.idx, self.short_id)
871
872
872 def __eq__(self, other):
873 def __eq__(self, other):
873 same_instance = isinstance(other, self.__class__)
874 same_instance = isinstance(other, self.__class__)
874 return same_instance and self.raw_id == other.raw_id
875 return same_instance and self.raw_id == other.raw_id
875
876
876 def __json__(self):
877 def __json__(self):
877 parents = []
878 parents = []
878 try:
879 try:
879 for parent in self.parents:
880 for parent in self.parents:
880 parents.append({'raw_id': parent.raw_id})
881 parents.append({'raw_id': parent.raw_id})
881 except NotImplementedError:
882 except NotImplementedError:
882 # empty commit doesn't have parents implemented
883 # empty commit doesn't have parents implemented
883 pass
884 pass
884
885
885 return {
886 return {
886 'short_id': self.short_id,
887 'short_id': self.short_id,
887 'raw_id': self.raw_id,
888 'raw_id': self.raw_id,
888 'revision': self.idx,
889 'revision': self.idx,
889 'message': self.message,
890 'message': self.message,
890 'date': self.date,
891 'date': self.date,
891 'author': self.author,
892 'author': self.author,
892 'parents': parents,
893 'parents': parents,
893 'branch': self.branch
894 'branch': self.branch
894 }
895 }
895
896
896 def __getstate__(self):
897 def __getstate__(self):
897 d = self.__dict__.copy()
898 d = self.__dict__.copy()
898 d.pop('_remote', None)
899 d.pop('_remote', None)
899 d.pop('repository', None)
900 d.pop('repository', None)
900 return d
901 return d
901
902
902 def _get_refs(self):
903 def _get_refs(self):
903 return {
904 return {
904 'branches': [self.branch] if self.branch else [],
905 'branches': [self.branch] if self.branch else [],
905 'bookmarks': getattr(self, 'bookmarks', []),
906 'bookmarks': getattr(self, 'bookmarks', []),
906 'tags': self.tags
907 'tags': self.tags
907 }
908 }
908
909
909 @LazyProperty
910 @LazyProperty
910 def last(self):
911 def last(self):
911 """
912 """
912 ``True`` if this is last commit in repository, ``False``
913 ``True`` if this is last commit in repository, ``False``
913 otherwise; trying to access this attribute while there is no
914 otherwise; trying to access this attribute while there is no
914 commits would raise `EmptyRepositoryError`
915 commits would raise `EmptyRepositoryError`
915 """
916 """
916 if self.repository is None:
917 if self.repository is None:
917 raise CommitError("Cannot check if it's most recent commit")
918 raise CommitError("Cannot check if it's most recent commit")
918 return self.raw_id == self.repository.commit_ids[-1]
919 return self.raw_id == self.repository.commit_ids[-1]
919
920
920 @LazyProperty
921 @LazyProperty
921 def parents(self):
922 def parents(self):
922 """
923 """
923 Returns list of parent commits.
924 Returns list of parent commits.
924 """
925 """
925 raise NotImplementedError
926 raise NotImplementedError
926
927
927 @LazyProperty
928 @LazyProperty
928 def first_parent(self):
929 def first_parent(self):
929 """
930 """
930 Returns list of parent commits.
931 Returns list of parent commits.
931 """
932 """
932 return self.parents[0] if self.parents else EmptyCommit()
933 return self.parents[0] if self.parents else EmptyCommit()
933
934
934 @property
935 @property
935 def merge(self):
936 def merge(self):
936 """
937 """
937 Returns boolean if commit is a merge.
938 Returns boolean if commit is a merge.
938 """
939 """
939 return len(self.parents) > 1
940 return len(self.parents) > 1
940
941
941 @LazyProperty
942 @LazyProperty
942 def children(self):
943 def children(self):
943 """
944 """
944 Returns list of child commits.
945 Returns list of child commits.
945 """
946 """
946 raise NotImplementedError
947 raise NotImplementedError
947
948
948 @LazyProperty
949 @LazyProperty
949 def id(self):
950 def id(self):
950 """
951 """
951 Returns string identifying this commit.
952 Returns string identifying this commit.
952 """
953 """
953 raise NotImplementedError
954 raise NotImplementedError
954
955
955 @LazyProperty
956 @LazyProperty
956 def raw_id(self):
957 def raw_id(self):
957 """
958 """
958 Returns raw string identifying this commit.
959 Returns raw string identifying this commit.
959 """
960 """
960 raise NotImplementedError
961 raise NotImplementedError
961
962
962 @LazyProperty
963 @LazyProperty
963 def short_id(self):
964 def short_id(self):
964 """
965 """
965 Returns shortened version of ``raw_id`` attribute, as string,
966 Returns shortened version of ``raw_id`` attribute, as string,
966 identifying this commit, useful for presentation to users.
967 identifying this commit, useful for presentation to users.
967 """
968 """
968 raise NotImplementedError
969 raise NotImplementedError
969
970
970 @LazyProperty
971 @LazyProperty
971 def idx(self):
972 def idx(self):
972 """
973 """
973 Returns integer identifying this commit.
974 Returns integer identifying this commit.
974 """
975 """
975 raise NotImplementedError
976 raise NotImplementedError
976
977
977 @LazyProperty
978 @LazyProperty
978 def committer(self):
979 def committer(self):
979 """
980 """
980 Returns committer for this commit
981 Returns committer for this commit
981 """
982 """
982 raise NotImplementedError
983 raise NotImplementedError
983
984
984 @LazyProperty
985 @LazyProperty
985 def committer_name(self):
986 def committer_name(self):
986 """
987 """
987 Returns committer name for this commit
988 Returns committer name for this commit
988 """
989 """
989
990
990 return author_name(self.committer)
991 return author_name(self.committer)
991
992
992 @LazyProperty
993 @LazyProperty
993 def committer_email(self):
994 def committer_email(self):
994 """
995 """
995 Returns committer email address for this commit
996 Returns committer email address for this commit
996 """
997 """
997
998
998 return author_email(self.committer)
999 return author_email(self.committer)
999
1000
1000 @LazyProperty
1001 @LazyProperty
1001 def author(self):
1002 def author(self):
1002 """
1003 """
1003 Returns author for this commit
1004 Returns author for this commit
1004 """
1005 """
1005
1006
1006 raise NotImplementedError
1007 raise NotImplementedError
1007
1008
1008 @LazyProperty
1009 @LazyProperty
1009 def author_name(self):
1010 def author_name(self):
1010 """
1011 """
1011 Returns author name for this commit
1012 Returns author name for this commit
1012 """
1013 """
1013
1014
1014 return author_name(self.author)
1015 return author_name(self.author)
1015
1016
1016 @LazyProperty
1017 @LazyProperty
1017 def author_email(self):
1018 def author_email(self):
1018 """
1019 """
1019 Returns author email address for this commit
1020 Returns author email address for this commit
1020 """
1021 """
1021
1022
1022 return author_email(self.author)
1023 return author_email(self.author)
1023
1024
1024 def get_file_mode(self, path):
1025 def get_file_mode(self, path):
1025 """
1026 """
1026 Returns stat mode of the file at `path`.
1027 Returns stat mode of the file at `path`.
1027 """
1028 """
1028 raise NotImplementedError
1029 raise NotImplementedError
1029
1030
1030 def is_link(self, path):
1031 def is_link(self, path):
1031 """
1032 """
1032 Returns ``True`` if given `path` is a symlink
1033 Returns ``True`` if given `path` is a symlink
1033 """
1034 """
1034 raise NotImplementedError
1035 raise NotImplementedError
1035
1036
1036 def get_file_content(self, path):
1037 def get_file_content(self, path):
1037 """
1038 """
1038 Returns content of the file at the given `path`.
1039 Returns content of the file at the given `path`.
1039 """
1040 """
1040 raise NotImplementedError
1041 raise NotImplementedError
1041
1042
1042 def get_file_size(self, path):
1043 def get_file_size(self, path):
1043 """
1044 """
1044 Returns size of the file at the given `path`.
1045 Returns size of the file at the given `path`.
1045 """
1046 """
1046 raise NotImplementedError
1047 raise NotImplementedError
1047
1048
1048 def get_path_commit(self, path, pre_load=None):
1049 def get_path_commit(self, path, pre_load=None):
1049 """
1050 """
1050 Returns last commit of the file at the given `path`.
1051 Returns last commit of the file at the given `path`.
1051
1052
1052 :param pre_load: Optional. List of commit attributes to load.
1053 :param pre_load: Optional. List of commit attributes to load.
1053 """
1054 """
1054 commits = self.get_path_history(path, limit=1, pre_load=pre_load)
1055 commits = self.get_path_history(path, limit=1, pre_load=pre_load)
1055 if not commits:
1056 if not commits:
1056 raise RepositoryError(
1057 raise RepositoryError(
1057 'Failed to fetch history for path {}. '
1058 'Failed to fetch history for path {}. '
1058 'Please check if such path exists in your repository'.format(
1059 'Please check if such path exists in your repository'.format(
1059 path))
1060 path))
1060 return commits[0]
1061 return commits[0]
1061
1062
1062 def get_path_history(self, path, limit=None, pre_load=None):
1063 def get_path_history(self, path, limit=None, pre_load=None):
1063 """
1064 """
1064 Returns history of file as reversed list of :class:`BaseCommit`
1065 Returns history of file as reversed list of :class:`BaseCommit`
1065 objects for which file at given `path` has been modified.
1066 objects for which file at given `path` has been modified.
1066
1067
1067 :param limit: Optional. Allows to limit the size of the returned
1068 :param limit: Optional. Allows to limit the size of the returned
1068 history. This is intended as a hint to the underlying backend, so
1069 history. This is intended as a hint to the underlying backend, so
1069 that it can apply optimizations depending on the limit.
1070 that it can apply optimizations depending on the limit.
1070 :param pre_load: Optional. List of commit attributes to load.
1071 :param pre_load: Optional. List of commit attributes to load.
1071 """
1072 """
1072 raise NotImplementedError
1073 raise NotImplementedError
1073
1074
1074 def get_file_annotate(self, path, pre_load=None):
1075 def get_file_annotate(self, path, pre_load=None):
1075 """
1076 """
1076 Returns a generator of four element tuples with
1077 Returns a generator of four element tuples with
1077 lineno, sha, commit lazy loader and line
1078 lineno, sha, commit lazy loader and line
1078
1079
1079 :param pre_load: Optional. List of commit attributes to load.
1080 :param pre_load: Optional. List of commit attributes to load.
1080 """
1081 """
1081 raise NotImplementedError
1082 raise NotImplementedError
1082
1083
1083 def get_nodes(self, path):
1084 def get_nodes(self, path):
1084 """
1085 """
1085 Returns combined ``DirNode`` and ``FileNode`` objects list representing
1086 Returns combined ``DirNode`` and ``FileNode`` objects list representing
1086 state of commit at the given ``path``.
1087 state of commit at the given ``path``.
1087
1088
1088 :raises ``CommitError``: if node at the given ``path`` is not
1089 :raises ``CommitError``: if node at the given ``path`` is not
1089 instance of ``DirNode``
1090 instance of ``DirNode``
1090 """
1091 """
1091 raise NotImplementedError
1092 raise NotImplementedError
1092
1093
1093 def get_node(self, path):
1094 def get_node(self, path):
1094 """
1095 """
1095 Returns ``Node`` object from the given ``path``.
1096 Returns ``Node`` object from the given ``path``.
1096
1097
1097 :raises ``NodeDoesNotExistError``: if there is no node at the given
1098 :raises ``NodeDoesNotExistError``: if there is no node at the given
1098 ``path``
1099 ``path``
1099 """
1100 """
1100 raise NotImplementedError
1101 raise NotImplementedError
1101
1102
1102 def get_largefile_node(self, path):
1103 def get_largefile_node(self, path):
1103 """
1104 """
1104 Returns the path to largefile from Mercurial/Git-lfs storage.
1105 Returns the path to largefile from Mercurial/Git-lfs storage.
1105 or None if it's not a largefile node
1106 or None if it's not a largefile node
1106 """
1107 """
1107 return None
1108 return None
1108
1109
1109 def archive_repo(self, archive_dest_path, kind='tgz', subrepos=None,
1110 def archive_repo(self, archive_dest_path, kind='tgz', subrepos=None,
1110 prefix=None, write_metadata=False, mtime=None, archive_at_path='/'):
1111 prefix=None, write_metadata=False, mtime=None, archive_at_path='/'):
1111 """
1112 """
1112 Creates an archive containing the contents of the repository.
1113 Creates an archive containing the contents of the repository.
1113
1114
1114 :param archive_dest_path: path to the file which to create the archive.
1115 :param archive_dest_path: path to the file which to create the archive.
1115 :param kind: one of following: ``"tbz2"``, ``"tgz"``, ``"zip"``.
1116 :param kind: one of following: ``"tbz2"``, ``"tgz"``, ``"zip"``.
1116 :param prefix: name of root directory in archive.
1117 :param prefix: name of root directory in archive.
1117 Default is repository name and commit's short_id joined with dash:
1118 Default is repository name and commit's short_id joined with dash:
1118 ``"{repo_name}-{short_id}"``.
1119 ``"{repo_name}-{short_id}"``.
1119 :param write_metadata: write a metadata file into archive.
1120 :param write_metadata: write a metadata file into archive.
1120 :param mtime: custom modification time for archive creation, defaults
1121 :param mtime: custom modification time for archive creation, defaults
1121 to time.time() if not given.
1122 to time.time() if not given.
1122 :param archive_at_path: pack files at this path (default '/')
1123 :param archive_at_path: pack files at this path (default '/')
1123
1124
1124 :raise VCSError: If prefix has a problem.
1125 :raise VCSError: If prefix has a problem.
1125 """
1126 """
1126 allowed_kinds = [x[0] for x in settings.ARCHIVE_SPECS]
1127 allowed_kinds = [x[0] for x in settings.ARCHIVE_SPECS]
1127 if kind not in allowed_kinds:
1128 if kind not in allowed_kinds:
1128 raise ImproperArchiveTypeError(
1129 raise ImproperArchiveTypeError(
1129 'Archive kind (%s) not supported use one of %s' %
1130 'Archive kind (%s) not supported use one of %s' %
1130 (kind, allowed_kinds))
1131 (kind, allowed_kinds))
1131
1132
1132 prefix = self._validate_archive_prefix(prefix)
1133 prefix = self._validate_archive_prefix(prefix)
1133
1134
1134 mtime = mtime is not None or time.mktime(self.date.timetuple())
1135 mtime = mtime is not None or time.mktime(self.date.timetuple())
1135
1136
1136 file_info = []
1137 file_info = []
1137 cur_rev = self.repository.get_commit(commit_id=self.raw_id)
1138 cur_rev = self.repository.get_commit(commit_id=self.raw_id)
1138 for _r, _d, files in cur_rev.walk(archive_at_path):
1139 for _r, _d, files in cur_rev.walk(archive_at_path):
1139 for f in files:
1140 for f in files:
1140 f_path = os.path.join(prefix, f.path)
1141 f_path = os.path.join(prefix, f.path)
1141 file_info.append(
1142 file_info.append(
1142 (f_path, f.mode, f.is_link(), f.raw_bytes))
1143 (f_path, f.mode, f.is_link(), f.raw_bytes))
1143
1144
1144 if write_metadata:
1145 if write_metadata:
1145 metadata = [
1146 metadata = [
1146 ('repo_name', self.repository.name),
1147 ('repo_name', self.repository.name),
1147 ('commit_id', self.raw_id),
1148 ('commit_id', self.raw_id),
1148 ('mtime', mtime),
1149 ('mtime', mtime),
1149 ('branch', self.branch),
1150 ('branch', self.branch),
1150 ('tags', ','.join(self.tags)),
1151 ('tags', ','.join(self.tags)),
1151 ]
1152 ]
1152 meta = ["%s:%s" % (f_name, value) for f_name, value in metadata]
1153 meta = ["%s:%s" % (f_name, value) for f_name, value in metadata]
1153 file_info.append(('.archival.txt', 0o644, False, '\n'.join(meta)))
1154 file_info.append(('.archival.txt', 0o644, False, '\n'.join(meta)))
1154
1155
1155 connection.Hg.archive_repo(archive_dest_path, mtime, file_info, kind)
1156 connection.Hg.archive_repo(archive_dest_path, mtime, file_info, kind)
1156
1157
1157 def _validate_archive_prefix(self, prefix):
1158 def _validate_archive_prefix(self, prefix):
1158 if prefix is None:
1159 if prefix is None:
1159 prefix = self._ARCHIVE_PREFIX_TEMPLATE.format(
1160 prefix = self._ARCHIVE_PREFIX_TEMPLATE.format(
1160 repo_name=safe_str(self.repository.name),
1161 repo_name=safe_str(self.repository.name),
1161 short_id=self.short_id)
1162 short_id=self.short_id)
1162 elif not isinstance(prefix, str):
1163 elif not isinstance(prefix, str):
1163 raise ValueError("prefix not a bytes object: %s" % repr(prefix))
1164 raise ValueError("prefix not a bytes object: %s" % repr(prefix))
1164 elif prefix.startswith('/'):
1165 elif prefix.startswith('/'):
1165 raise VCSError("Prefix cannot start with leading slash")
1166 raise VCSError("Prefix cannot start with leading slash")
1166 elif prefix.strip() == '':
1167 elif prefix.strip() == '':
1167 raise VCSError("Prefix cannot be empty")
1168 raise VCSError("Prefix cannot be empty")
1168 return prefix
1169 return prefix
1169
1170
1170 @LazyProperty
1171 @LazyProperty
1171 def root(self):
1172 def root(self):
1172 """
1173 """
1173 Returns ``RootNode`` object for this commit.
1174 Returns ``RootNode`` object for this commit.
1174 """
1175 """
1175 return self.get_node('')
1176 return self.get_node('')
1176
1177
1177 def next(self, branch=None):
1178 def next(self, branch=None):
1178 """
1179 """
1179 Returns next commit from current, if branch is gives it will return
1180 Returns next commit from current, if branch is gives it will return
1180 next commit belonging to this branch
1181 next commit belonging to this branch
1181
1182
1182 :param branch: show commits within the given named branch
1183 :param branch: show commits within the given named branch
1183 """
1184 """
1184 indexes = xrange(self.idx + 1, self.repository.count())
1185 indexes = xrange(self.idx + 1, self.repository.count())
1185 return self._find_next(indexes, branch)
1186 return self._find_next(indexes, branch)
1186
1187
1187 def prev(self, branch=None):
1188 def prev(self, branch=None):
1188 """
1189 """
1189 Returns previous commit from current, if branch is gives it will
1190 Returns previous commit from current, if branch is gives it will
1190 return previous commit belonging to this branch
1191 return previous commit belonging to this branch
1191
1192
1192 :param branch: show commit within the given named branch
1193 :param branch: show commit within the given named branch
1193 """
1194 """
1194 indexes = xrange(self.idx - 1, -1, -1)
1195 indexes = xrange(self.idx - 1, -1, -1)
1195 return self._find_next(indexes, branch)
1196 return self._find_next(indexes, branch)
1196
1197
1197 def _find_next(self, indexes, branch=None):
1198 def _find_next(self, indexes, branch=None):
1198 if branch and self.branch != branch:
1199 if branch and self.branch != branch:
1199 raise VCSError('Branch option used on commit not belonging '
1200 raise VCSError('Branch option used on commit not belonging '
1200 'to that branch')
1201 'to that branch')
1201
1202
1202 for next_idx in indexes:
1203 for next_idx in indexes:
1203 commit = self.repository.get_commit(commit_idx=next_idx)
1204 commit = self.repository.get_commit(commit_idx=next_idx)
1204 if branch and branch != commit.branch:
1205 if branch and branch != commit.branch:
1205 continue
1206 continue
1206 return commit
1207 return commit
1207 raise CommitDoesNotExistError
1208 raise CommitDoesNotExistError
1208
1209
1209 def diff(self, ignore_whitespace=True, context=3):
1210 def diff(self, ignore_whitespace=True, context=3):
1210 """
1211 """
1211 Returns a `Diff` object representing the change made by this commit.
1212 Returns a `Diff` object representing the change made by this commit.
1212 """
1213 """
1213 parent = self.first_parent
1214 parent = self.first_parent
1214 diff = self.repository.get_diff(
1215 diff = self.repository.get_diff(
1215 parent, self,
1216 parent, self,
1216 ignore_whitespace=ignore_whitespace,
1217 ignore_whitespace=ignore_whitespace,
1217 context=context)
1218 context=context)
1218 return diff
1219 return diff
1219
1220
1220 @LazyProperty
1221 @LazyProperty
1221 def added(self):
1222 def added(self):
1222 """
1223 """
1223 Returns list of added ``FileNode`` objects.
1224 Returns list of added ``FileNode`` objects.
1224 """
1225 """
1225 raise NotImplementedError
1226 raise NotImplementedError
1226
1227
1227 @LazyProperty
1228 @LazyProperty
1228 def changed(self):
1229 def changed(self):
1229 """
1230 """
1230 Returns list of modified ``FileNode`` objects.
1231 Returns list of modified ``FileNode`` objects.
1231 """
1232 """
1232 raise NotImplementedError
1233 raise NotImplementedError
1233
1234
1234 @LazyProperty
1235 @LazyProperty
1235 def removed(self):
1236 def removed(self):
1236 """
1237 """
1237 Returns list of removed ``FileNode`` objects.
1238 Returns list of removed ``FileNode`` objects.
1238 """
1239 """
1239 raise NotImplementedError
1240 raise NotImplementedError
1240
1241
1241 @LazyProperty
1242 @LazyProperty
1242 def size(self):
1243 def size(self):
1243 """
1244 """
1244 Returns total number of bytes from contents of all filenodes.
1245 Returns total number of bytes from contents of all filenodes.
1245 """
1246 """
1246 return sum((node.size for node in self.get_filenodes_generator()))
1247 return sum((node.size for node in self.get_filenodes_generator()))
1247
1248
1248 def walk(self, topurl=''):
1249 def walk(self, topurl=''):
1249 """
1250 """
1250 Similar to os.walk method. Insted of filesystem it walks through
1251 Similar to os.walk method. Insted of filesystem it walks through
1251 commit starting at given ``topurl``. Returns generator of tuples
1252 commit starting at given ``topurl``. Returns generator of tuples
1252 (topnode, dirnodes, filenodes).
1253 (topnode, dirnodes, filenodes).
1253 """
1254 """
1254 topnode = self.get_node(topurl)
1255 topnode = self.get_node(topurl)
1255 if not topnode.is_dir():
1256 if not topnode.is_dir():
1256 return
1257 return
1257 yield (topnode, topnode.dirs, topnode.files)
1258 yield (topnode, topnode.dirs, topnode.files)
1258 for dirnode in topnode.dirs:
1259 for dirnode in topnode.dirs:
1259 for tup in self.walk(dirnode.path):
1260 for tup in self.walk(dirnode.path):
1260 yield tup
1261 yield tup
1261
1262
1262 def get_filenodes_generator(self):
1263 def get_filenodes_generator(self):
1263 """
1264 """
1264 Returns generator that yields *all* file nodes.
1265 Returns generator that yields *all* file nodes.
1265 """
1266 """
1266 for topnode, dirs, files in self.walk():
1267 for topnode, dirs, files in self.walk():
1267 for node in files:
1268 for node in files:
1268 yield node
1269 yield node
1269
1270
1270 #
1271 #
1271 # Utilities for sub classes to support consistent behavior
1272 # Utilities for sub classes to support consistent behavior
1272 #
1273 #
1273
1274
1274 def no_node_at_path(self, path):
1275 def no_node_at_path(self, path):
1275 return NodeDoesNotExistError(
1276 return NodeDoesNotExistError(
1276 u"There is no file nor directory at the given path: "
1277 u"There is no file nor directory at the given path: "
1277 u"`%s` at commit %s" % (safe_unicode(path), self.short_id))
1278 u"`%s` at commit %s" % (safe_unicode(path), self.short_id))
1278
1279
1279 def _fix_path(self, path):
1280 def _fix_path(self, path):
1280 """
1281 """
1281 Paths are stored without trailing slash so we need to get rid off it if
1282 Paths are stored without trailing slash so we need to get rid off it if
1282 needed.
1283 needed.
1283 """
1284 """
1284 return path.rstrip('/')
1285 return path.rstrip('/')
1285
1286
1286 #
1287 #
1287 # Deprecated API based on changesets
1288 # Deprecated API based on changesets
1288 #
1289 #
1289
1290
1290 @property
1291 @property
1291 def revision(self):
1292 def revision(self):
1292 warnings.warn("Use idx instead", DeprecationWarning)
1293 warnings.warn("Use idx instead", DeprecationWarning)
1293 return self.idx
1294 return self.idx
1294
1295
1295 @revision.setter
1296 @revision.setter
1296 def revision(self, value):
1297 def revision(self, value):
1297 warnings.warn("Use idx instead", DeprecationWarning)
1298 warnings.warn("Use idx instead", DeprecationWarning)
1298 self.idx = value
1299 self.idx = value
1299
1300
1300 def get_file_changeset(self, path):
1301 def get_file_changeset(self, path):
1301 warnings.warn("Use get_path_commit instead", DeprecationWarning)
1302 warnings.warn("Use get_path_commit instead", DeprecationWarning)
1302 return self.get_path_commit(path)
1303 return self.get_path_commit(path)
1303
1304
1304
1305
1305 class BaseChangesetClass(type):
1306 class BaseChangesetClass(type):
1306
1307
1307 def __instancecheck__(self, instance):
1308 def __instancecheck__(self, instance):
1308 return isinstance(instance, BaseCommit)
1309 return isinstance(instance, BaseCommit)
1309
1310
1310
1311
1311 class BaseChangeset(BaseCommit):
1312 class BaseChangeset(BaseCommit):
1312
1313
1313 __metaclass__ = BaseChangesetClass
1314 __metaclass__ = BaseChangesetClass
1314
1315
1315 def __new__(cls, *args, **kwargs):
1316 def __new__(cls, *args, **kwargs):
1316 warnings.warn(
1317 warnings.warn(
1317 "Use BaseCommit instead of BaseChangeset", DeprecationWarning)
1318 "Use BaseCommit instead of BaseChangeset", DeprecationWarning)
1318 return super(BaseChangeset, cls).__new__(cls, *args, **kwargs)
1319 return super(BaseChangeset, cls).__new__(cls, *args, **kwargs)
1319
1320
1320
1321
1321 class BaseInMemoryCommit(object):
1322 class BaseInMemoryCommit(object):
1322 """
1323 """
1323 Represents differences between repository's state (most recent head) and
1324 Represents differences between repository's state (most recent head) and
1324 changes made *in place*.
1325 changes made *in place*.
1325
1326
1326 **Attributes**
1327 **Attributes**
1327
1328
1328 ``repository``
1329 ``repository``
1329 repository object for this in-memory-commit
1330 repository object for this in-memory-commit
1330
1331
1331 ``added``
1332 ``added``
1332 list of ``FileNode`` objects marked as *added*
1333 list of ``FileNode`` objects marked as *added*
1333
1334
1334 ``changed``
1335 ``changed``
1335 list of ``FileNode`` objects marked as *changed*
1336 list of ``FileNode`` objects marked as *changed*
1336
1337
1337 ``removed``
1338 ``removed``
1338 list of ``FileNode`` or ``RemovedFileNode`` objects marked to be
1339 list of ``FileNode`` or ``RemovedFileNode`` objects marked to be
1339 *removed*
1340 *removed*
1340
1341
1341 ``parents``
1342 ``parents``
1342 list of :class:`BaseCommit` instances representing parents of
1343 list of :class:`BaseCommit` instances representing parents of
1343 in-memory commit. Should always be 2-element sequence.
1344 in-memory commit. Should always be 2-element sequence.
1344
1345
1345 """
1346 """
1346
1347
1347 def __init__(self, repository):
1348 def __init__(self, repository):
1348 self.repository = repository
1349 self.repository = repository
1349 self.added = []
1350 self.added = []
1350 self.changed = []
1351 self.changed = []
1351 self.removed = []
1352 self.removed = []
1352 self.parents = []
1353 self.parents = []
1353
1354
1354 def add(self, *filenodes):
1355 def add(self, *filenodes):
1355 """
1356 """
1356 Marks given ``FileNode`` objects as *to be committed*.
1357 Marks given ``FileNode`` objects as *to be committed*.
1357
1358
1358 :raises ``NodeAlreadyExistsError``: if node with same path exists at
1359 :raises ``NodeAlreadyExistsError``: if node with same path exists at
1359 latest commit
1360 latest commit
1360 :raises ``NodeAlreadyAddedError``: if node with same path is already
1361 :raises ``NodeAlreadyAddedError``: if node with same path is already
1361 marked as *added*
1362 marked as *added*
1362 """
1363 """
1363 # Check if not already marked as *added* first
1364 # Check if not already marked as *added* first
1364 for node in filenodes:
1365 for node in filenodes:
1365 if node.path in (n.path for n in self.added):
1366 if node.path in (n.path for n in self.added):
1366 raise NodeAlreadyAddedError(
1367 raise NodeAlreadyAddedError(
1367 "Such FileNode %s is already marked for addition"
1368 "Such FileNode %s is already marked for addition"
1368 % node.path)
1369 % node.path)
1369 for node in filenodes:
1370 for node in filenodes:
1370 self.added.append(node)
1371 self.added.append(node)
1371
1372
1372 def change(self, *filenodes):
1373 def change(self, *filenodes):
1373 """
1374 """
1374 Marks given ``FileNode`` objects to be *changed* in next commit.
1375 Marks given ``FileNode`` objects to be *changed* in next commit.
1375
1376
1376 :raises ``EmptyRepositoryError``: if there are no commits yet
1377 :raises ``EmptyRepositoryError``: if there are no commits yet
1377 :raises ``NodeAlreadyExistsError``: if node with same path is already
1378 :raises ``NodeAlreadyExistsError``: if node with same path is already
1378 marked to be *changed*
1379 marked to be *changed*
1379 :raises ``NodeAlreadyRemovedError``: if node with same path is already
1380 :raises ``NodeAlreadyRemovedError``: if node with same path is already
1380 marked to be *removed*
1381 marked to be *removed*
1381 :raises ``NodeDoesNotExistError``: if node doesn't exist in latest
1382 :raises ``NodeDoesNotExistError``: if node doesn't exist in latest
1382 commit
1383 commit
1383 :raises ``NodeNotChangedError``: if node hasn't really be changed
1384 :raises ``NodeNotChangedError``: if node hasn't really be changed
1384 """
1385 """
1385 for node in filenodes:
1386 for node in filenodes:
1386 if node.path in (n.path for n in self.removed):
1387 if node.path in (n.path for n in self.removed):
1387 raise NodeAlreadyRemovedError(
1388 raise NodeAlreadyRemovedError(
1388 "Node at %s is already marked as removed" % node.path)
1389 "Node at %s is already marked as removed" % node.path)
1389 try:
1390 try:
1390 self.repository.get_commit()
1391 self.repository.get_commit()
1391 except EmptyRepositoryError:
1392 except EmptyRepositoryError:
1392 raise EmptyRepositoryError(
1393 raise EmptyRepositoryError(
1393 "Nothing to change - try to *add* new nodes rather than "
1394 "Nothing to change - try to *add* new nodes rather than "
1394 "changing them")
1395 "changing them")
1395 for node in filenodes:
1396 for node in filenodes:
1396 if node.path in (n.path for n in self.changed):
1397 if node.path in (n.path for n in self.changed):
1397 raise NodeAlreadyChangedError(
1398 raise NodeAlreadyChangedError(
1398 "Node at '%s' is already marked as changed" % node.path)
1399 "Node at '%s' is already marked as changed" % node.path)
1399 self.changed.append(node)
1400 self.changed.append(node)
1400
1401
1401 def remove(self, *filenodes):
1402 def remove(self, *filenodes):
1402 """
1403 """
1403 Marks given ``FileNode`` (or ``RemovedFileNode``) objects to be
1404 Marks given ``FileNode`` (or ``RemovedFileNode``) objects to be
1404 *removed* in next commit.
1405 *removed* in next commit.
1405
1406
1406 :raises ``NodeAlreadyRemovedError``: if node has been already marked to
1407 :raises ``NodeAlreadyRemovedError``: if node has been already marked to
1407 be *removed*
1408 be *removed*
1408 :raises ``NodeAlreadyChangedError``: if node has been already marked to
1409 :raises ``NodeAlreadyChangedError``: if node has been already marked to
1409 be *changed*
1410 be *changed*
1410 """
1411 """
1411 for node in filenodes:
1412 for node in filenodes:
1412 if node.path in (n.path for n in self.removed):
1413 if node.path in (n.path for n in self.removed):
1413 raise NodeAlreadyRemovedError(
1414 raise NodeAlreadyRemovedError(
1414 "Node is already marked to for removal at %s" % node.path)
1415 "Node is already marked to for removal at %s" % node.path)
1415 if node.path in (n.path for n in self.changed):
1416 if node.path in (n.path for n in self.changed):
1416 raise NodeAlreadyChangedError(
1417 raise NodeAlreadyChangedError(
1417 "Node is already marked to be changed at %s" % node.path)
1418 "Node is already marked to be changed at %s" % node.path)
1418 # We only mark node as *removed* - real removal is done by
1419 # We only mark node as *removed* - real removal is done by
1419 # commit method
1420 # commit method
1420 self.removed.append(node)
1421 self.removed.append(node)
1421
1422
1422 def reset(self):
1423 def reset(self):
1423 """
1424 """
1424 Resets this instance to initial state (cleans ``added``, ``changed``
1425 Resets this instance to initial state (cleans ``added``, ``changed``
1425 and ``removed`` lists).
1426 and ``removed`` lists).
1426 """
1427 """
1427 self.added = []
1428 self.added = []
1428 self.changed = []
1429 self.changed = []
1429 self.removed = []
1430 self.removed = []
1430 self.parents = []
1431 self.parents = []
1431
1432
1432 def get_ipaths(self):
1433 def get_ipaths(self):
1433 """
1434 """
1434 Returns generator of paths from nodes marked as added, changed or
1435 Returns generator of paths from nodes marked as added, changed or
1435 removed.
1436 removed.
1436 """
1437 """
1437 for node in itertools.chain(self.added, self.changed, self.removed):
1438 for node in itertools.chain(self.added, self.changed, self.removed):
1438 yield node.path
1439 yield node.path
1439
1440
1440 def get_paths(self):
1441 def get_paths(self):
1441 """
1442 """
1442 Returns list of paths from nodes marked as added, changed or removed.
1443 Returns list of paths from nodes marked as added, changed or removed.
1443 """
1444 """
1444 return list(self.get_ipaths())
1445 return list(self.get_ipaths())
1445
1446
1446 def check_integrity(self, parents=None):
1447 def check_integrity(self, parents=None):
1447 """
1448 """
1448 Checks in-memory commit's integrity. Also, sets parents if not
1449 Checks in-memory commit's integrity. Also, sets parents if not
1449 already set.
1450 already set.
1450
1451
1451 :raises CommitError: if any error occurs (i.e.
1452 :raises CommitError: if any error occurs (i.e.
1452 ``NodeDoesNotExistError``).
1453 ``NodeDoesNotExistError``).
1453 """
1454 """
1454 if not self.parents:
1455 if not self.parents:
1455 parents = parents or []
1456 parents = parents or []
1456 if len(parents) == 0:
1457 if len(parents) == 0:
1457 try:
1458 try:
1458 parents = [self.repository.get_commit(), None]
1459 parents = [self.repository.get_commit(), None]
1459 except EmptyRepositoryError:
1460 except EmptyRepositoryError:
1460 parents = [None, None]
1461 parents = [None, None]
1461 elif len(parents) == 1:
1462 elif len(parents) == 1:
1462 parents += [None]
1463 parents += [None]
1463 self.parents = parents
1464 self.parents = parents
1464
1465
1465 # Local parents, only if not None
1466 # Local parents, only if not None
1466 parents = [p for p in self.parents if p]
1467 parents = [p for p in self.parents if p]
1467
1468
1468 # Check nodes marked as added
1469 # Check nodes marked as added
1469 for p in parents:
1470 for p in parents:
1470 for node in self.added:
1471 for node in self.added:
1471 try:
1472 try:
1472 p.get_node(node.path)
1473 p.get_node(node.path)
1473 except NodeDoesNotExistError:
1474 except NodeDoesNotExistError:
1474 pass
1475 pass
1475 else:
1476 else:
1476 raise NodeAlreadyExistsError(
1477 raise NodeAlreadyExistsError(
1477 "Node `%s` already exists at %s" % (node.path, p))
1478 "Node `%s` already exists at %s" % (node.path, p))
1478
1479
1479 # Check nodes marked as changed
1480 # Check nodes marked as changed
1480 missing = set(self.changed)
1481 missing = set(self.changed)
1481 not_changed = set(self.changed)
1482 not_changed = set(self.changed)
1482 if self.changed and not parents:
1483 if self.changed and not parents:
1483 raise NodeDoesNotExistError(str(self.changed[0].path))
1484 raise NodeDoesNotExistError(str(self.changed[0].path))
1484 for p in parents:
1485 for p in parents:
1485 for node in self.changed:
1486 for node in self.changed:
1486 try:
1487 try:
1487 old = p.get_node(node.path)
1488 old = p.get_node(node.path)
1488 missing.remove(node)
1489 missing.remove(node)
1489 # if content actually changed, remove node from not_changed
1490 # if content actually changed, remove node from not_changed
1490 if old.content != node.content:
1491 if old.content != node.content:
1491 not_changed.remove(node)
1492 not_changed.remove(node)
1492 except NodeDoesNotExistError:
1493 except NodeDoesNotExistError:
1493 pass
1494 pass
1494 if self.changed and missing:
1495 if self.changed and missing:
1495 raise NodeDoesNotExistError(
1496 raise NodeDoesNotExistError(
1496 "Node `%s` marked as modified but missing in parents: %s"
1497 "Node `%s` marked as modified but missing in parents: %s"
1497 % (node.path, parents))
1498 % (node.path, parents))
1498
1499
1499 if self.changed and not_changed:
1500 if self.changed and not_changed:
1500 raise NodeNotChangedError(
1501 raise NodeNotChangedError(
1501 "Node `%s` wasn't actually changed (parents: %s)"
1502 "Node `%s` wasn't actually changed (parents: %s)"
1502 % (not_changed.pop().path, parents))
1503 % (not_changed.pop().path, parents))
1503
1504
1504 # Check nodes marked as removed
1505 # Check nodes marked as removed
1505 if self.removed and not parents:
1506 if self.removed and not parents:
1506 raise NodeDoesNotExistError(
1507 raise NodeDoesNotExistError(
1507 "Cannot remove node at %s as there "
1508 "Cannot remove node at %s as there "
1508 "were no parents specified" % self.removed[0].path)
1509 "were no parents specified" % self.removed[0].path)
1509 really_removed = set()
1510 really_removed = set()
1510 for p in parents:
1511 for p in parents:
1511 for node in self.removed:
1512 for node in self.removed:
1512 try:
1513 try:
1513 p.get_node(node.path)
1514 p.get_node(node.path)
1514 really_removed.add(node)
1515 really_removed.add(node)
1515 except CommitError:
1516 except CommitError:
1516 pass
1517 pass
1517 not_removed = set(self.removed) - really_removed
1518 not_removed = set(self.removed) - really_removed
1518 if not_removed:
1519 if not_removed:
1519 # TODO: johbo: This code branch does not seem to be covered
1520 # TODO: johbo: This code branch does not seem to be covered
1520 raise NodeDoesNotExistError(
1521 raise NodeDoesNotExistError(
1521 "Cannot remove node at %s from "
1522 "Cannot remove node at %s from "
1522 "following parents: %s" % (not_removed, parents))
1523 "following parents: %s" % (not_removed, parents))
1523
1524
1524 def commit(self, message, author, parents=None, branch=None, date=None, **kwargs):
1525 def commit(self, message, author, parents=None, branch=None, date=None, **kwargs):
1525 """
1526 """
1526 Performs in-memory commit (doesn't check workdir in any way) and
1527 Performs in-memory commit (doesn't check workdir in any way) and
1527 returns newly created :class:`BaseCommit`. Updates repository's
1528 returns newly created :class:`BaseCommit`. Updates repository's
1528 attribute `commits`.
1529 attribute `commits`.
1529
1530
1530 .. note::
1531 .. note::
1531
1532
1532 While overriding this method each backend's should call
1533 While overriding this method each backend's should call
1533 ``self.check_integrity(parents)`` in the first place.
1534 ``self.check_integrity(parents)`` in the first place.
1534
1535
1535 :param message: message of the commit
1536 :param message: message of the commit
1536 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
1537 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
1537 :param parents: single parent or sequence of parents from which commit
1538 :param parents: single parent or sequence of parents from which commit
1538 would be derived
1539 would be derived
1539 :param date: ``datetime.datetime`` instance. Defaults to
1540 :param date: ``datetime.datetime`` instance. Defaults to
1540 ``datetime.datetime.now()``.
1541 ``datetime.datetime.now()``.
1541 :param branch: branch name, as string. If none given, default backend's
1542 :param branch: branch name, as string. If none given, default backend's
1542 branch would be used.
1543 branch would be used.
1543
1544
1544 :raises ``CommitError``: if any error occurs while committing
1545 :raises ``CommitError``: if any error occurs while committing
1545 """
1546 """
1546 raise NotImplementedError
1547 raise NotImplementedError
1547
1548
1548
1549
1549 class BaseInMemoryChangesetClass(type):
1550 class BaseInMemoryChangesetClass(type):
1550
1551
1551 def __instancecheck__(self, instance):
1552 def __instancecheck__(self, instance):
1552 return isinstance(instance, BaseInMemoryCommit)
1553 return isinstance(instance, BaseInMemoryCommit)
1553
1554
1554
1555
1555 class BaseInMemoryChangeset(BaseInMemoryCommit):
1556 class BaseInMemoryChangeset(BaseInMemoryCommit):
1556
1557
1557 __metaclass__ = BaseInMemoryChangesetClass
1558 __metaclass__ = BaseInMemoryChangesetClass
1558
1559
1559 def __new__(cls, *args, **kwargs):
1560 def __new__(cls, *args, **kwargs):
1560 warnings.warn(
1561 warnings.warn(
1561 "Use BaseCommit instead of BaseInMemoryCommit", DeprecationWarning)
1562 "Use BaseCommit instead of BaseInMemoryCommit", DeprecationWarning)
1562 return super(BaseInMemoryChangeset, cls).__new__(cls, *args, **kwargs)
1563 return super(BaseInMemoryChangeset, cls).__new__(cls, *args, **kwargs)
1563
1564
1564
1565
1565 class EmptyCommit(BaseCommit):
1566 class EmptyCommit(BaseCommit):
1566 """
1567 """
1567 An dummy empty commit. It's possible to pass hash when creating
1568 An dummy empty commit. It's possible to pass hash when creating
1568 an EmptyCommit
1569 an EmptyCommit
1569 """
1570 """
1570
1571
1571 def __init__(
1572 def __init__(
1572 self, commit_id='0' * 40, repo=None, alias=None, idx=-1,
1573 self, commit_id=EMPTY_COMMIT_ID, repo=None, alias=None, idx=-1,
1573 message='', author='', date=None):
1574 message='', author='', date=None):
1574 self._empty_commit_id = commit_id
1575 self._empty_commit_id = commit_id
1575 # TODO: johbo: Solve idx parameter, default value does not make
1576 # TODO: johbo: Solve idx parameter, default value does not make
1576 # too much sense
1577 # too much sense
1577 self.idx = idx
1578 self.idx = idx
1578 self.message = message
1579 self.message = message
1579 self.author = author
1580 self.author = author
1580 self.date = date or datetime.datetime.fromtimestamp(0)
1581 self.date = date or datetime.datetime.fromtimestamp(0)
1581 self.repository = repo
1582 self.repository = repo
1582 self.alias = alias
1583 self.alias = alias
1583
1584
1584 @LazyProperty
1585 @LazyProperty
1585 def raw_id(self):
1586 def raw_id(self):
1586 """
1587 """
1587 Returns raw string identifying this commit, useful for web
1588 Returns raw string identifying this commit, useful for web
1588 representation.
1589 representation.
1589 """
1590 """
1590
1591
1591 return self._empty_commit_id
1592 return self._empty_commit_id
1592
1593
1593 @LazyProperty
1594 @LazyProperty
1594 def branch(self):
1595 def branch(self):
1595 if self.alias:
1596 if self.alias:
1596 from rhodecode.lib.vcs.backends import get_backend
1597 from rhodecode.lib.vcs.backends import get_backend
1597 return get_backend(self.alias).DEFAULT_BRANCH_NAME
1598 return get_backend(self.alias).DEFAULT_BRANCH_NAME
1598
1599
1599 @LazyProperty
1600 @LazyProperty
1600 def short_id(self):
1601 def short_id(self):
1601 return self.raw_id[:12]
1602 return self.raw_id[:12]
1602
1603
1603 @LazyProperty
1604 @LazyProperty
1604 def id(self):
1605 def id(self):
1605 return self.raw_id
1606 return self.raw_id
1606
1607
1607 def get_path_commit(self, path):
1608 def get_path_commit(self, path):
1608 return self
1609 return self
1609
1610
1610 def get_file_content(self, path):
1611 def get_file_content(self, path):
1611 return u''
1612 return u''
1612
1613
1613 def get_file_size(self, path):
1614 def get_file_size(self, path):
1614 return 0
1615 return 0
1615
1616
1616
1617
1617 class EmptyChangesetClass(type):
1618 class EmptyChangesetClass(type):
1618
1619
1619 def __instancecheck__(self, instance):
1620 def __instancecheck__(self, instance):
1620 return isinstance(instance, EmptyCommit)
1621 return isinstance(instance, EmptyCommit)
1621
1622
1622
1623
1623 class EmptyChangeset(EmptyCommit):
1624 class EmptyChangeset(EmptyCommit):
1624
1625
1625 __metaclass__ = EmptyChangesetClass
1626 __metaclass__ = EmptyChangesetClass
1626
1627
1627 def __new__(cls, *args, **kwargs):
1628 def __new__(cls, *args, **kwargs):
1628 warnings.warn(
1629 warnings.warn(
1629 "Use EmptyCommit instead of EmptyChangeset", DeprecationWarning)
1630 "Use EmptyCommit instead of EmptyChangeset", DeprecationWarning)
1630 return super(EmptyCommit, cls).__new__(cls, *args, **kwargs)
1631 return super(EmptyCommit, cls).__new__(cls, *args, **kwargs)
1631
1632
1632 def __init__(self, cs='0' * 40, repo=None, requested_revision=None,
1633 def __init__(self, cs=EMPTY_COMMIT_ID, repo=None, requested_revision=None,
1633 alias=None, revision=-1, message='', author='', date=None):
1634 alias=None, revision=-1, message='', author='', date=None):
1634 if requested_revision is not None:
1635 if requested_revision is not None:
1635 warnings.warn(
1636 warnings.warn(
1636 "Parameter requested_revision not supported anymore",
1637 "Parameter requested_revision not supported anymore",
1637 DeprecationWarning)
1638 DeprecationWarning)
1638 super(EmptyChangeset, self).__init__(
1639 super(EmptyChangeset, self).__init__(
1639 commit_id=cs, repo=repo, alias=alias, idx=revision,
1640 commit_id=cs, repo=repo, alias=alias, idx=revision,
1640 message=message, author=author, date=date)
1641 message=message, author=author, date=date)
1641
1642
1642 @property
1643 @property
1643 def revision(self):
1644 def revision(self):
1644 warnings.warn("Use idx instead", DeprecationWarning)
1645 warnings.warn("Use idx instead", DeprecationWarning)
1645 return self.idx
1646 return self.idx
1646
1647
1647 @revision.setter
1648 @revision.setter
1648 def revision(self, value):
1649 def revision(self, value):
1649 warnings.warn("Use idx instead", DeprecationWarning)
1650 warnings.warn("Use idx instead", DeprecationWarning)
1650 self.idx = value
1651 self.idx = value
1651
1652
1652
1653
1653 class EmptyRepository(BaseRepository):
1654 class EmptyRepository(BaseRepository):
1654 def __init__(self, repo_path=None, config=None, create=False, **kwargs):
1655 def __init__(self, repo_path=None, config=None, create=False, **kwargs):
1655 pass
1656 pass
1656
1657
1657 def get_diff(self, *args, **kwargs):
1658 def get_diff(self, *args, **kwargs):
1658 from rhodecode.lib.vcs.backends.git.diff import GitDiff
1659 from rhodecode.lib.vcs.backends.git.diff import GitDiff
1659 return GitDiff('')
1660 return GitDiff('')
1660
1661
1661
1662
1662 class CollectionGenerator(object):
1663 class CollectionGenerator(object):
1663
1664
1664 def __init__(self, repo, commit_ids, collection_size=None, pre_load=None, translate_tag=None):
1665 def __init__(self, repo, commit_ids, collection_size=None, pre_load=None, translate_tag=None):
1665 self.repo = repo
1666 self.repo = repo
1666 self.commit_ids = commit_ids
1667 self.commit_ids = commit_ids
1667 # TODO: (oliver) this isn't currently hooked up
1668 # TODO: (oliver) this isn't currently hooked up
1668 self.collection_size = None
1669 self.collection_size = None
1669 self.pre_load = pre_load
1670 self.pre_load = pre_load
1670 self.translate_tag = translate_tag
1671 self.translate_tag = translate_tag
1671
1672
1672 def __len__(self):
1673 def __len__(self):
1673 if self.collection_size is not None:
1674 if self.collection_size is not None:
1674 return self.collection_size
1675 return self.collection_size
1675 return self.commit_ids.__len__()
1676 return self.commit_ids.__len__()
1676
1677
1677 def __iter__(self):
1678 def __iter__(self):
1678 for commit_id in self.commit_ids:
1679 for commit_id in self.commit_ids:
1679 # TODO: johbo: Mercurial passes in commit indices or commit ids
1680 # TODO: johbo: Mercurial passes in commit indices or commit ids
1680 yield self._commit_factory(commit_id)
1681 yield self._commit_factory(commit_id)
1681
1682
1682 def _commit_factory(self, commit_id):
1683 def _commit_factory(self, commit_id):
1683 """
1684 """
1684 Allows backends to override the way commits are generated.
1685 Allows backends to override the way commits are generated.
1685 """
1686 """
1686 return self.repo.get_commit(
1687 return self.repo.get_commit(
1687 commit_id=commit_id, pre_load=self.pre_load,
1688 commit_id=commit_id, pre_load=self.pre_load,
1688 translate_tag=self.translate_tag)
1689 translate_tag=self.translate_tag)
1689
1690
1690 def __getslice__(self, i, j):
1691 def __getslice__(self, i, j):
1691 """
1692 """
1692 Returns an iterator of sliced repository
1693 Returns an iterator of sliced repository
1693 """
1694 """
1694 commit_ids = self.commit_ids[i:j]
1695 commit_ids = self.commit_ids[i:j]
1695 return self.__class__(
1696 return self.__class__(
1696 self.repo, commit_ids, pre_load=self.pre_load,
1697 self.repo, commit_ids, pre_load=self.pre_load,
1697 translate_tag=self.translate_tag)
1698 translate_tag=self.translate_tag)
1698
1699
1699 def __repr__(self):
1700 def __repr__(self):
1700 return '<CollectionGenerator[len:%s]>' % (self.__len__())
1701 return '<CollectionGenerator[len:%s]>' % (self.__len__())
1701
1702
1702
1703
1703 class Config(object):
1704 class Config(object):
1704 """
1705 """
1705 Represents the configuration for a repository.
1706 Represents the configuration for a repository.
1706
1707
1707 The API is inspired by :class:`ConfigParser.ConfigParser` from the
1708 The API is inspired by :class:`ConfigParser.ConfigParser` from the
1708 standard library. It implements only the needed subset.
1709 standard library. It implements only the needed subset.
1709 """
1710 """
1710
1711
1711 def __init__(self):
1712 def __init__(self):
1712 self._values = {}
1713 self._values = {}
1713
1714
1714 def copy(self):
1715 def copy(self):
1715 clone = Config()
1716 clone = Config()
1716 for section, values in self._values.items():
1717 for section, values in self._values.items():
1717 clone._values[section] = values.copy()
1718 clone._values[section] = values.copy()
1718 return clone
1719 return clone
1719
1720
1720 def __repr__(self):
1721 def __repr__(self):
1721 return '<Config(%s sections) at %s>' % (
1722 return '<Config(%s sections) at %s>' % (
1722 len(self._values), hex(id(self)))
1723 len(self._values), hex(id(self)))
1723
1724
1724 def items(self, section):
1725 def items(self, section):
1725 return self._values.get(section, {}).iteritems()
1726 return self._values.get(section, {}).iteritems()
1726
1727
1727 def get(self, section, option):
1728 def get(self, section, option):
1728 return self._values.get(section, {}).get(option)
1729 return self._values.get(section, {}).get(option)
1729
1730
1730 def set(self, section, option, value):
1731 def set(self, section, option, value):
1731 section_values = self._values.setdefault(section, {})
1732 section_values = self._values.setdefault(section, {})
1732 section_values[option] = value
1733 section_values[option] = value
1733
1734
1734 def clear_section(self, section):
1735 def clear_section(self, section):
1735 self._values[section] = {}
1736 self._values[section] = {}
1736
1737
1737 def serialize(self):
1738 def serialize(self):
1738 """
1739 """
1739 Creates a list of three tuples (section, key, value) representing
1740 Creates a list of three tuples (section, key, value) representing
1740 this config object.
1741 this config object.
1741 """
1742 """
1742 items = []
1743 items = []
1743 for section in self._values:
1744 for section in self._values:
1744 for option, value in self._values[section].items():
1745 for option, value in self._values[section].items():
1745 items.append(
1746 items.append(
1746 (safe_str(section), safe_str(option), safe_str(value)))
1747 (safe_str(section), safe_str(option), safe_str(value)))
1747 return items
1748 return items
1748
1749
1749
1750
1750 class Diff(object):
1751 class Diff(object):
1751 """
1752 """
1752 Represents a diff result from a repository backend.
1753 Represents a diff result from a repository backend.
1753
1754
1754 Subclasses have to provide a backend specific value for
1755 Subclasses have to provide a backend specific value for
1755 :attr:`_header_re` and :attr:`_meta_re`.
1756 :attr:`_header_re` and :attr:`_meta_re`.
1756 """
1757 """
1757 _meta_re = None
1758 _meta_re = None
1758 _header_re = None
1759 _header_re = None
1759
1760
1760 def __init__(self, raw_diff):
1761 def __init__(self, raw_diff):
1761 self.raw = raw_diff
1762 self.raw = raw_diff
1762
1763
1763 def chunks(self):
1764 def chunks(self):
1764 """
1765 """
1765 split the diff in chunks of separate --git a/file b/file chunks
1766 split the diff in chunks of separate --git a/file b/file chunks
1766 to make diffs consistent we must prepend with \n, and make sure
1767 to make diffs consistent we must prepend with \n, and make sure
1767 we can detect last chunk as this was also has special rule
1768 we can detect last chunk as this was also has special rule
1768 """
1769 """
1769
1770
1770 diff_parts = ('\n' + self.raw).split('\ndiff --git')
1771 diff_parts = ('\n' + self.raw).split('\ndiff --git')
1771 header = diff_parts[0]
1772 header = diff_parts[0]
1772
1773
1773 if self._meta_re:
1774 if self._meta_re:
1774 match = self._meta_re.match(header)
1775 match = self._meta_re.match(header)
1775
1776
1776 chunks = diff_parts[1:]
1777 chunks = diff_parts[1:]
1777 total_chunks = len(chunks)
1778 total_chunks = len(chunks)
1778
1779
1779 return (
1780 return (
1780 DiffChunk(chunk, self, cur_chunk == total_chunks)
1781 DiffChunk(chunk, self, cur_chunk == total_chunks)
1781 for cur_chunk, chunk in enumerate(chunks, start=1))
1782 for cur_chunk, chunk in enumerate(chunks, start=1))
1782
1783
1783
1784
1784 class DiffChunk(object):
1785 class DiffChunk(object):
1785
1786
1786 def __init__(self, chunk, diff, last_chunk):
1787 def __init__(self, chunk, diff, last_chunk):
1787 self._diff = diff
1788 self._diff = diff
1788
1789
1789 # since we split by \ndiff --git that part is lost from original diff
1790 # since we split by \ndiff --git that part is lost from original diff
1790 # we need to re-apply it at the end, EXCEPT ! if it's last chunk
1791 # we need to re-apply it at the end, EXCEPT ! if it's last chunk
1791 if not last_chunk:
1792 if not last_chunk:
1792 chunk += '\n'
1793 chunk += '\n'
1793
1794
1794 match = self._diff._header_re.match(chunk)
1795 match = self._diff._header_re.match(chunk)
1795 self.header = match.groupdict()
1796 self.header = match.groupdict()
1796 self.diff = chunk[match.end():]
1797 self.diff = chunk[match.end():]
1797 self.raw = chunk
1798 self.raw = chunk
1798
1799
1799
1800
1800 class BasePathPermissionChecker(object):
1801 class BasePathPermissionChecker(object):
1801
1802
1802 @staticmethod
1803 @staticmethod
1803 def create_from_patterns(includes, excludes):
1804 def create_from_patterns(includes, excludes):
1804 if includes and '*' in includes and not excludes:
1805 if includes and '*' in includes and not excludes:
1805 return AllPathPermissionChecker()
1806 return AllPathPermissionChecker()
1806 elif excludes and '*' in excludes:
1807 elif excludes and '*' in excludes:
1807 return NonePathPermissionChecker()
1808 return NonePathPermissionChecker()
1808 else:
1809 else:
1809 return PatternPathPermissionChecker(includes, excludes)
1810 return PatternPathPermissionChecker(includes, excludes)
1810
1811
1811 @property
1812 @property
1812 def has_full_access(self):
1813 def has_full_access(self):
1813 raise NotImplemented()
1814 raise NotImplemented()
1814
1815
1815 def has_access(self, path):
1816 def has_access(self, path):
1816 raise NotImplemented()
1817 raise NotImplemented()
1817
1818
1818
1819
1819 class AllPathPermissionChecker(BasePathPermissionChecker):
1820 class AllPathPermissionChecker(BasePathPermissionChecker):
1820
1821
1821 @property
1822 @property
1822 def has_full_access(self):
1823 def has_full_access(self):
1823 return True
1824 return True
1824
1825
1825 def has_access(self, path):
1826 def has_access(self, path):
1826 return True
1827 return True
1827
1828
1828
1829
1829 class NonePathPermissionChecker(BasePathPermissionChecker):
1830 class NonePathPermissionChecker(BasePathPermissionChecker):
1830
1831
1831 @property
1832 @property
1832 def has_full_access(self):
1833 def has_full_access(self):
1833 return False
1834 return False
1834
1835
1835 def has_access(self, path):
1836 def has_access(self, path):
1836 return False
1837 return False
1837
1838
1838
1839
1839 class PatternPathPermissionChecker(BasePathPermissionChecker):
1840 class PatternPathPermissionChecker(BasePathPermissionChecker):
1840
1841
1841 def __init__(self, includes, excludes):
1842 def __init__(self, includes, excludes):
1842 self.includes = includes
1843 self.includes = includes
1843 self.excludes = excludes
1844 self.excludes = excludes
1844 self.includes_re = [] if not includes else [
1845 self.includes_re = [] if not includes else [
1845 re.compile(fnmatch.translate(pattern)) for pattern in includes]
1846 re.compile(fnmatch.translate(pattern)) for pattern in includes]
1846 self.excludes_re = [] if not excludes else [
1847 self.excludes_re = [] if not excludes else [
1847 re.compile(fnmatch.translate(pattern)) for pattern in excludes]
1848 re.compile(fnmatch.translate(pattern)) for pattern in excludes]
1848
1849
1849 @property
1850 @property
1850 def has_full_access(self):
1851 def has_full_access(self):
1851 return '*' in self.includes and not self.excludes
1852 return '*' in self.includes and not self.excludes
1852
1853
1853 def has_access(self, path):
1854 def has_access(self, path):
1854 for regex in self.excludes_re:
1855 for regex in self.excludes_re:
1855 if regex.match(path):
1856 if regex.match(path):
1856 return False
1857 return False
1857 for regex in self.includes_re:
1858 for regex in self.includes_re:
1858 if regex.match(path):
1859 if regex.match(path):
1859 return True
1860 return True
1860 return False
1861 return False
@@ -1,311 +1,311 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <%inherit file="/base/base.mako"/>
2 <%inherit file="/base/base.mako"/>
3 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
3 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
4
4
5 <%def name="title()">
5 <%def name="title()">
6 %if c.compare_home:
6 %if c.compare_home:
7 ${_('%s Compare') % c.repo_name}
7 ${_('%s Compare') % c.repo_name}
8 %else:
8 %else:
9 ${_('%s Compare') % c.repo_name} - ${'%s@%s' % (c.source_repo.repo_name, c.source_ref)} &gt; ${'%s@%s' % (c.target_repo.repo_name, c.target_ref)}
9 ${_('%s Compare') % c.repo_name} - ${'%s@%s' % (c.source_repo.repo_name, c.source_ref)} &gt; ${'%s@%s' % (c.target_repo.repo_name, c.target_ref)}
10 %endif
10 %endif
11 %if c.rhodecode_name:
11 %if c.rhodecode_name:
12 &middot; ${h.branding(c.rhodecode_name)}
12 &middot; ${h.branding(c.rhodecode_name)}
13 %endif
13 %endif
14 </%def>
14 </%def>
15
15
16 <%def name="breadcrumbs_links()"></%def>
16 <%def name="breadcrumbs_links()"></%def>
17
17
18 <%def name="menu_bar_nav()">
18 <%def name="menu_bar_nav()">
19 ${self.menu_items(active='repositories')}
19 ${self.menu_items(active='repositories')}
20 </%def>
20 </%def>
21
21
22 <%def name="menu_bar_subnav()">
22 <%def name="menu_bar_subnav()">
23 ${self.repo_menu(active='compare')}
23 ${self.repo_menu(active='compare')}
24 </%def>
24 </%def>
25
25
26 <%def name="main()">
26 <%def name="main()">
27 <script type="text/javascript">
27 <script type="text/javascript">
28 // set fake commitId on this commit-range page
28 // set fake commitId on this commit-range page
29 templateContext.commit_data.commit_id = "${h.EmptyCommit().raw_id}";
29 templateContext.commit_data.commit_id = "${h.EmptyCommit().raw_id}";
30 </script>
30 </script>
31
31
32 <div class="box">
32 <div class="box">
33 <div class="summary changeset">
33 <div class="summary changeset">
34 <div class="summary-detail">
34 <div class="summary-detail">
35 <div class="summary-detail-header">
35 <div class="summary-detail-header">
36 <span class="breadcrumbs files_location">
36 <span class="breadcrumbs files_location">
37 <h4>
37 <h4>
38 ${_('Compare Commits')}
38 ${_('Compare Commits')}
39 % if c.file_path:
39 % if c.file_path:
40 ${_('for file')} <a href="#${'a_' + h.FID('',c.file_path)}">${c.file_path}</a>
40 ${_('for file')} <a href="#${('a_' + h.FID('',c.file_path))}">${c.file_path}</a>
41 % endif
41 % endif
42
42
43 % if c.commit_ranges:
43 % if c.commit_ranges:
44 <code>
44 <code>
45 r${c.source_commit.idx}:${h.short_id(c.source_commit.raw_id)}...r${c.target_commit.idx}:${h.short_id(c.target_commit.raw_id)}
45 r${c.commit_ranges[0].idx}:${h.short_id(c.commit_ranges[0].raw_id)}...r${c.commit_ranges[-1].idx}:${h.short_id(c.commit_ranges[-1].raw_id)}
46 </code>
46 </code>
47 % endif
47 % endif
48 </h4>
48 </h4>
49 </span>
49 </span>
50
50
51 <div class="clear-fix"></div>
51 <div class="clear-fix"></div>
52 </div>
52 </div>
53
53
54 <div class="fieldset">
54 <div class="fieldset">
55 <div class="left-label-summary">
55 <div class="left-label-summary">
56 <p class="spacing">${_('Target')}:</p>
56 <p class="spacing">${_('Target')}:</p>
57 <div class="right-label-summary">
57 <div class="right-label-summary">
58 <div class="code-header" >
58 <div class="code-header" >
59 <div class="compare_header">
59 <div class="compare_header">
60 ## The hidden elements are replaced with a select2 widget
60 ## The hidden elements are replaced with a select2 widget
61 ${h.hidden('compare_source')}
61 ${h.hidden('compare_source')}
62 </div>
62 </div>
63 </div>
63 </div>
64 </div>
64 </div>
65 </div>
65 </div>
66 </div>
66 </div>
67
67
68 <div class="fieldset">
68 <div class="fieldset">
69 <div class="left-label-summary">
69 <div class="left-label-summary">
70 <p class="spacing">${_('Source')}:</p>
70 <p class="spacing">${_('Source')}:</p>
71 <div class="right-label-summary">
71 <div class="right-label-summary">
72 <div class="code-header" >
72 <div class="code-header" >
73 <div class="compare_header">
73 <div class="compare_header">
74 ## The hidden elements are replaced with a select2 widget
74 ## The hidden elements are replaced with a select2 widget
75 ${h.hidden('compare_target')}
75 ${h.hidden('compare_target')}
76 </div>
76 </div>
77 </div>
77 </div>
78 </div>
78 </div>
79 </div>
79 </div>
80 </div>
80 </div>
81
81
82 <div class="fieldset">
82 <div class="fieldset">
83 <div class="left-label-summary">
83 <div class="left-label-summary">
84 <p class="spacing">${_('Actions')}:</p>
84 <p class="spacing">${_('Actions')}:</p>
85 <div class="right-label-summary">
85 <div class="right-label-summary">
86 <div class="code-header" >
86 <div class="code-header" >
87 <div class="compare_header">
87 <div class="compare_header">
88 <div class="compare-buttons">
88 <div class="compare-buttons">
89 % if c.compare_home:
89 % if c.compare_home:
90 <a id="compare_revs" class="btn btn-primary"> ${_('Compare Commits')}</a>
90 <a id="compare_revs" class="btn btn-primary"> ${_('Compare Commits')}</a>
91 %if c.rhodecode_db_repo.fork:
91 %if c.rhodecode_db_repo.fork:
92
92
93 <a class="btn btn-default" title="${h.tooltip(_('Compare fork with %s' % c.rhodecode_db_repo.fork.repo_name))}"
93 <a class="btn btn-default" title="${h.tooltip(_('Compare fork with %s' % c.rhodecode_db_repo.fork.repo_name))}"
94 href="${h.route_path('repo_compare',
94 href="${h.route_path('repo_compare',
95 repo_name=c.rhodecode_db_repo.fork.repo_name,
95 repo_name=c.rhodecode_db_repo.fork.repo_name,
96 source_ref_type=c.rhodecode_db_repo.landing_rev[0],
96 source_ref_type=c.rhodecode_db_repo.landing_rev[0],
97 source_ref=c.rhodecode_db_repo.landing_rev[1],
97 source_ref=c.rhodecode_db_repo.landing_rev[1],
98 target_repo=c.repo_name,target_ref_type='branch' if request.GET.get('branch') else c.rhodecode_db_repo.landing_rev[0],
98 target_repo=c.repo_name,target_ref_type='branch' if request.GET.get('branch') else c.rhodecode_db_repo.landing_rev[0],
99 target_ref=request.GET.get('branch') or c.rhodecode_db_repo.landing_rev[1],
99 target_ref=request.GET.get('branch') or c.rhodecode_db_repo.landing_rev[1],
100 _query=dict(merge=1))}"
100 _query=dict(merge=1))}"
101 >
101 >
102 ${_('Compare with origin')}
102 ${_('Compare with origin')}
103 </a>
103 </a>
104
104
105 %endif
105 %endif
106
106
107 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Swap')}</a>
107 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Swap')}</a>
108 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Comment')}</a>
108 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Comment')}</a>
109 <div id="changeset_compare_view_content">
109 <div id="changeset_compare_view_content">
110 <div class="help-block">${_('Compare commits, branches, bookmarks or tags.')}</div>
110 <div class="help-block">${_('Compare commits, branches, bookmarks or tags.')}</div>
111 </div>
111 </div>
112
112
113 % elif c.preview_mode:
113 % elif c.preview_mode:
114 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Compare Commits')}</a>
114 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Compare Commits')}</a>
115 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Swap')}</a>
115 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Swap')}</a>
116 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Comment')}</a>
116 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Comment')}</a>
117
117
118 % else:
118 % else:
119 <a id="compare_revs" class="btn btn-primary"> ${_('Compare Commits')}</a>
119 <a id="compare_revs" class="btn btn-primary"> ${_('Compare Commits')}</a>
120 <a id="btn-swap" class="btn btn-primary" href="${c.swap_url}">${_('Swap')}</a>
120 <a id="btn-swap" class="btn btn-primary" href="${c.swap_url}">${_('Swap')}</a>
121
121
122 ## allow comment only if there are commits to comment on
122 ## allow comment only if there are commits to comment on
123 % if c.diffset and c.diffset.files and c.commit_ranges:
123 % if c.diffset and c.diffset.files and c.commit_ranges:
124 <a id="compare_changeset_status_toggle" class="btn btn-primary">${_('Comment')}</a>
124 <a id="compare_changeset_status_toggle" class="btn btn-primary">${_('Comment')}</a>
125 % else:
125 % else:
126 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Comment')}</a>
126 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Comment')}</a>
127 % endif
127 % endif
128 % endif
128 % endif
129 </div>
129 </div>
130 </div>
130 </div>
131 </div>
131 </div>
132 </div>
132 </div>
133 </div>
133 </div>
134 </div>
134 </div>
135
135
136 ## commit status form
136 ## commit status form
137 <div class="fieldset" id="compare_changeset_status" style="display: none; margin-bottom: -80px;">
137 <div class="fieldset" id="compare_changeset_status" style="display: none; margin-bottom: -80px;">
138 <div class="left-label-summary">
138 <div class="left-label-summary">
139 <p class="spacing">${_('Commit status')}:</p>
139 <p class="spacing">${_('Commit status')}:</p>
140 <div class="right-label-summary">
140 <div class="right-label-summary">
141 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
141 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
142 ## main comment form and it status
142 ## main comment form and it status
143 <%
143 <%
144 def revs(_revs):
144 def revs(_revs):
145 form_inputs = []
145 form_inputs = []
146 for cs in _revs:
146 for cs in _revs:
147 tmpl = '<input type="hidden" data-commit-id="%(cid)s" name="commit_ids" value="%(cid)s">' % {'cid': cs.raw_id}
147 tmpl = '<input type="hidden" data-commit-id="%(cid)s" name="commit_ids" value="%(cid)s">' % {'cid': cs.raw_id}
148 form_inputs.append(tmpl)
148 form_inputs.append(tmpl)
149 return form_inputs
149 return form_inputs
150 %>
150 %>
151 <div>
151 <div>
152 ${comment.comments(h.route_path('repo_commit_comment_create', repo_name=c.repo_name, commit_id='0'*16), None, is_compare=True, form_extras=revs(c.commit_ranges))}
152 ${comment.comments(h.route_path('repo_commit_comment_create', repo_name=c.repo_name, commit_id='0'*16), None, is_compare=True, form_extras=revs(c.commit_ranges))}
153 </div>
153 </div>
154 </div>
154 </div>
155 </div>
155 </div>
156 </div>
156 </div>
157 <div class="clear-fix"></div>
157 <div class="clear-fix"></div>
158 </div> <!-- end summary-detail -->
158 </div> <!-- end summary-detail -->
159 </div> <!-- end summary -->
159 </div> <!-- end summary -->
160
160
161 ## use JS script to load it quickly before potentially large diffs render long time
161 ## use JS script to load it quickly before potentially large diffs render long time
162 ## this prevents from situation when large diffs block rendering of select2 fields
162 ## this prevents from situation when large diffs block rendering of select2 fields
163 <script type="text/javascript">
163 <script type="text/javascript">
164
164
165 var cache = {};
165 var cache = {};
166
166
167 var formatSelection = function(repoName){
167 var formatSelection = function(repoName){
168 return function(data, container, escapeMarkup) {
168 return function(data, container, escapeMarkup) {
169 var selection = data ? this.text(data) : "";
169 var selection = data ? this.text(data) : "";
170 return escapeMarkup('{0}@{1}'.format(repoName, selection));
170 return escapeMarkup('{0}@{1}'.format(repoName, selection));
171 }
171 }
172 };
172 };
173
173
174 var feedCompareData = function(query, cachedValue){
174 var feedCompareData = function(query, cachedValue){
175 var data = {results: []};
175 var data = {results: []};
176 //filter results
176 //filter results
177 $.each(cachedValue.results, function() {
177 $.each(cachedValue.results, function() {
178 var section = this.text;
178 var section = this.text;
179 var children = [];
179 var children = [];
180 $.each(this.children, function() {
180 $.each(this.children, function() {
181 if (query.term.length === 0 || this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0) {
181 if (query.term.length === 0 || this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0) {
182 children.push({
182 children.push({
183 'id': this.id,
183 'id': this.id,
184 'text': this.text,
184 'text': this.text,
185 'type': this.type
185 'type': this.type
186 })
186 })
187 }
187 }
188 });
188 });
189 data.results.push({
189 data.results.push({
190 'text': section,
190 'text': section,
191 'children': children
191 'children': children
192 })
192 })
193 });
193 });
194 //push the typed in changeset
194 //push the typed in changeset
195 data.results.push({
195 data.results.push({
196 'text': _gettext('specify commit'),
196 'text': _gettext('specify commit'),
197 'children': [{
197 'children': [{
198 'id': query.term,
198 'id': query.term,
199 'text': query.term,
199 'text': query.term,
200 'type': 'rev'
200 'type': 'rev'
201 }]
201 }]
202 });
202 });
203 query.callback(data);
203 query.callback(data);
204 };
204 };
205
205
206 var loadCompareData = function(repoName, query, cache){
206 var loadCompareData = function(repoName, query, cache){
207 $.ajax({
207 $.ajax({
208 url: pyroutes.url('repo_refs_data', {'repo_name': repoName}),
208 url: pyroutes.url('repo_refs_data', {'repo_name': repoName}),
209 data: {},
209 data: {},
210 dataType: 'json',
210 dataType: 'json',
211 type: 'GET',
211 type: 'GET',
212 success: function(data) {
212 success: function(data) {
213 cache[repoName] = data;
213 cache[repoName] = data;
214 query.callback({results: data.results});
214 query.callback({results: data.results});
215 }
215 }
216 })
216 })
217 };
217 };
218
218
219 var enable_fields = ${"false" if c.preview_mode else "true"};
219 var enable_fields = ${"false" if c.preview_mode else "true"};
220 $("#compare_source").select2({
220 $("#compare_source").select2({
221 placeholder: "${'%s@%s' % (c.source_repo.repo_name, c.source_ref)}",
221 placeholder: "${'%s@%s' % (c.source_repo.repo_name, c.source_ref)}",
222 containerCssClass: "drop-menu",
222 containerCssClass: "drop-menu",
223 dropdownCssClass: "drop-menu-dropdown",
223 dropdownCssClass: "drop-menu-dropdown",
224 formatSelection: formatSelection("${c.source_repo.repo_name}"),
224 formatSelection: formatSelection("${c.source_repo.repo_name}"),
225 dropdownAutoWidth: true,
225 dropdownAutoWidth: true,
226 query: function(query) {
226 query: function(query) {
227 var repoName = '${c.source_repo.repo_name}';
227 var repoName = '${c.source_repo.repo_name}';
228 var cachedValue = cache[repoName];
228 var cachedValue = cache[repoName];
229
229
230 if (cachedValue){
230 if (cachedValue){
231 feedCompareData(query, cachedValue);
231 feedCompareData(query, cachedValue);
232 }
232 }
233 else {
233 else {
234 loadCompareData(repoName, query, cache);
234 loadCompareData(repoName, query, cache);
235 }
235 }
236 }
236 }
237 }).select2("enable", enable_fields);
237 }).select2("enable", enable_fields);
238
238
239 $("#compare_target").select2({
239 $("#compare_target").select2({
240 placeholder: "${'%s@%s' % (c.target_repo.repo_name, c.target_ref)}",
240 placeholder: "${'%s@%s' % (c.target_repo.repo_name, c.target_ref)}",
241 dropdownAutoWidth: true,
241 dropdownAutoWidth: true,
242 containerCssClass: "drop-menu",
242 containerCssClass: "drop-menu",
243 dropdownCssClass: "drop-menu-dropdown",
243 dropdownCssClass: "drop-menu-dropdown",
244 formatSelection: formatSelection("${c.target_repo.repo_name}"),
244 formatSelection: formatSelection("${c.target_repo.repo_name}"),
245 query: function(query) {
245 query: function(query) {
246 var repoName = '${c.target_repo.repo_name}';
246 var repoName = '${c.target_repo.repo_name}';
247 var cachedValue = cache[repoName];
247 var cachedValue = cache[repoName];
248
248
249 if (cachedValue){
249 if (cachedValue){
250 feedCompareData(query, cachedValue);
250 feedCompareData(query, cachedValue);
251 }
251 }
252 else {
252 else {
253 loadCompareData(repoName, query, cache);
253 loadCompareData(repoName, query, cache);
254 }
254 }
255 }
255 }
256 }).select2("enable", enable_fields);
256 }).select2("enable", enable_fields);
257 var initial_compare_source = {id: "${c.source_ref}", type:"${c.source_ref_type}"};
257 var initial_compare_source = {id: "${c.source_ref}", type:"${c.source_ref_type}"};
258 var initial_compare_target = {id: "${c.target_ref}", type:"${c.target_ref_type}"};
258 var initial_compare_target = {id: "${c.target_ref}", type:"${c.target_ref_type}"};
259
259
260 $('#compare_revs').on('click', function(e) {
260 $('#compare_revs').on('click', function(e) {
261 var source = $('#compare_source').select2('data') || initial_compare_source;
261 var source = $('#compare_source').select2('data') || initial_compare_source;
262 var target = $('#compare_target').select2('data') || initial_compare_target;
262 var target = $('#compare_target').select2('data') || initial_compare_target;
263 if (source && target) {
263 if (source && target) {
264 var url_data = {
264 var url_data = {
265 repo_name: "${c.repo_name}",
265 repo_name: "${c.repo_name}",
266 source_ref: source.id,
266 source_ref: source.id,
267 source_ref_type: source.type,
267 source_ref_type: source.type,
268 target_ref: target.id,
268 target_ref: target.id,
269 target_ref_type: target.type
269 target_ref_type: target.type
270 };
270 };
271 window.location = pyroutes.url('repo_compare', url_data);
271 window.location = pyroutes.url('repo_compare', url_data);
272 }
272 }
273 });
273 });
274 $('#compare_changeset_status_toggle').on('click', function(e) {
274 $('#compare_changeset_status_toggle').on('click', function(e) {
275 $('#compare_changeset_status').toggle();
275 $('#compare_changeset_status').toggle();
276 });
276 });
277
277
278 </script>
278 </script>
279
279
280 ## table diff data
280 ## table diff data
281 <div class="table">
281 <div class="table">
282 % if not c.compare_home:
282 % if not c.compare_home:
283 <div id="changeset_compare_view_content">
283 <div id="changeset_compare_view_content">
284 <div class="pull-left">
284 <div class="pull-left">
285 <div class="btn-group">
285 <div class="btn-group">
286 <a
286 <a
287 class="btn"
287 class="btn"
288 href="#"
288 href="#"
289 onclick="$('.compare_select').show();$('.compare_select_hidden').hide(); return false">
289 onclick="$('.compare_select').show();$('.compare_select_hidden').hide(); return false">
290 ${_ungettext('Expand %s commit','Expand %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
290 ${_ungettext('Expand %s commit','Expand %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
291 </a>
291 </a>
292 <a
292 <a
293 class="btn"
293 class="btn"
294 href="#"
294 href="#"
295 onclick="$('.compare_select').hide();$('.compare_select_hidden').show(); return false">
295 onclick="$('.compare_select').hide();$('.compare_select_hidden').show(); return false">
296 ${_ungettext('Collapse %s commit','Collapse %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
296 ${_ungettext('Collapse %s commit','Collapse %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
297 </a>
297 </a>
298 </div>
298 </div>
299 </div>
299 </div>
300 <div style="padding:0 10px 10px 0px" class="pull-left"></div>
300 <div style="padding:0 10px 10px 0px" class="pull-left"></div>
301 ## commit compare generated below
301 ## commit compare generated below
302 <%include file="compare_commits.mako"/>
302 <%include file="compare_commits.mako"/>
303 ${cbdiffs.render_diffset_menu(c.diffset)}
303 ${cbdiffs.render_diffset_menu(c.diffset)}
304 ${cbdiffs.render_diffset(c.diffset)}
304 ${cbdiffs.render_diffset(c.diffset)}
305 </div>
305 </div>
306 % endif
306 % endif
307
307
308 </div>
308 </div>
309 </div>
309 </div>
310
310
311 </%def>
311 </%def>
@@ -1,464 +1,467 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 threading
21 import threading
22 import time
22 import time
23 import logging
23 import logging
24 import os.path
24 import os.path
25 import subprocess32
25 import subprocess32
26 import tempfile
26 import tempfile
27 import urllib2
27 import urllib2
28 from lxml.html import fromstring, tostring
28 from lxml.html import fromstring, tostring
29 from lxml.cssselect import CSSSelector
29 from lxml.cssselect import CSSSelector
30 from urlparse import urlparse, parse_qsl
30 from urlparse import urlparse, parse_qsl
31 from urllib import unquote_plus
31 from urllib import unquote_plus
32 import webob
32 import webob
33
33
34 from webtest.app import TestResponse, TestApp, string_types
34 from webtest.app import TestResponse, TestApp, string_types
35 from webtest.compat import print_stderr
35 from webtest.compat import print_stderr
36
36
37 import pytest
37 import pytest
38 import rc_testdata
38 import rc_testdata
39
39
40 from rhodecode.model.db import User, Repository
40 from rhodecode.model.db import User, Repository
41 from rhodecode.model.meta import Session
41 from rhodecode.model.meta import Session
42 from rhodecode.model.scm import ScmModel
42 from rhodecode.model.scm import ScmModel
43 from rhodecode.lib.vcs.backends.svn.repository import SubversionRepository
43 from rhodecode.lib.vcs.backends.svn.repository import SubversionRepository
44 from rhodecode.lib.vcs.backends.base import EmptyCommit
44 from rhodecode.lib.vcs.backends.base import EmptyCommit
45 from rhodecode.tests import login_user_session
45 from rhodecode.tests import login_user_session
46
46
47 log = logging.getLogger(__name__)
47 log = logging.getLogger(__name__)
48
48
49
49
50 class CustomTestResponse(TestResponse):
50 class CustomTestResponse(TestResponse):
51
51 def _save_output(self, out):
52 def _save_output(self, out):
52 f = tempfile.NamedTemporaryFile(
53 f = tempfile.NamedTemporaryFile(delete=False, prefix='rc-test-', suffix='.html')
53 delete=False, prefix='rc-test-', suffix='.html')
54 f.write(out)
54 f.write(out)
55 return f.name
55 return f.name
56
56
57 def mustcontain(self, *strings, **kw):
57 def mustcontain(self, *strings, **kw):
58 """
58 """
59 Assert that the response contains all of the strings passed
59 Assert that the response contains all of the strings passed
60 in as arguments.
60 in as arguments.
61
61
62 Equivalent to::
62 Equivalent to::
63
63
64 assert string in res
64 assert string in res
65 """
65 """
66 print_body = kw.pop('print_body', False)
66 if 'no' in kw:
67 if 'no' in kw:
67 no = kw['no']
68 no = kw['no']
68 del kw['no']
69 del kw['no']
69 if isinstance(no, string_types):
70 if isinstance(no, string_types):
70 no = [no]
71 no = [no]
71 else:
72 else:
72 no = []
73 no = []
73 if kw:
74 if kw:
74 raise TypeError(
75 raise TypeError(
75 "The only keyword argument allowed is 'no' got %s" % kw)
76 "The only keyword argument allowed is 'no' got %s" % kw)
76
77
77 f = self._save_output(str(self))
78 f = self._save_output(str(self))
78
79
79 for s in strings:
80 for s in strings:
80 if not s in self:
81 if not s in self:
81 print_stderr("Actual response (no %r):" % s)
82 print_stderr("Actual response (no %r):" % s)
82 print_stderr(str(self))
83 print_stderr("body output saved as `%s`" % f)
84 if print_body:
85 print_stderr(str(self))
83 raise IndexError(
86 raise IndexError(
84 "Body does not contain string %r, output saved as %s" % (
87 "Body does not contain string %r, body output saved as %s" % (s, f))
85 s, f))
86
88
87 for no_s in no:
89 for no_s in no:
88 if no_s in self:
90 if no_s in self:
89 print_stderr("Actual response (has %r)" % no_s)
91 print_stderr("Actual response (has %r)" % no_s)
90 print_stderr(str(self))
92 print_stderr("body output saved as `%s`" % f)
93 if print_body:
94 print_stderr(str(self))
91 raise IndexError(
95 raise IndexError(
92 "Body contains bad string %r, output saved as %s" % (
96 "Body contains bad string %r, body output saved as %s" % (no_s, f))
93 no_s, f))
94
97
95 def assert_response(self):
98 def assert_response(self):
96 return AssertResponse(self)
99 return AssertResponse(self)
97
100
98 def get_session_from_response(self):
101 def get_session_from_response(self):
99 """
102 """
100 This returns the session from a response object.
103 This returns the session from a response object.
101 """
104 """
102 from rhodecode.lib.rc_beaker import session_factory_from_settings
105 from rhodecode.lib.rc_beaker import session_factory_from_settings
103 session = session_factory_from_settings(self.test_app._pyramid_settings)
106 session = session_factory_from_settings(self.test_app._pyramid_settings)
104 return session(self.request)
107 return session(self.request)
105
108
106
109
107 class TestRequest(webob.BaseRequest):
110 class TestRequest(webob.BaseRequest):
108
111
109 # for py.test
112 # for py.test
110 disabled = True
113 disabled = True
111 ResponseClass = CustomTestResponse
114 ResponseClass = CustomTestResponse
112
115
113 def add_response_callback(self, callback):
116 def add_response_callback(self, callback):
114 pass
117 pass
115
118
116
119
117 class CustomTestApp(TestApp):
120 class CustomTestApp(TestApp):
118 """
121 """
119 Custom app to make mustcontain more Useful, and extract special methods
122 Custom app to make mustcontain more Useful, and extract special methods
120 """
123 """
121 RequestClass = TestRequest
124 RequestClass = TestRequest
122 rc_login_data = {}
125 rc_login_data = {}
123 rc_current_session = None
126 rc_current_session = None
124
127
125 def login(self, username=None, password=None):
128 def login(self, username=None, password=None):
126 from rhodecode.lib import auth
129 from rhodecode.lib import auth
127
130
128 if username and password:
131 if username and password:
129 session = login_user_session(self, username, password)
132 session = login_user_session(self, username, password)
130 else:
133 else:
131 session = login_user_session(self)
134 session = login_user_session(self)
132
135
133 self.rc_login_data['csrf_token'] = auth.get_csrf_token(session)
136 self.rc_login_data['csrf_token'] = auth.get_csrf_token(session)
134 self.rc_current_session = session
137 self.rc_current_session = session
135 return session['rhodecode_user']
138 return session['rhodecode_user']
136
139
137 @property
140 @property
138 def csrf_token(self):
141 def csrf_token(self):
139 return self.rc_login_data['csrf_token']
142 return self.rc_login_data['csrf_token']
140
143
141 @property
144 @property
142 def _pyramid_registry(self):
145 def _pyramid_registry(self):
143 return self.app.config.registry
146 return self.app.config.registry
144
147
145 @property
148 @property
146 def _pyramid_settings(self):
149 def _pyramid_settings(self):
147 return self._pyramid_registry.settings
150 return self._pyramid_registry.settings
148
151
149
152
150 def set_anonymous_access(enabled):
153 def set_anonymous_access(enabled):
151 """(Dis)allows anonymous access depending on parameter `enabled`"""
154 """(Dis)allows anonymous access depending on parameter `enabled`"""
152 user = User.get_default_user()
155 user = User.get_default_user()
153 user.active = enabled
156 user.active = enabled
154 Session().add(user)
157 Session().add(user)
155 Session().commit()
158 Session().commit()
156 time.sleep(1.5) # must sleep for cache (1s to expire)
159 time.sleep(1.5) # must sleep for cache (1s to expire)
157 log.info('anonymous access is now: %s', enabled)
160 log.info('anonymous access is now: %s', enabled)
158 assert enabled == User.get_default_user().active, (
161 assert enabled == User.get_default_user().active, (
159 'Cannot set anonymous access')
162 'Cannot set anonymous access')
160
163
161
164
162 def check_xfail_backends(node, backend_alias):
165 def check_xfail_backends(node, backend_alias):
163 # Using "xfail_backends" here intentionally, since this marks work
166 # Using "xfail_backends" here intentionally, since this marks work
164 # which is "to be done" soon.
167 # which is "to be done" soon.
165 skip_marker = node.get_closest_marker('xfail_backends')
168 skip_marker = node.get_closest_marker('xfail_backends')
166 if skip_marker and backend_alias in skip_marker.args:
169 if skip_marker and backend_alias in skip_marker.args:
167 msg = "Support for backend %s to be developed." % (backend_alias, )
170 msg = "Support for backend %s to be developed." % (backend_alias, )
168 msg = skip_marker.kwargs.get('reason', msg)
171 msg = skip_marker.kwargs.get('reason', msg)
169 pytest.xfail(msg)
172 pytest.xfail(msg)
170
173
171
174
172 def check_skip_backends(node, backend_alias):
175 def check_skip_backends(node, backend_alias):
173 # Using "skip_backends" here intentionally, since this marks work which is
176 # Using "skip_backends" here intentionally, since this marks work which is
174 # not supported.
177 # not supported.
175 skip_marker = node.get_closest_marker('skip_backends')
178 skip_marker = node.get_closest_marker('skip_backends')
176 if skip_marker and backend_alias in skip_marker.args:
179 if skip_marker and backend_alias in skip_marker.args:
177 msg = "Feature not supported for backend %s." % (backend_alias, )
180 msg = "Feature not supported for backend %s." % (backend_alias, )
178 msg = skip_marker.kwargs.get('reason', msg)
181 msg = skip_marker.kwargs.get('reason', msg)
179 pytest.skip(msg)
182 pytest.skip(msg)
180
183
181
184
182 def extract_git_repo_from_dump(dump_name, repo_name):
185 def extract_git_repo_from_dump(dump_name, repo_name):
183 """Create git repo `repo_name` from dump `dump_name`."""
186 """Create git repo `repo_name` from dump `dump_name`."""
184 repos_path = ScmModel().repos_path
187 repos_path = ScmModel().repos_path
185 target_path = os.path.join(repos_path, repo_name)
188 target_path = os.path.join(repos_path, repo_name)
186 rc_testdata.extract_git_dump(dump_name, target_path)
189 rc_testdata.extract_git_dump(dump_name, target_path)
187 return target_path
190 return target_path
188
191
189
192
190 def extract_hg_repo_from_dump(dump_name, repo_name):
193 def extract_hg_repo_from_dump(dump_name, repo_name):
191 """Create hg repo `repo_name` from dump `dump_name`."""
194 """Create hg repo `repo_name` from dump `dump_name`."""
192 repos_path = ScmModel().repos_path
195 repos_path = ScmModel().repos_path
193 target_path = os.path.join(repos_path, repo_name)
196 target_path = os.path.join(repos_path, repo_name)
194 rc_testdata.extract_hg_dump(dump_name, target_path)
197 rc_testdata.extract_hg_dump(dump_name, target_path)
195 return target_path
198 return target_path
196
199
197
200
198 def extract_svn_repo_from_dump(dump_name, repo_name):
201 def extract_svn_repo_from_dump(dump_name, repo_name):
199 """Create a svn repo `repo_name` from dump `dump_name`."""
202 """Create a svn repo `repo_name` from dump `dump_name`."""
200 repos_path = ScmModel().repos_path
203 repos_path = ScmModel().repos_path
201 target_path = os.path.join(repos_path, repo_name)
204 target_path = os.path.join(repos_path, repo_name)
202 SubversionRepository(target_path, create=True)
205 SubversionRepository(target_path, create=True)
203 _load_svn_dump_into_repo(dump_name, target_path)
206 _load_svn_dump_into_repo(dump_name, target_path)
204 return target_path
207 return target_path
205
208
206
209
207 def assert_message_in_log(log_records, message, levelno, module):
210 def assert_message_in_log(log_records, message, levelno, module):
208 messages = [
211 messages = [
209 r.message for r in log_records
212 r.message for r in log_records
210 if r.module == module and r.levelno == levelno
213 if r.module == module and r.levelno == levelno
211 ]
214 ]
212 assert message in messages
215 assert message in messages
213
216
214
217
215 def _load_svn_dump_into_repo(dump_name, repo_path):
218 def _load_svn_dump_into_repo(dump_name, repo_path):
216 """
219 """
217 Utility to populate a svn repository with a named dump
220 Utility to populate a svn repository with a named dump
218
221
219 Currently the dumps are in rc_testdata. They might later on be
222 Currently the dumps are in rc_testdata. They might later on be
220 integrated with the main repository once they stabilize more.
223 integrated with the main repository once they stabilize more.
221 """
224 """
222 dump = rc_testdata.load_svn_dump(dump_name)
225 dump = rc_testdata.load_svn_dump(dump_name)
223 load_dump = subprocess32.Popen(
226 load_dump = subprocess32.Popen(
224 ['svnadmin', 'load', repo_path],
227 ['svnadmin', 'load', repo_path],
225 stdin=subprocess32.PIPE, stdout=subprocess32.PIPE,
228 stdin=subprocess32.PIPE, stdout=subprocess32.PIPE,
226 stderr=subprocess32.PIPE)
229 stderr=subprocess32.PIPE)
227 out, err = load_dump.communicate(dump)
230 out, err = load_dump.communicate(dump)
228 if load_dump.returncode != 0:
231 if load_dump.returncode != 0:
229 log.error("Output of load_dump command: %s", out)
232 log.error("Output of load_dump command: %s", out)
230 log.error("Error output of load_dump command: %s", err)
233 log.error("Error output of load_dump command: %s", err)
231 raise Exception(
234 raise Exception(
232 'Failed to load dump "%s" into repository at path "%s".'
235 'Failed to load dump "%s" into repository at path "%s".'
233 % (dump_name, repo_path))
236 % (dump_name, repo_path))
234
237
235
238
236 class AssertResponse(object):
239 class AssertResponse(object):
237 """
240 """
238 Utility that helps to assert things about a given HTML response.
241 Utility that helps to assert things about a given HTML response.
239 """
242 """
240
243
241 def __init__(self, response):
244 def __init__(self, response):
242 self.response = response
245 self.response = response
243
246
244 def get_imports(self):
247 def get_imports(self):
245 return fromstring, tostring, CSSSelector
248 return fromstring, tostring, CSSSelector
246
249
247 def one_element_exists(self, css_selector):
250 def one_element_exists(self, css_selector):
248 self.get_element(css_selector)
251 self.get_element(css_selector)
249
252
250 def no_element_exists(self, css_selector):
253 def no_element_exists(self, css_selector):
251 assert not self._get_elements(css_selector)
254 assert not self._get_elements(css_selector)
252
255
253 def element_equals_to(self, css_selector, expected_content):
256 def element_equals_to(self, css_selector, expected_content):
254 element = self.get_element(css_selector)
257 element = self.get_element(css_selector)
255 element_text = self._element_to_string(element)
258 element_text = self._element_to_string(element)
256 assert expected_content in element_text
259 assert expected_content in element_text
257
260
258 def element_contains(self, css_selector, expected_content):
261 def element_contains(self, css_selector, expected_content):
259 element = self.get_element(css_selector)
262 element = self.get_element(css_selector)
260 assert expected_content in element.text_content()
263 assert expected_content in element.text_content()
261
264
262 def element_value_contains(self, css_selector, expected_content):
265 def element_value_contains(self, css_selector, expected_content):
263 element = self.get_element(css_selector)
266 element = self.get_element(css_selector)
264 assert expected_content in element.value
267 assert expected_content in element.value
265
268
266 def contains_one_link(self, link_text, href):
269 def contains_one_link(self, link_text, href):
267 fromstring, tostring, CSSSelector = self.get_imports()
270 fromstring, tostring, CSSSelector = self.get_imports()
268 doc = fromstring(self.response.body)
271 doc = fromstring(self.response.body)
269 sel = CSSSelector('a[href]')
272 sel = CSSSelector('a[href]')
270 elements = [
273 elements = [
271 e for e in sel(doc) if e.text_content().strip() == link_text]
274 e for e in sel(doc) if e.text_content().strip() == link_text]
272 assert len(elements) == 1, "Did not find link or found multiple links"
275 assert len(elements) == 1, "Did not find link or found multiple links"
273 self._ensure_url_equal(elements[0].attrib.get('href'), href)
276 self._ensure_url_equal(elements[0].attrib.get('href'), href)
274
277
275 def contains_one_anchor(self, anchor_id):
278 def contains_one_anchor(self, anchor_id):
276 fromstring, tostring, CSSSelector = self.get_imports()
279 fromstring, tostring, CSSSelector = self.get_imports()
277 doc = fromstring(self.response.body)
280 doc = fromstring(self.response.body)
278 sel = CSSSelector('#' + anchor_id)
281 sel = CSSSelector('#' + anchor_id)
279 elements = sel(doc)
282 elements = sel(doc)
280 assert len(elements) == 1, 'cannot find 1 element {}'.format(anchor_id)
283 assert len(elements) == 1, 'cannot find 1 element {}'.format(anchor_id)
281
284
282 def _ensure_url_equal(self, found, expected):
285 def _ensure_url_equal(self, found, expected):
283 assert _Url(found) == _Url(expected)
286 assert _Url(found) == _Url(expected)
284
287
285 def get_element(self, css_selector):
288 def get_element(self, css_selector):
286 elements = self._get_elements(css_selector)
289 elements = self._get_elements(css_selector)
287 assert len(elements) == 1, 'cannot find 1 element {}'.format(css_selector)
290 assert len(elements) == 1, 'cannot find 1 element {}'.format(css_selector)
288 return elements[0]
291 return elements[0]
289
292
290 def get_elements(self, css_selector):
293 def get_elements(self, css_selector):
291 return self._get_elements(css_selector)
294 return self._get_elements(css_selector)
292
295
293 def _get_elements(self, css_selector):
296 def _get_elements(self, css_selector):
294 fromstring, tostring, CSSSelector = self.get_imports()
297 fromstring, tostring, CSSSelector = self.get_imports()
295 doc = fromstring(self.response.body)
298 doc = fromstring(self.response.body)
296 sel = CSSSelector(css_selector)
299 sel = CSSSelector(css_selector)
297 elements = sel(doc)
300 elements = sel(doc)
298 return elements
301 return elements
299
302
300 def _element_to_string(self, element):
303 def _element_to_string(self, element):
301 fromstring, tostring, CSSSelector = self.get_imports()
304 fromstring, tostring, CSSSelector = self.get_imports()
302 return tostring(element)
305 return tostring(element)
303
306
304
307
305 class _Url(object):
308 class _Url(object):
306 """
309 """
307 A url object that can be compared with other url orbjects
310 A url object that can be compared with other url orbjects
308 without regard to the vagaries of encoding, escaping, and ordering
311 without regard to the vagaries of encoding, escaping, and ordering
309 of parameters in query strings.
312 of parameters in query strings.
310
313
311 Inspired by
314 Inspired by
312 http://stackoverflow.com/questions/5371992/comparing-two-urls-in-python
315 http://stackoverflow.com/questions/5371992/comparing-two-urls-in-python
313 """
316 """
314
317
315 def __init__(self, url):
318 def __init__(self, url):
316 parts = urlparse(url)
319 parts = urlparse(url)
317 _query = frozenset(parse_qsl(parts.query))
320 _query = frozenset(parse_qsl(parts.query))
318 _path = unquote_plus(parts.path)
321 _path = unquote_plus(parts.path)
319 parts = parts._replace(query=_query, path=_path)
322 parts = parts._replace(query=_query, path=_path)
320 self.parts = parts
323 self.parts = parts
321
324
322 def __eq__(self, other):
325 def __eq__(self, other):
323 return self.parts == other.parts
326 return self.parts == other.parts
324
327
325 def __hash__(self):
328 def __hash__(self):
326 return hash(self.parts)
329 return hash(self.parts)
327
330
328
331
329 def run_test_concurrently(times, raise_catched_exc=True):
332 def run_test_concurrently(times, raise_catched_exc=True):
330 """
333 """
331 Add this decorator to small pieces of code that you want to test
334 Add this decorator to small pieces of code that you want to test
332 concurrently
335 concurrently
333
336
334 ex:
337 ex:
335
338
336 @test_concurrently(25)
339 @test_concurrently(25)
337 def my_test_function():
340 def my_test_function():
338 ...
341 ...
339 """
342 """
340 def test_concurrently_decorator(test_func):
343 def test_concurrently_decorator(test_func):
341 def wrapper(*args, **kwargs):
344 def wrapper(*args, **kwargs):
342 exceptions = []
345 exceptions = []
343
346
344 def call_test_func():
347 def call_test_func():
345 try:
348 try:
346 test_func(*args, **kwargs)
349 test_func(*args, **kwargs)
347 except Exception as e:
350 except Exception as e:
348 exceptions.append(e)
351 exceptions.append(e)
349 if raise_catched_exc:
352 if raise_catched_exc:
350 raise
353 raise
351 threads = []
354 threads = []
352 for i in range(times):
355 for i in range(times):
353 threads.append(threading.Thread(target=call_test_func))
356 threads.append(threading.Thread(target=call_test_func))
354 for t in threads:
357 for t in threads:
355 t.start()
358 t.start()
356 for t in threads:
359 for t in threads:
357 t.join()
360 t.join()
358 if exceptions:
361 if exceptions:
359 raise Exception(
362 raise Exception(
360 'test_concurrently intercepted %s exceptions: %s' % (
363 'test_concurrently intercepted %s exceptions: %s' % (
361 len(exceptions), exceptions))
364 len(exceptions), exceptions))
362 return wrapper
365 return wrapper
363 return test_concurrently_decorator
366 return test_concurrently_decorator
364
367
365
368
366 def wait_for_url(url, timeout=10):
369 def wait_for_url(url, timeout=10):
367 """
370 """
368 Wait until URL becomes reachable.
371 Wait until URL becomes reachable.
369
372
370 It polls the URL until the timeout is reached or it became reachable.
373 It polls the URL until the timeout is reached or it became reachable.
371 If will call to `py.test.fail` in case the URL is not reachable.
374 If will call to `py.test.fail` in case the URL is not reachable.
372 """
375 """
373 timeout = time.time() + timeout
376 timeout = time.time() + timeout
374 last = 0
377 last = 0
375 wait = 0.1
378 wait = 0.1
376
379
377 while timeout > last:
380 while timeout > last:
378 last = time.time()
381 last = time.time()
379 if is_url_reachable(url):
382 if is_url_reachable(url):
380 break
383 break
381 elif (last + wait) > time.time():
384 elif (last + wait) > time.time():
382 # Go to sleep because not enough time has passed since last check.
385 # Go to sleep because not enough time has passed since last check.
383 time.sleep(wait)
386 time.sleep(wait)
384 else:
387 else:
385 pytest.fail("Timeout while waiting for URL {}".format(url))
388 pytest.fail("Timeout while waiting for URL {}".format(url))
386
389
387
390
388 def is_url_reachable(url):
391 def is_url_reachable(url):
389 try:
392 try:
390 urllib2.urlopen(url)
393 urllib2.urlopen(url)
391 except urllib2.URLError:
394 except urllib2.URLError:
392 return False
395 return False
393 return True
396 return True
394
397
395
398
396 def repo_on_filesystem(repo_name):
399 def repo_on_filesystem(repo_name):
397 from rhodecode.lib import vcs
400 from rhodecode.lib import vcs
398 from rhodecode.tests import TESTS_TMP_PATH
401 from rhodecode.tests import TESTS_TMP_PATH
399 repo = vcs.get_vcs_instance(
402 repo = vcs.get_vcs_instance(
400 os.path.join(TESTS_TMP_PATH, repo_name), create=False)
403 os.path.join(TESTS_TMP_PATH, repo_name), create=False)
401 return repo is not None
404 return repo is not None
402
405
403
406
404 def commit_change(
407 def commit_change(
405 repo, filename, content, message, vcs_type, parent=None, newfile=False):
408 repo, filename, content, message, vcs_type, parent=None, newfile=False):
406 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
409 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
407
410
408 repo = Repository.get_by_repo_name(repo)
411 repo = Repository.get_by_repo_name(repo)
409 _commit = parent
412 _commit = parent
410 if not parent:
413 if not parent:
411 _commit = EmptyCommit(alias=vcs_type)
414 _commit = EmptyCommit(alias=vcs_type)
412
415
413 if newfile:
416 if newfile:
414 nodes = {
417 nodes = {
415 filename: {
418 filename: {
416 'content': content
419 'content': content
417 }
420 }
418 }
421 }
419 commit = ScmModel().create_nodes(
422 commit = ScmModel().create_nodes(
420 user=TEST_USER_ADMIN_LOGIN, repo=repo,
423 user=TEST_USER_ADMIN_LOGIN, repo=repo,
421 message=message,
424 message=message,
422 nodes=nodes,
425 nodes=nodes,
423 parent_commit=_commit,
426 parent_commit=_commit,
424 author=TEST_USER_ADMIN_LOGIN,
427 author=TEST_USER_ADMIN_LOGIN,
425 )
428 )
426 else:
429 else:
427 commit = ScmModel().commit_change(
430 commit = ScmModel().commit_change(
428 repo=repo.scm_instance(), repo_name=repo.repo_name,
431 repo=repo.scm_instance(), repo_name=repo.repo_name,
429 commit=parent, user=TEST_USER_ADMIN_LOGIN,
432 commit=parent, user=TEST_USER_ADMIN_LOGIN,
430 author=TEST_USER_ADMIN_LOGIN,
433 author=TEST_USER_ADMIN_LOGIN,
431 message=message,
434 message=message,
432 content=content,
435 content=content,
433 f_path=filename
436 f_path=filename
434 )
437 )
435 return commit
438 return commit
436
439
437
440
438 def permission_update_data_generator(csrf_token, default=None, grant=None, revoke=None):
441 def permission_update_data_generator(csrf_token, default=None, grant=None, revoke=None):
439 if not default:
442 if not default:
440 raise ValueError('Permission for default user must be given')
443 raise ValueError('Permission for default user must be given')
441 form_data = [(
444 form_data = [(
442 'csrf_token', csrf_token
445 'csrf_token', csrf_token
443 )]
446 )]
444 # add default
447 # add default
445 form_data.extend([
448 form_data.extend([
446 ('u_perm_1', default)
449 ('u_perm_1', default)
447 ])
450 ])
448
451
449 if grant:
452 if grant:
450 for cnt, (obj_id, perm, obj_name, obj_type) in enumerate(grant, 1):
453 for cnt, (obj_id, perm, obj_name, obj_type) in enumerate(grant, 1):
451 form_data.extend([
454 form_data.extend([
452 ('perm_new_member_perm_new{}'.format(cnt), perm),
455 ('perm_new_member_perm_new{}'.format(cnt), perm),
453 ('perm_new_member_id_new{}'.format(cnt), obj_id),
456 ('perm_new_member_id_new{}'.format(cnt), obj_id),
454 ('perm_new_member_name_new{}'.format(cnt), obj_name),
457 ('perm_new_member_name_new{}'.format(cnt), obj_name),
455 ('perm_new_member_type_new{}'.format(cnt), obj_type),
458 ('perm_new_member_type_new{}'.format(cnt), obj_type),
456
459
457 ])
460 ])
458 if revoke:
461 if revoke:
459 for obj_id, obj_type in revoke:
462 for obj_id, obj_type in revoke:
460 form_data.extend([
463 form_data.extend([
461 ('perm_del_member_id_{}'.format(obj_id), obj_id),
464 ('perm_del_member_id_{}'.format(obj_id), obj_id),
462 ('perm_del_member_type_{}'.format(obj_id), obj_type),
465 ('perm_del_member_type_{}'.format(obj_id), obj_type),
463 ])
466 ])
464 return form_data
467 return form_data
General Comments 0
You need to be logged in to leave comments. Login now