##// END OF EJS Templates
vcs: rename get_file_history to get_path_history as it better reflects what it does.
marcink -
r3275:5c68832b default
parent child Browse files
Show More
@@ -1,358 +1,358 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2018 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 HTTPNotFound, HTTPFound
24 from pyramid.httpexceptions import 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 import rhodecode.lib.helpers as h
30 import rhodecode.lib.helpers as h
31 from rhodecode.lib.auth import (
31 from rhodecode.lib.auth import (
32 LoginRequired, HasRepoPermissionAnyDecorator)
32 LoginRequired, HasRepoPermissionAnyDecorator)
33
33
34 from rhodecode.lib.ext_json import json
34 from rhodecode.lib.ext_json import json
35 from rhodecode.lib.graphmod import _colored, _dagwalker
35 from rhodecode.lib.graphmod import _colored, _dagwalker
36 from rhodecode.lib.helpers import RepoPage
36 from rhodecode.lib.helpers import RepoPage
37 from rhodecode.lib.utils2 import safe_int, safe_str, str2bool
37 from rhodecode.lib.utils2 import safe_int, safe_str, str2bool
38 from rhodecode.lib.vcs.exceptions import (
38 from rhodecode.lib.vcs.exceptions import (
39 RepositoryError, CommitDoesNotExistError,
39 RepositoryError, CommitDoesNotExistError,
40 CommitError, NodeDoesNotExistError, EmptyRepositoryError)
40 CommitError, NodeDoesNotExistError, EmptyRepositoryError)
41
41
42 log = logging.getLogger(__name__)
42 log = logging.getLogger(__name__)
43
43
44 DEFAULT_CHANGELOG_SIZE = 20
44 DEFAULT_CHANGELOG_SIZE = 20
45
45
46
46
47 class RepoChangelogView(RepoAppView):
47 class RepoChangelogView(RepoAppView):
48
48
49 def _get_commit_or_redirect(self, commit_id, redirect_after=True):
49 def _get_commit_or_redirect(self, commit_id, redirect_after=True):
50 """
50 """
51 This is a safe way to get commit. If an error occurs it redirects to
51 This is a safe way to get commit. If an error occurs it redirects to
52 tip with proper message
52 tip with proper message
53
53
54 :param commit_id: id of commit to fetch
54 :param commit_id: id of commit to fetch
55 :param redirect_after: toggle redirection
55 :param redirect_after: toggle redirection
56 """
56 """
57 _ = self.request.translate
57 _ = self.request.translate
58
58
59 try:
59 try:
60 return self.rhodecode_vcs_repo.get_commit(commit_id)
60 return self.rhodecode_vcs_repo.get_commit(commit_id)
61 except EmptyRepositoryError:
61 except EmptyRepositoryError:
62 if not redirect_after:
62 if not redirect_after:
63 return None
63 return None
64
64
65 h.flash(h.literal(
65 h.flash(h.literal(
66 _('There are no commits yet')), category='warning')
66 _('There are no commits yet')), category='warning')
67 raise HTTPFound(
67 raise HTTPFound(
68 h.route_path('repo_summary', repo_name=self.db_repo_name))
68 h.route_path('repo_summary', repo_name=self.db_repo_name))
69
69
70 except (CommitDoesNotExistError, LookupError):
70 except (CommitDoesNotExistError, LookupError):
71 msg = _('No such commit exists for this repository')
71 msg = _('No such commit exists for this repository')
72 h.flash(msg, category='error')
72 h.flash(msg, category='error')
73 raise HTTPNotFound()
73 raise HTTPNotFound()
74 except RepositoryError as e:
74 except RepositoryError as e:
75 h.flash(safe_str(h.escape(e)), category='error')
75 h.flash(safe_str(h.escape(e)), category='error')
76 raise HTTPNotFound()
76 raise HTTPNotFound()
77
77
78 def _graph(self, repo, commits, prev_data=None, next_data=None):
78 def _graph(self, repo, commits, prev_data=None, next_data=None):
79 """
79 """
80 Generates a DAG graph for repo
80 Generates a DAG graph for repo
81
81
82 :param repo: repo instance
82 :param repo: repo instance
83 :param commits: list of commits
83 :param commits: list of commits
84 """
84 """
85 if not commits:
85 if not commits:
86 return json.dumps([]), json.dumps([])
86 return json.dumps([]), json.dumps([])
87
87
88 def serialize(commit, parents=True):
88 def serialize(commit, parents=True):
89 data = dict(
89 data = dict(
90 raw_id=commit.raw_id,
90 raw_id=commit.raw_id,
91 idx=commit.idx,
91 idx=commit.idx,
92 branch=h.escape(commit.branch),
92 branch=h.escape(commit.branch),
93 )
93 )
94 if parents:
94 if parents:
95 data['parents'] = [
95 data['parents'] = [
96 serialize(x, parents=False) for x in commit.parents]
96 serialize(x, parents=False) for x in commit.parents]
97 return data
97 return data
98
98
99 prev_data = prev_data or []
99 prev_data = prev_data or []
100 next_data = next_data or []
100 next_data = next_data or []
101
101
102 current = [serialize(x) for x in commits]
102 current = [serialize(x) for x in commits]
103 commits = prev_data + current + next_data
103 commits = prev_data + current + next_data
104
104
105 dag = _dagwalker(repo, commits)
105 dag = _dagwalker(repo, commits)
106
106
107 data = [[commit_id, vtx, edges, branch]
107 data = [[commit_id, vtx, edges, branch]
108 for commit_id, vtx, edges, branch in _colored(dag)]
108 for commit_id, vtx, edges, branch in _colored(dag)]
109 return json.dumps(data), json.dumps(current)
109 return json.dumps(data), json.dumps(current)
110
110
111 def _check_if_valid_branch(self, branch_name, repo_name, f_path):
111 def _check_if_valid_branch(self, branch_name, repo_name, f_path):
112 if branch_name not in self.rhodecode_vcs_repo.branches_all:
112 if branch_name not in self.rhodecode_vcs_repo.branches_all:
113 h.flash('Branch {} is not found.'.format(h.escape(branch_name)),
113 h.flash('Branch {} is not found.'.format(h.escape(branch_name)),
114 category='warning')
114 category='warning')
115 redirect_url = h.route_path(
115 redirect_url = h.route_path(
116 'repo_changelog_file', repo_name=repo_name,
116 'repo_changelog_file', repo_name=repo_name,
117 commit_id=branch_name, f_path=f_path or '')
117 commit_id=branch_name, f_path=f_path or '')
118 raise HTTPFound(redirect_url)
118 raise HTTPFound(redirect_url)
119
119
120 def _load_changelog_data(
120 def _load_changelog_data(
121 self, c, collection, page, chunk_size, branch_name=None,
121 self, c, collection, page, chunk_size, branch_name=None,
122 dynamic=False, f_path=None, commit_id=None):
122 dynamic=False, f_path=None, commit_id=None):
123
123
124 def url_generator(**kw):
124 def url_generator(**kw):
125 query_params = {}
125 query_params = {}
126 query_params.update(kw)
126 query_params.update(kw)
127 if f_path:
127 if f_path:
128 # changelog for file
128 # changelog for file
129 return h.route_path(
129 return h.route_path(
130 'repo_changelog_file',
130 'repo_changelog_file',
131 repo_name=c.rhodecode_db_repo.repo_name,
131 repo_name=c.rhodecode_db_repo.repo_name,
132 commit_id=commit_id, f_path=f_path,
132 commit_id=commit_id, f_path=f_path,
133 _query=query_params)
133 _query=query_params)
134 else:
134 else:
135 return h.route_path(
135 return h.route_path(
136 'repo_changelog',
136 'repo_changelog',
137 repo_name=c.rhodecode_db_repo.repo_name, _query=query_params)
137 repo_name=c.rhodecode_db_repo.repo_name, _query=query_params)
138
138
139 c.total_cs = len(collection)
139 c.total_cs = len(collection)
140 c.showing_commits = min(chunk_size, c.total_cs)
140 c.showing_commits = min(chunk_size, c.total_cs)
141 c.pagination = RepoPage(collection, page=page, item_count=c.total_cs,
141 c.pagination = RepoPage(collection, page=page, item_count=c.total_cs,
142 items_per_page=chunk_size, branch=branch_name,
142 items_per_page=chunk_size, branch=branch_name,
143 url=url_generator)
143 url=url_generator)
144
144
145 c.next_page = c.pagination.next_page
145 c.next_page = c.pagination.next_page
146 c.prev_page = c.pagination.previous_page
146 c.prev_page = c.pagination.previous_page
147
147
148 if dynamic:
148 if dynamic:
149 if self.request.GET.get('chunk') != 'next':
149 if self.request.GET.get('chunk') != 'next':
150 c.next_page = None
150 c.next_page = None
151 if self.request.GET.get('chunk') != 'prev':
151 if self.request.GET.get('chunk') != 'prev':
152 c.prev_page = None
152 c.prev_page = None
153
153
154 page_commit_ids = [x.raw_id for x in c.pagination]
154 page_commit_ids = [x.raw_id for x in c.pagination]
155 c.comments = c.rhodecode_db_repo.get_comments(page_commit_ids)
155 c.comments = c.rhodecode_db_repo.get_comments(page_commit_ids)
156 c.statuses = c.rhodecode_db_repo.statuses(page_commit_ids)
156 c.statuses = c.rhodecode_db_repo.statuses(page_commit_ids)
157
157
158 def load_default_context(self):
158 def load_default_context(self):
159 c = self._get_local_tmpl_context(include_app_defaults=True)
159 c = self._get_local_tmpl_context(include_app_defaults=True)
160
160
161 c.rhodecode_repo = self.rhodecode_vcs_repo
161 c.rhodecode_repo = self.rhodecode_vcs_repo
162
162
163 return c
163 return c
164
164
165 def _get_preload_attrs(self):
165 def _get_preload_attrs(self):
166 pre_load = ['author', 'branch', 'date', 'message', 'parents',
166 pre_load = ['author', 'branch', 'date', 'message', 'parents',
167 'obsolete', 'phase', 'hidden']
167 'obsolete', 'phase', 'hidden']
168 return pre_load
168 return pre_load
169
169
170 @LoginRequired()
170 @LoginRequired()
171 @HasRepoPermissionAnyDecorator(
171 @HasRepoPermissionAnyDecorator(
172 'repository.read', 'repository.write', 'repository.admin')
172 'repository.read', 'repository.write', 'repository.admin')
173 @view_config(
173 @view_config(
174 route_name='repo_changelog', request_method='GET',
174 route_name='repo_changelog', request_method='GET',
175 renderer='rhodecode:templates/changelog/changelog.mako')
175 renderer='rhodecode:templates/changelog/changelog.mako')
176 @view_config(
176 @view_config(
177 route_name='repo_changelog_file', request_method='GET',
177 route_name='repo_changelog_file', request_method='GET',
178 renderer='rhodecode:templates/changelog/changelog.mako')
178 renderer='rhodecode:templates/changelog/changelog.mako')
179 def repo_changelog(self):
179 def repo_changelog(self):
180 c = self.load_default_context()
180 c = self.load_default_context()
181
181
182 commit_id = self.request.matchdict.get('commit_id')
182 commit_id = self.request.matchdict.get('commit_id')
183 f_path = self._get_f_path(self.request.matchdict)
183 f_path = self._get_f_path(self.request.matchdict)
184 show_hidden = str2bool(self.request.GET.get('evolve'))
184 show_hidden = str2bool(self.request.GET.get('evolve'))
185
185
186 chunk_size = 20
186 chunk_size = 20
187
187
188 c.branch_name = branch_name = self.request.GET.get('branch') or ''
188 c.branch_name = branch_name = self.request.GET.get('branch') or ''
189 c.book_name = book_name = self.request.GET.get('bookmark') or ''
189 c.book_name = book_name = self.request.GET.get('bookmark') or ''
190 c.f_path = f_path
190 c.f_path = f_path
191 c.commit_id = commit_id
191 c.commit_id = commit_id
192 c.show_hidden = show_hidden
192 c.show_hidden = show_hidden
193
193
194 hist_limit = safe_int(self.request.GET.get('limit')) or None
194 hist_limit = safe_int(self.request.GET.get('limit')) or None
195
195
196 p = safe_int(self.request.GET.get('page', 1), 1)
196 p = safe_int(self.request.GET.get('page', 1), 1)
197
197
198 c.selected_name = branch_name or book_name
198 c.selected_name = branch_name or book_name
199 if not commit_id and branch_name:
199 if not commit_id and branch_name:
200 self._check_if_valid_branch(branch_name, self.db_repo_name, f_path)
200 self._check_if_valid_branch(branch_name, self.db_repo_name, f_path)
201
201
202 c.changelog_for_path = f_path
202 c.changelog_for_path = f_path
203 pre_load = self._get_preload_attrs()
203 pre_load = self._get_preload_attrs()
204
204
205 partial_xhr = self.request.environ.get('HTTP_X_PARTIAL_XHR')
205 partial_xhr = self.request.environ.get('HTTP_X_PARTIAL_XHR')
206
206
207 try:
207 try:
208 if f_path:
208 if f_path:
209 log.debug('generating changelog for path %s', f_path)
209 log.debug('generating changelog for path %s', f_path)
210 # get the history for the file !
210 # get the history for the file !
211 base_commit = self.rhodecode_vcs_repo.get_commit(commit_id)
211 base_commit = self.rhodecode_vcs_repo.get_commit(commit_id)
212
212
213 try:
213 try:
214 collection = base_commit.get_file_history(
214 collection = base_commit.get_path_history(
215 f_path, limit=hist_limit, pre_load=pre_load)
215 f_path, limit=hist_limit, pre_load=pre_load)
216 if collection and partial_xhr:
216 if collection and partial_xhr:
217 # for ajax call we remove first one since we're looking
217 # for ajax call we remove first one since we're looking
218 # at it right now in the context of a file commit
218 # at it right now in the context of a file commit
219 collection.pop(0)
219 collection.pop(0)
220 except (NodeDoesNotExistError, CommitError):
220 except (NodeDoesNotExistError, CommitError):
221 # this node is not present at tip!
221 # this node is not present at tip!
222 try:
222 try:
223 commit = self._get_commit_or_redirect(commit_id)
223 commit = self._get_commit_or_redirect(commit_id)
224 collection = commit.get_file_history(f_path)
224 collection = commit.get_path_history(f_path)
225 except RepositoryError as e:
225 except RepositoryError as e:
226 h.flash(safe_str(e), category='warning')
226 h.flash(safe_str(e), category='warning')
227 redirect_url = h.route_path(
227 redirect_url = h.route_path(
228 'repo_changelog', repo_name=self.db_repo_name)
228 'repo_changelog', repo_name=self.db_repo_name)
229 raise HTTPFound(redirect_url)
229 raise HTTPFound(redirect_url)
230 collection = list(reversed(collection))
230 collection = list(reversed(collection))
231 else:
231 else:
232 collection = self.rhodecode_vcs_repo.get_commits(
232 collection = self.rhodecode_vcs_repo.get_commits(
233 branch_name=branch_name, show_hidden=show_hidden,
233 branch_name=branch_name, show_hidden=show_hidden,
234 pre_load=pre_load)
234 pre_load=pre_load)
235
235
236 self._load_changelog_data(
236 self._load_changelog_data(
237 c, collection, p, chunk_size, c.branch_name,
237 c, collection, p, chunk_size, c.branch_name,
238 f_path=f_path, commit_id=commit_id)
238 f_path=f_path, commit_id=commit_id)
239
239
240 except EmptyRepositoryError as e:
240 except EmptyRepositoryError as e:
241 h.flash(safe_str(h.escape(e)), category='warning')
241 h.flash(safe_str(h.escape(e)), category='warning')
242 raise HTTPFound(
242 raise HTTPFound(
243 h.route_path('repo_summary', repo_name=self.db_repo_name))
243 h.route_path('repo_summary', repo_name=self.db_repo_name))
244 except HTTPFound:
244 except HTTPFound:
245 raise
245 raise
246 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
246 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
247 log.exception(safe_str(e))
247 log.exception(safe_str(e))
248 h.flash(safe_str(h.escape(e)), category='error')
248 h.flash(safe_str(h.escape(e)), category='error')
249 raise HTTPFound(
249 raise HTTPFound(
250 h.route_path('repo_changelog', repo_name=self.db_repo_name))
250 h.route_path('repo_changelog', repo_name=self.db_repo_name))
251
251
252 if partial_xhr or self.request.environ.get('HTTP_X_PJAX'):
252 if partial_xhr or self.request.environ.get('HTTP_X_PJAX'):
253 # case when loading dynamic file history in file view
253 # case when loading dynamic file history in file view
254 # loading from ajax, we don't want the first result, it's popped
254 # loading from ajax, we don't want the first result, it's popped
255 # in the code above
255 # in the code above
256 html = render(
256 html = render(
257 'rhodecode:templates/changelog/changelog_file_history.mako',
257 'rhodecode:templates/changelog/changelog_file_history.mako',
258 self._get_template_context(c), self.request)
258 self._get_template_context(c), self.request)
259 return Response(html)
259 return Response(html)
260
260
261 commit_ids = []
261 commit_ids = []
262 if not f_path:
262 if not f_path:
263 # only load graph data when not in file history mode
263 # only load graph data when not in file history mode
264 commit_ids = c.pagination
264 commit_ids = c.pagination
265
265
266 c.graph_data, c.graph_commits = self._graph(
266 c.graph_data, c.graph_commits = self._graph(
267 self.rhodecode_vcs_repo, commit_ids)
267 self.rhodecode_vcs_repo, commit_ids)
268
268
269 return self._get_template_context(c)
269 return self._get_template_context(c)
270
270
271 @LoginRequired()
271 @LoginRequired()
272 @HasRepoPermissionAnyDecorator(
272 @HasRepoPermissionAnyDecorator(
273 'repository.read', 'repository.write', 'repository.admin')
273 'repository.read', 'repository.write', 'repository.admin')
274 @view_config(
274 @view_config(
275 route_name='repo_changelog_elements', request_method=('GET', 'POST'),
275 route_name='repo_changelog_elements', request_method=('GET', 'POST'),
276 renderer='rhodecode:templates/changelog/changelog_elements.mako',
276 renderer='rhodecode:templates/changelog/changelog_elements.mako',
277 xhr=True)
277 xhr=True)
278 @view_config(
278 @view_config(
279 route_name='repo_changelog_elements_file', request_method=('GET', 'POST'),
279 route_name='repo_changelog_elements_file', request_method=('GET', 'POST'),
280 renderer='rhodecode:templates/changelog/changelog_elements.mako',
280 renderer='rhodecode:templates/changelog/changelog_elements.mako',
281 xhr=True)
281 xhr=True)
282 def repo_changelog_elements(self):
282 def repo_changelog_elements(self):
283 c = self.load_default_context()
283 c = self.load_default_context()
284 commit_id = self.request.matchdict.get('commit_id')
284 commit_id = self.request.matchdict.get('commit_id')
285 f_path = self._get_f_path(self.request.matchdict)
285 f_path = self._get_f_path(self.request.matchdict)
286 show_hidden = str2bool(self.request.GET.get('evolve'))
286 show_hidden = str2bool(self.request.GET.get('evolve'))
287
287
288 chunk_size = 20
288 chunk_size = 20
289 hist_limit = safe_int(self.request.GET.get('limit')) or None
289 hist_limit = safe_int(self.request.GET.get('limit')) or None
290
290
291 def wrap_for_error(err):
291 def wrap_for_error(err):
292 html = '<tr>' \
292 html = '<tr>' \
293 '<td colspan="9" class="alert alert-error">ERROR: {}</td>' \
293 '<td colspan="9" class="alert alert-error">ERROR: {}</td>' \
294 '</tr>'.format(err)
294 '</tr>'.format(err)
295 return Response(html)
295 return Response(html)
296
296
297 c.branch_name = branch_name = self.request.GET.get('branch') or ''
297 c.branch_name = branch_name = self.request.GET.get('branch') or ''
298 c.book_name = book_name = self.request.GET.get('bookmark') or ''
298 c.book_name = book_name = self.request.GET.get('bookmark') or ''
299 c.f_path = f_path
299 c.f_path = f_path
300 c.commit_id = commit_id
300 c.commit_id = commit_id
301 c.show_hidden = show_hidden
301 c.show_hidden = show_hidden
302
302
303 c.selected_name = branch_name or book_name
303 c.selected_name = branch_name or book_name
304 if branch_name and branch_name not in self.rhodecode_vcs_repo.branches_all:
304 if branch_name and branch_name not in self.rhodecode_vcs_repo.branches_all:
305 return wrap_for_error(
305 return wrap_for_error(
306 safe_str('Branch: {} is not valid'.format(branch_name)))
306 safe_str('Branch: {} is not valid'.format(branch_name)))
307
307
308 pre_load = self._get_preload_attrs()
308 pre_load = self._get_preload_attrs()
309
309
310 if f_path:
310 if f_path:
311 try:
311 try:
312 base_commit = self.rhodecode_vcs_repo.get_commit(commit_id)
312 base_commit = self.rhodecode_vcs_repo.get_commit(commit_id)
313 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
313 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
314 log.exception(safe_str(e))
314 log.exception(safe_str(e))
315 raise HTTPFound(
315 raise HTTPFound(
316 h.route_path('repo_changelog', repo_name=self.db_repo_name))
316 h.route_path('repo_changelog', repo_name=self.db_repo_name))
317
317
318 collection = base_commit.get_file_history(
318 collection = base_commit.get_path_history(
319 f_path, limit=hist_limit, pre_load=pre_load)
319 f_path, limit=hist_limit, pre_load=pre_load)
320 collection = list(reversed(collection))
320 collection = list(reversed(collection))
321 else:
321 else:
322 collection = self.rhodecode_vcs_repo.get_commits(
322 collection = self.rhodecode_vcs_repo.get_commits(
323 branch_name=branch_name, show_hidden=show_hidden, pre_load=pre_load)
323 branch_name=branch_name, show_hidden=show_hidden, pre_load=pre_load)
324
324
325 p = safe_int(self.request.GET.get('page', 1), 1)
325 p = safe_int(self.request.GET.get('page', 1), 1)
326 try:
326 try:
327 self._load_changelog_data(
327 self._load_changelog_data(
328 c, collection, p, chunk_size, dynamic=True,
328 c, collection, p, chunk_size, dynamic=True,
329 f_path=f_path, commit_id=commit_id)
329 f_path=f_path, commit_id=commit_id)
330 except EmptyRepositoryError as e:
330 except EmptyRepositoryError as e:
331 return wrap_for_error(safe_str(e))
331 return wrap_for_error(safe_str(e))
332 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
332 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
333 log.exception('Failed to fetch commits')
333 log.exception('Failed to fetch commits')
334 return wrap_for_error(safe_str(e))
334 return wrap_for_error(safe_str(e))
335
335
336 prev_data = None
336 prev_data = None
337 next_data = None
337 next_data = None
338
338
339 try:
339 try:
340 prev_graph = json.loads(self.request.POST.get('graph') or '{}')
340 prev_graph = json.loads(self.request.POST.get('graph') or '{}')
341 except json.JSONDecodeError:
341 except json.JSONDecodeError:
342 prev_graph = {}
342 prev_graph = {}
343
343
344 if self.request.GET.get('chunk') == 'prev':
344 if self.request.GET.get('chunk') == 'prev':
345 next_data = prev_graph
345 next_data = prev_graph
346 elif self.request.GET.get('chunk') == 'next':
346 elif self.request.GET.get('chunk') == 'next':
347 prev_data = prev_graph
347 prev_data = prev_graph
348
348
349 commit_ids = []
349 commit_ids = []
350 if not f_path:
350 if not f_path:
351 # only load graph data when not in file history mode
351 # only load graph data when not in file history mode
352 commit_ids = c.pagination
352 commit_ids = c.pagination
353
353
354 c.graph_data, c.graph_commits = self._graph(
354 c.graph_data, c.graph_commits = self._graph(
355 self.rhodecode_vcs_repo, commit_ids,
355 self.rhodecode_vcs_repo, commit_ids,
356 prev_data=prev_data, next_data=next_data)
356 prev_data=prev_data, next_data=next_data)
357
357
358 return self._get_template_context(c)
358 return self._get_template_context(c)
@@ -1,1385 +1,1385 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2018 RhodeCode GmbH
3 # Copyright (C) 2011-2018 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 itertools
21 import itertools
22 import logging
22 import logging
23 import os
23 import os
24 import shutil
24 import shutil
25 import tempfile
25 import tempfile
26 import collections
26 import collections
27
27
28 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPFound
28 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPFound
29 from pyramid.view import view_config
29 from pyramid.view import view_config
30 from pyramid.renderers import render
30 from pyramid.renderers import render
31 from pyramid.response import Response
31 from pyramid.response import Response
32
32
33 import rhodecode
33 import rhodecode
34 from rhodecode.apps._base import RepoAppView
34 from rhodecode.apps._base import RepoAppView
35
35
36 from rhodecode.controllers.utils import parse_path_ref
36 from rhodecode.controllers.utils import parse_path_ref
37 from rhodecode.lib import diffs, helpers as h, rc_cache
37 from rhodecode.lib import diffs, helpers as h, rc_cache
38 from rhodecode.lib import audit_logger
38 from rhodecode.lib import audit_logger
39 from rhodecode.lib.exceptions import NonRelativePathError
39 from rhodecode.lib.exceptions import NonRelativePathError
40 from rhodecode.lib.codeblocks import (
40 from rhodecode.lib.codeblocks import (
41 filenode_as_lines_tokens, filenode_as_annotated_lines_tokens)
41 filenode_as_lines_tokens, filenode_as_annotated_lines_tokens)
42 from rhodecode.lib.utils2 import (
42 from rhodecode.lib.utils2 import (
43 convert_line_endings, detect_mode, safe_str, str2bool, safe_int)
43 convert_line_endings, detect_mode, safe_str, str2bool, safe_int)
44 from rhodecode.lib.auth import (
44 from rhodecode.lib.auth import (
45 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
45 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
46 from rhodecode.lib.vcs import path as vcspath
46 from rhodecode.lib.vcs import path as vcspath
47 from rhodecode.lib.vcs.backends.base import EmptyCommit
47 from rhodecode.lib.vcs.backends.base import EmptyCommit
48 from rhodecode.lib.vcs.conf import settings
48 from rhodecode.lib.vcs.conf import settings
49 from rhodecode.lib.vcs.nodes import FileNode
49 from rhodecode.lib.vcs.nodes import FileNode
50 from rhodecode.lib.vcs.exceptions import (
50 from rhodecode.lib.vcs.exceptions import (
51 RepositoryError, CommitDoesNotExistError, EmptyRepositoryError,
51 RepositoryError, CommitDoesNotExistError, EmptyRepositoryError,
52 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError,
52 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError,
53 NodeDoesNotExistError, CommitError, NodeError)
53 NodeDoesNotExistError, CommitError, NodeError)
54
54
55 from rhodecode.model.scm import ScmModel
55 from rhodecode.model.scm import ScmModel
56 from rhodecode.model.db import Repository
56 from rhodecode.model.db import Repository
57
57
58 log = logging.getLogger(__name__)
58 log = logging.getLogger(__name__)
59
59
60
60
61 class RepoFilesView(RepoAppView):
61 class RepoFilesView(RepoAppView):
62
62
63 @staticmethod
63 @staticmethod
64 def adjust_file_path_for_svn(f_path, repo):
64 def adjust_file_path_for_svn(f_path, repo):
65 """
65 """
66 Computes the relative path of `f_path`.
66 Computes the relative path of `f_path`.
67
67
68 This is mainly based on prefix matching of the recognized tags and
68 This is mainly based on prefix matching of the recognized tags and
69 branches in the underlying repository.
69 branches in the underlying repository.
70 """
70 """
71 tags_and_branches = itertools.chain(
71 tags_and_branches = itertools.chain(
72 repo.branches.iterkeys(),
72 repo.branches.iterkeys(),
73 repo.tags.iterkeys())
73 repo.tags.iterkeys())
74 tags_and_branches = sorted(tags_and_branches, key=len, reverse=True)
74 tags_and_branches = sorted(tags_and_branches, key=len, reverse=True)
75
75
76 for name in tags_and_branches:
76 for name in tags_and_branches:
77 if f_path.startswith('{}/'.format(name)):
77 if f_path.startswith('{}/'.format(name)):
78 f_path = vcspath.relpath(f_path, name)
78 f_path = vcspath.relpath(f_path, name)
79 break
79 break
80 return f_path
80 return f_path
81
81
82 def load_default_context(self):
82 def load_default_context(self):
83 c = self._get_local_tmpl_context(include_app_defaults=True)
83 c = self._get_local_tmpl_context(include_app_defaults=True)
84 c.rhodecode_repo = self.rhodecode_vcs_repo
84 c.rhodecode_repo = self.rhodecode_vcs_repo
85 return c
85 return c
86
86
87 def _ensure_not_locked(self):
87 def _ensure_not_locked(self):
88 _ = self.request.translate
88 _ = self.request.translate
89
89
90 repo = self.db_repo
90 repo = self.db_repo
91 if repo.enable_locking and repo.locked[0]:
91 if repo.enable_locking and repo.locked[0]:
92 h.flash(_('This repository has been locked by %s on %s')
92 h.flash(_('This repository has been locked by %s on %s')
93 % (h.person_by_id(repo.locked[0]),
93 % (h.person_by_id(repo.locked[0]),
94 h.format_date(h.time_to_datetime(repo.locked[1]))),
94 h.format_date(h.time_to_datetime(repo.locked[1]))),
95 'warning')
95 'warning')
96 files_url = h.route_path(
96 files_url = h.route_path(
97 'repo_files:default_path',
97 'repo_files:default_path',
98 repo_name=self.db_repo_name, commit_id='tip')
98 repo_name=self.db_repo_name, commit_id='tip')
99 raise HTTPFound(files_url)
99 raise HTTPFound(files_url)
100
100
101 def check_branch_permission(self, branch_name):
101 def check_branch_permission(self, branch_name):
102 _ = self.request.translate
102 _ = self.request.translate
103
103
104 rule, branch_perm = self._rhodecode_user.get_rule_and_branch_permission(
104 rule, branch_perm = self._rhodecode_user.get_rule_and_branch_permission(
105 self.db_repo_name, branch_name)
105 self.db_repo_name, branch_name)
106 if branch_perm and branch_perm not in ['branch.push', 'branch.push_force']:
106 if branch_perm and branch_perm not in ['branch.push', 'branch.push_force']:
107 h.flash(
107 h.flash(
108 _('Branch `{}` changes forbidden by rule {}.').format(branch_name, rule),
108 _('Branch `{}` changes forbidden by rule {}.').format(branch_name, rule),
109 'warning')
109 'warning')
110 files_url = h.route_path(
110 files_url = h.route_path(
111 'repo_files:default_path',
111 'repo_files:default_path',
112 repo_name=self.db_repo_name, commit_id='tip')
112 repo_name=self.db_repo_name, commit_id='tip')
113 raise HTTPFound(files_url)
113 raise HTTPFound(files_url)
114
114
115 def _get_commit_and_path(self):
115 def _get_commit_and_path(self):
116 default_commit_id = self.db_repo.landing_rev[1]
116 default_commit_id = self.db_repo.landing_rev[1]
117 default_f_path = '/'
117 default_f_path = '/'
118
118
119 commit_id = self.request.matchdict.get(
119 commit_id = self.request.matchdict.get(
120 'commit_id', default_commit_id)
120 'commit_id', default_commit_id)
121 f_path = self._get_f_path(self.request.matchdict, default_f_path)
121 f_path = self._get_f_path(self.request.matchdict, default_f_path)
122 return commit_id, f_path
122 return commit_id, f_path
123
123
124 def _get_default_encoding(self, c):
124 def _get_default_encoding(self, c):
125 enc_list = getattr(c, 'default_encodings', [])
125 enc_list = getattr(c, 'default_encodings', [])
126 return enc_list[0] if enc_list else 'UTF-8'
126 return enc_list[0] if enc_list else 'UTF-8'
127
127
128 def _get_commit_or_redirect(self, commit_id, redirect_after=True):
128 def _get_commit_or_redirect(self, commit_id, redirect_after=True):
129 """
129 """
130 This is a safe way to get commit. If an error occurs it redirects to
130 This is a safe way to get commit. If an error occurs it redirects to
131 tip with proper message
131 tip with proper message
132
132
133 :param commit_id: id of commit to fetch
133 :param commit_id: id of commit to fetch
134 :param redirect_after: toggle redirection
134 :param redirect_after: toggle redirection
135 """
135 """
136 _ = self.request.translate
136 _ = self.request.translate
137
137
138 try:
138 try:
139 return self.rhodecode_vcs_repo.get_commit(commit_id)
139 return self.rhodecode_vcs_repo.get_commit(commit_id)
140 except EmptyRepositoryError:
140 except EmptyRepositoryError:
141 if not redirect_after:
141 if not redirect_after:
142 return None
142 return None
143
143
144 _url = h.route_path(
144 _url = h.route_path(
145 'repo_files_add_file',
145 'repo_files_add_file',
146 repo_name=self.db_repo_name, commit_id=0, f_path='',
146 repo_name=self.db_repo_name, commit_id=0, f_path='',
147 _anchor='edit')
147 _anchor='edit')
148
148
149 if h.HasRepoPermissionAny(
149 if h.HasRepoPermissionAny(
150 'repository.write', 'repository.admin')(self.db_repo_name):
150 'repository.write', 'repository.admin')(self.db_repo_name):
151 add_new = h.link_to(
151 add_new = h.link_to(
152 _('Click here to add a new file.'), _url, class_="alert-link")
152 _('Click here to add a new file.'), _url, class_="alert-link")
153 else:
153 else:
154 add_new = ""
154 add_new = ""
155
155
156 h.flash(h.literal(
156 h.flash(h.literal(
157 _('There are no files yet. %s') % add_new), category='warning')
157 _('There are no files yet. %s') % add_new), category='warning')
158 raise HTTPFound(
158 raise HTTPFound(
159 h.route_path('repo_summary', repo_name=self.db_repo_name))
159 h.route_path('repo_summary', repo_name=self.db_repo_name))
160
160
161 except (CommitDoesNotExistError, LookupError):
161 except (CommitDoesNotExistError, LookupError):
162 msg = _('No such commit exists for this repository')
162 msg = _('No such commit exists for this repository')
163 h.flash(msg, category='error')
163 h.flash(msg, category='error')
164 raise HTTPNotFound()
164 raise HTTPNotFound()
165 except RepositoryError as e:
165 except RepositoryError as e:
166 h.flash(safe_str(h.escape(e)), category='error')
166 h.flash(safe_str(h.escape(e)), category='error')
167 raise HTTPNotFound()
167 raise HTTPNotFound()
168
168
169 def _get_filenode_or_redirect(self, commit_obj, path):
169 def _get_filenode_or_redirect(self, commit_obj, path):
170 """
170 """
171 Returns file_node, if error occurs or given path is directory,
171 Returns file_node, if error occurs or given path is directory,
172 it'll redirect to top level path
172 it'll redirect to top level path
173 """
173 """
174 _ = self.request.translate
174 _ = self.request.translate
175
175
176 try:
176 try:
177 file_node = commit_obj.get_node(path)
177 file_node = commit_obj.get_node(path)
178 if file_node.is_dir():
178 if file_node.is_dir():
179 raise RepositoryError('The given path is a directory')
179 raise RepositoryError('The given path is a directory')
180 except CommitDoesNotExistError:
180 except CommitDoesNotExistError:
181 log.exception('No such commit exists for this repository')
181 log.exception('No such commit exists for this repository')
182 h.flash(_('No such commit exists for this repository'), category='error')
182 h.flash(_('No such commit exists for this repository'), category='error')
183 raise HTTPNotFound()
183 raise HTTPNotFound()
184 except RepositoryError as e:
184 except RepositoryError as e:
185 log.warning('Repository error while fetching '
185 log.warning('Repository error while fetching '
186 'filenode `%s`. Err:%s', path, e)
186 'filenode `%s`. Err:%s', path, e)
187 h.flash(safe_str(h.escape(e)), category='error')
187 h.flash(safe_str(h.escape(e)), category='error')
188 raise HTTPNotFound()
188 raise HTTPNotFound()
189
189
190 return file_node
190 return file_node
191
191
192 def _is_valid_head(self, commit_id, repo):
192 def _is_valid_head(self, commit_id, repo):
193 branch_name = sha_commit_id = ''
193 branch_name = sha_commit_id = ''
194 is_head = False
194 is_head = False
195
195
196 if h.is_svn(repo) and not repo.is_empty():
196 if h.is_svn(repo) and not repo.is_empty():
197 # Note: Subversion only has one head.
197 # Note: Subversion only has one head.
198 if commit_id == repo.get_commit(commit_idx=-1).raw_id:
198 if commit_id == repo.get_commit(commit_idx=-1).raw_id:
199 is_head = True
199 is_head = True
200 return branch_name, sha_commit_id, is_head
200 return branch_name, sha_commit_id, is_head
201
201
202 for _branch_name, branch_commit_id in repo.branches.items():
202 for _branch_name, branch_commit_id in repo.branches.items():
203 # simple case we pass in branch name, it's a HEAD
203 # simple case we pass in branch name, it's a HEAD
204 if commit_id == _branch_name:
204 if commit_id == _branch_name:
205 is_head = True
205 is_head = True
206 branch_name = _branch_name
206 branch_name = _branch_name
207 sha_commit_id = branch_commit_id
207 sha_commit_id = branch_commit_id
208 break
208 break
209 # case when we pass in full sha commit_id, which is a head
209 # case when we pass in full sha commit_id, which is a head
210 elif commit_id == branch_commit_id:
210 elif commit_id == branch_commit_id:
211 is_head = True
211 is_head = True
212 branch_name = _branch_name
212 branch_name = _branch_name
213 sha_commit_id = branch_commit_id
213 sha_commit_id = branch_commit_id
214 break
214 break
215
215
216 # checked branches, means we only need to try to get the branch/commit_sha
216 # checked branches, means we only need to try to get the branch/commit_sha
217 if not repo.is_empty:
217 if not repo.is_empty:
218 commit = repo.get_commit(commit_id=commit_id)
218 commit = repo.get_commit(commit_id=commit_id)
219 if commit:
219 if commit:
220 branch_name = commit.branch
220 branch_name = commit.branch
221 sha_commit_id = commit.raw_id
221 sha_commit_id = commit.raw_id
222
222
223 return branch_name, sha_commit_id, is_head
223 return branch_name, sha_commit_id, is_head
224
224
225 def _get_tree_at_commit(
225 def _get_tree_at_commit(
226 self, c, commit_id, f_path, full_load=False):
226 self, c, commit_id, f_path, full_load=False):
227
227
228 repo_id = self.db_repo.repo_id
228 repo_id = self.db_repo.repo_id
229
229
230 cache_seconds = safe_int(
230 cache_seconds = safe_int(
231 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
231 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
232 cache_on = cache_seconds > 0
232 cache_on = cache_seconds > 0
233 log.debug(
233 log.debug(
234 'Computing FILE TREE for repo_id %s commit_id `%s` and path `%s`'
234 'Computing FILE TREE for repo_id %s commit_id `%s` and path `%s`'
235 'with caching: %s[TTL: %ss]' % (
235 'with caching: %s[TTL: %ss]' % (
236 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
236 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
237
237
238 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
238 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
239 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
239 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
240
240
241 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
241 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
242 condition=cache_on)
242 condition=cache_on)
243 def compute_file_tree(repo_id, commit_id, f_path, full_load):
243 def compute_file_tree(repo_id, commit_id, f_path, full_load):
244 log.debug('Generating cached file tree for repo_id: %s, %s, %s',
244 log.debug('Generating cached file tree for repo_id: %s, %s, %s',
245 repo_id, commit_id, f_path)
245 repo_id, commit_id, f_path)
246
246
247 c.full_load = full_load
247 c.full_load = full_load
248 return render(
248 return render(
249 'rhodecode:templates/files/files_browser_tree.mako',
249 'rhodecode:templates/files/files_browser_tree.mako',
250 self._get_template_context(c), self.request)
250 self._get_template_context(c), self.request)
251
251
252 return compute_file_tree(self.db_repo.repo_id, commit_id, f_path, full_load)
252 return compute_file_tree(self.db_repo.repo_id, commit_id, f_path, full_load)
253
253
254 def _get_archive_spec(self, fname):
254 def _get_archive_spec(self, fname):
255 log.debug('Detecting archive spec for: `%s`', fname)
255 log.debug('Detecting archive spec for: `%s`', fname)
256
256
257 fileformat = None
257 fileformat = None
258 ext = None
258 ext = None
259 content_type = None
259 content_type = None
260 for a_type, ext_data in settings.ARCHIVE_SPECS.items():
260 for a_type, ext_data in settings.ARCHIVE_SPECS.items():
261 content_type, extension = ext_data
261 content_type, extension = ext_data
262
262
263 if fname.endswith(extension):
263 if fname.endswith(extension):
264 fileformat = a_type
264 fileformat = a_type
265 log.debug('archive is of type: %s', fileformat)
265 log.debug('archive is of type: %s', fileformat)
266 ext = extension
266 ext = extension
267 break
267 break
268
268
269 if not fileformat:
269 if not fileformat:
270 raise ValueError()
270 raise ValueError()
271
271
272 # left over part of whole fname is the commit
272 # left over part of whole fname is the commit
273 commit_id = fname[:-len(ext)]
273 commit_id = fname[:-len(ext)]
274
274
275 return commit_id, ext, fileformat, content_type
275 return commit_id, ext, fileformat, content_type
276
276
277 @LoginRequired()
277 @LoginRequired()
278 @HasRepoPermissionAnyDecorator(
278 @HasRepoPermissionAnyDecorator(
279 'repository.read', 'repository.write', 'repository.admin')
279 'repository.read', 'repository.write', 'repository.admin')
280 @view_config(
280 @view_config(
281 route_name='repo_archivefile', request_method='GET',
281 route_name='repo_archivefile', request_method='GET',
282 renderer=None)
282 renderer=None)
283 def repo_archivefile(self):
283 def repo_archivefile(self):
284 # archive cache config
284 # archive cache config
285 from rhodecode import CONFIG
285 from rhodecode import CONFIG
286 _ = self.request.translate
286 _ = self.request.translate
287 self.load_default_context()
287 self.load_default_context()
288
288
289 fname = self.request.matchdict['fname']
289 fname = self.request.matchdict['fname']
290 subrepos = self.request.GET.get('subrepos') == 'true'
290 subrepos = self.request.GET.get('subrepos') == 'true'
291
291
292 if not self.db_repo.enable_downloads:
292 if not self.db_repo.enable_downloads:
293 return Response(_('Downloads disabled'))
293 return Response(_('Downloads disabled'))
294
294
295 try:
295 try:
296 commit_id, ext, fileformat, content_type = \
296 commit_id, ext, fileformat, content_type = \
297 self._get_archive_spec(fname)
297 self._get_archive_spec(fname)
298 except ValueError:
298 except ValueError:
299 return Response(_('Unknown archive type for: `{}`').format(
299 return Response(_('Unknown archive type for: `{}`').format(
300 h.escape(fname)))
300 h.escape(fname)))
301
301
302 try:
302 try:
303 commit = self.rhodecode_vcs_repo.get_commit(commit_id)
303 commit = self.rhodecode_vcs_repo.get_commit(commit_id)
304 except CommitDoesNotExistError:
304 except CommitDoesNotExistError:
305 return Response(_('Unknown commit_id {}').format(
305 return Response(_('Unknown commit_id {}').format(
306 h.escape(commit_id)))
306 h.escape(commit_id)))
307 except EmptyRepositoryError:
307 except EmptyRepositoryError:
308 return Response(_('Empty repository'))
308 return Response(_('Empty repository'))
309
309
310 archive_name = '%s-%s%s%s' % (
310 archive_name = '%s-%s%s%s' % (
311 safe_str(self.db_repo_name.replace('/', '_')),
311 safe_str(self.db_repo_name.replace('/', '_')),
312 '-sub' if subrepos else '',
312 '-sub' if subrepos else '',
313 safe_str(commit.short_id), ext)
313 safe_str(commit.short_id), ext)
314
314
315 use_cached_archive = False
315 use_cached_archive = False
316 archive_cache_enabled = CONFIG.get(
316 archive_cache_enabled = CONFIG.get(
317 'archive_cache_dir') and not self.request.GET.get('no_cache')
317 'archive_cache_dir') and not self.request.GET.get('no_cache')
318 cached_archive_path = None
318 cached_archive_path = None
319
319
320 if archive_cache_enabled:
320 if archive_cache_enabled:
321 # check if we it's ok to write
321 # check if we it's ok to write
322 if not os.path.isdir(CONFIG['archive_cache_dir']):
322 if not os.path.isdir(CONFIG['archive_cache_dir']):
323 os.makedirs(CONFIG['archive_cache_dir'])
323 os.makedirs(CONFIG['archive_cache_dir'])
324 cached_archive_path = os.path.join(
324 cached_archive_path = os.path.join(
325 CONFIG['archive_cache_dir'], archive_name)
325 CONFIG['archive_cache_dir'], archive_name)
326 if os.path.isfile(cached_archive_path):
326 if os.path.isfile(cached_archive_path):
327 log.debug('Found cached archive in %s', cached_archive_path)
327 log.debug('Found cached archive in %s', cached_archive_path)
328 fd, archive = None, cached_archive_path
328 fd, archive = None, cached_archive_path
329 use_cached_archive = True
329 use_cached_archive = True
330 else:
330 else:
331 log.debug('Archive %s is not yet cached', archive_name)
331 log.debug('Archive %s is not yet cached', archive_name)
332
332
333 if not use_cached_archive:
333 if not use_cached_archive:
334 # generate new archive
334 # generate new archive
335 fd, archive = tempfile.mkstemp()
335 fd, archive = tempfile.mkstemp()
336 log.debug('Creating new temp archive in %s', archive)
336 log.debug('Creating new temp archive in %s', archive)
337 try:
337 try:
338 commit.archive_repo(archive, kind=fileformat, subrepos=subrepos)
338 commit.archive_repo(archive, kind=fileformat, subrepos=subrepos)
339 except ImproperArchiveTypeError:
339 except ImproperArchiveTypeError:
340 return _('Unknown archive type')
340 return _('Unknown archive type')
341 if archive_cache_enabled:
341 if archive_cache_enabled:
342 # if we generated the archive and we have cache enabled
342 # if we generated the archive and we have cache enabled
343 # let's use this for future
343 # let's use this for future
344 log.debug('Storing new archive in %s', cached_archive_path)
344 log.debug('Storing new archive in %s', cached_archive_path)
345 shutil.move(archive, cached_archive_path)
345 shutil.move(archive, cached_archive_path)
346 archive = cached_archive_path
346 archive = cached_archive_path
347
347
348 # store download action
348 # store download action
349 audit_logger.store_web(
349 audit_logger.store_web(
350 'repo.archive.download', action_data={
350 'repo.archive.download', action_data={
351 'user_agent': self.request.user_agent,
351 'user_agent': self.request.user_agent,
352 'archive_name': archive_name,
352 'archive_name': archive_name,
353 'archive_spec': fname,
353 'archive_spec': fname,
354 'archive_cached': use_cached_archive},
354 'archive_cached': use_cached_archive},
355 user=self._rhodecode_user,
355 user=self._rhodecode_user,
356 repo=self.db_repo,
356 repo=self.db_repo,
357 commit=True
357 commit=True
358 )
358 )
359
359
360 def get_chunked_archive(archive_path):
360 def get_chunked_archive(archive_path):
361 with open(archive_path, 'rb') as stream:
361 with open(archive_path, 'rb') as stream:
362 while True:
362 while True:
363 data = stream.read(16 * 1024)
363 data = stream.read(16 * 1024)
364 if not data:
364 if not data:
365 if fd: # fd means we used temporary file
365 if fd: # fd means we used temporary file
366 os.close(fd)
366 os.close(fd)
367 if not archive_cache_enabled:
367 if not archive_cache_enabled:
368 log.debug('Destroying temp archive %s', archive_path)
368 log.debug('Destroying temp archive %s', archive_path)
369 os.remove(archive_path)
369 os.remove(archive_path)
370 break
370 break
371 yield data
371 yield data
372
372
373 response = Response(app_iter=get_chunked_archive(archive))
373 response = Response(app_iter=get_chunked_archive(archive))
374 response.content_disposition = str(
374 response.content_disposition = str(
375 'attachment; filename=%s' % archive_name)
375 'attachment; filename=%s' % archive_name)
376 response.content_type = str(content_type)
376 response.content_type = str(content_type)
377
377
378 return response
378 return response
379
379
380 def _get_file_node(self, commit_id, f_path):
380 def _get_file_node(self, commit_id, f_path):
381 if commit_id not in ['', None, 'None', '0' * 12, '0' * 40]:
381 if commit_id not in ['', None, 'None', '0' * 12, '0' * 40]:
382 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
382 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
383 try:
383 try:
384 node = commit.get_node(f_path)
384 node = commit.get_node(f_path)
385 if node.is_dir():
385 if node.is_dir():
386 raise NodeError('%s path is a %s not a file'
386 raise NodeError('%s path is a %s not a file'
387 % (node, type(node)))
387 % (node, type(node)))
388 except NodeDoesNotExistError:
388 except NodeDoesNotExistError:
389 commit = EmptyCommit(
389 commit = EmptyCommit(
390 commit_id=commit_id,
390 commit_id=commit_id,
391 idx=commit.idx,
391 idx=commit.idx,
392 repo=commit.repository,
392 repo=commit.repository,
393 alias=commit.repository.alias,
393 alias=commit.repository.alias,
394 message=commit.message,
394 message=commit.message,
395 author=commit.author,
395 author=commit.author,
396 date=commit.date)
396 date=commit.date)
397 node = FileNode(f_path, '', commit=commit)
397 node = FileNode(f_path, '', commit=commit)
398 else:
398 else:
399 commit = EmptyCommit(
399 commit = EmptyCommit(
400 repo=self.rhodecode_vcs_repo,
400 repo=self.rhodecode_vcs_repo,
401 alias=self.rhodecode_vcs_repo.alias)
401 alias=self.rhodecode_vcs_repo.alias)
402 node = FileNode(f_path, '', commit=commit)
402 node = FileNode(f_path, '', commit=commit)
403 return node
403 return node
404
404
405 @LoginRequired()
405 @LoginRequired()
406 @HasRepoPermissionAnyDecorator(
406 @HasRepoPermissionAnyDecorator(
407 'repository.read', 'repository.write', 'repository.admin')
407 'repository.read', 'repository.write', 'repository.admin')
408 @view_config(
408 @view_config(
409 route_name='repo_files_diff', request_method='GET',
409 route_name='repo_files_diff', request_method='GET',
410 renderer=None)
410 renderer=None)
411 def repo_files_diff(self):
411 def repo_files_diff(self):
412 c = self.load_default_context()
412 c = self.load_default_context()
413 f_path = self._get_f_path(self.request.matchdict)
413 f_path = self._get_f_path(self.request.matchdict)
414 diff1 = self.request.GET.get('diff1', '')
414 diff1 = self.request.GET.get('diff1', '')
415 diff2 = self.request.GET.get('diff2', '')
415 diff2 = self.request.GET.get('diff2', '')
416
416
417 path1, diff1 = parse_path_ref(diff1, default_path=f_path)
417 path1, diff1 = parse_path_ref(diff1, default_path=f_path)
418
418
419 ignore_whitespace = str2bool(self.request.GET.get('ignorews'))
419 ignore_whitespace = str2bool(self.request.GET.get('ignorews'))
420 line_context = self.request.GET.get('context', 3)
420 line_context = self.request.GET.get('context', 3)
421
421
422 if not any((diff1, diff2)):
422 if not any((diff1, diff2)):
423 h.flash(
423 h.flash(
424 'Need query parameter "diff1" or "diff2" to generate a diff.',
424 'Need query parameter "diff1" or "diff2" to generate a diff.',
425 category='error')
425 category='error')
426 raise HTTPBadRequest()
426 raise HTTPBadRequest()
427
427
428 c.action = self.request.GET.get('diff')
428 c.action = self.request.GET.get('diff')
429 if c.action not in ['download', 'raw']:
429 if c.action not in ['download', 'raw']:
430 compare_url = h.route_path(
430 compare_url = h.route_path(
431 'repo_compare',
431 'repo_compare',
432 repo_name=self.db_repo_name,
432 repo_name=self.db_repo_name,
433 source_ref_type='rev',
433 source_ref_type='rev',
434 source_ref=diff1,
434 source_ref=diff1,
435 target_repo=self.db_repo_name,
435 target_repo=self.db_repo_name,
436 target_ref_type='rev',
436 target_ref_type='rev',
437 target_ref=diff2,
437 target_ref=diff2,
438 _query=dict(f_path=f_path))
438 _query=dict(f_path=f_path))
439 # redirect to new view if we render diff
439 # redirect to new view if we render diff
440 raise HTTPFound(compare_url)
440 raise HTTPFound(compare_url)
441
441
442 try:
442 try:
443 node1 = self._get_file_node(diff1, path1)
443 node1 = self._get_file_node(diff1, path1)
444 node2 = self._get_file_node(diff2, f_path)
444 node2 = self._get_file_node(diff2, f_path)
445 except (RepositoryError, NodeError):
445 except (RepositoryError, NodeError):
446 log.exception("Exception while trying to get node from repository")
446 log.exception("Exception while trying to get node from repository")
447 raise HTTPFound(
447 raise HTTPFound(
448 h.route_path('repo_files', repo_name=self.db_repo_name,
448 h.route_path('repo_files', repo_name=self.db_repo_name,
449 commit_id='tip', f_path=f_path))
449 commit_id='tip', f_path=f_path))
450
450
451 if all(isinstance(node.commit, EmptyCommit)
451 if all(isinstance(node.commit, EmptyCommit)
452 for node in (node1, node2)):
452 for node in (node1, node2)):
453 raise HTTPNotFound()
453 raise HTTPNotFound()
454
454
455 c.commit_1 = node1.commit
455 c.commit_1 = node1.commit
456 c.commit_2 = node2.commit
456 c.commit_2 = node2.commit
457
457
458 if c.action == 'download':
458 if c.action == 'download':
459 _diff = diffs.get_gitdiff(node1, node2,
459 _diff = diffs.get_gitdiff(node1, node2,
460 ignore_whitespace=ignore_whitespace,
460 ignore_whitespace=ignore_whitespace,
461 context=line_context)
461 context=line_context)
462 diff = diffs.DiffProcessor(_diff, format='gitdiff')
462 diff = diffs.DiffProcessor(_diff, format='gitdiff')
463
463
464 response = Response(self.path_filter.get_raw_patch(diff))
464 response = Response(self.path_filter.get_raw_patch(diff))
465 response.content_type = 'text/plain'
465 response.content_type = 'text/plain'
466 response.content_disposition = (
466 response.content_disposition = (
467 'attachment; filename=%s_%s_vs_%s.diff' % (f_path, diff1, diff2)
467 'attachment; filename=%s_%s_vs_%s.diff' % (f_path, diff1, diff2)
468 )
468 )
469 charset = self._get_default_encoding(c)
469 charset = self._get_default_encoding(c)
470 if charset:
470 if charset:
471 response.charset = charset
471 response.charset = charset
472 return response
472 return response
473
473
474 elif c.action == 'raw':
474 elif c.action == 'raw':
475 _diff = diffs.get_gitdiff(node1, node2,
475 _diff = diffs.get_gitdiff(node1, node2,
476 ignore_whitespace=ignore_whitespace,
476 ignore_whitespace=ignore_whitespace,
477 context=line_context)
477 context=line_context)
478 diff = diffs.DiffProcessor(_diff, format='gitdiff')
478 diff = diffs.DiffProcessor(_diff, format='gitdiff')
479
479
480 response = Response(self.path_filter.get_raw_patch(diff))
480 response = Response(self.path_filter.get_raw_patch(diff))
481 response.content_type = 'text/plain'
481 response.content_type = 'text/plain'
482 charset = self._get_default_encoding(c)
482 charset = self._get_default_encoding(c)
483 if charset:
483 if charset:
484 response.charset = charset
484 response.charset = charset
485 return response
485 return response
486
486
487 # in case we ever end up here
487 # in case we ever end up here
488 raise HTTPNotFound()
488 raise HTTPNotFound()
489
489
490 @LoginRequired()
490 @LoginRequired()
491 @HasRepoPermissionAnyDecorator(
491 @HasRepoPermissionAnyDecorator(
492 'repository.read', 'repository.write', 'repository.admin')
492 'repository.read', 'repository.write', 'repository.admin')
493 @view_config(
493 @view_config(
494 route_name='repo_files_diff_2way_redirect', request_method='GET',
494 route_name='repo_files_diff_2way_redirect', request_method='GET',
495 renderer=None)
495 renderer=None)
496 def repo_files_diff_2way_redirect(self):
496 def repo_files_diff_2way_redirect(self):
497 """
497 """
498 Kept only to make OLD links work
498 Kept only to make OLD links work
499 """
499 """
500 f_path = self._get_f_path_unchecked(self.request.matchdict)
500 f_path = self._get_f_path_unchecked(self.request.matchdict)
501 diff1 = self.request.GET.get('diff1', '')
501 diff1 = self.request.GET.get('diff1', '')
502 diff2 = self.request.GET.get('diff2', '')
502 diff2 = self.request.GET.get('diff2', '')
503
503
504 if not any((diff1, diff2)):
504 if not any((diff1, diff2)):
505 h.flash(
505 h.flash(
506 'Need query parameter "diff1" or "diff2" to generate a diff.',
506 'Need query parameter "diff1" or "diff2" to generate a diff.',
507 category='error')
507 category='error')
508 raise HTTPBadRequest()
508 raise HTTPBadRequest()
509
509
510 compare_url = h.route_path(
510 compare_url = h.route_path(
511 'repo_compare',
511 'repo_compare',
512 repo_name=self.db_repo_name,
512 repo_name=self.db_repo_name,
513 source_ref_type='rev',
513 source_ref_type='rev',
514 source_ref=diff1,
514 source_ref=diff1,
515 target_ref_type='rev',
515 target_ref_type='rev',
516 target_ref=diff2,
516 target_ref=diff2,
517 _query=dict(f_path=f_path, diffmode='sideside',
517 _query=dict(f_path=f_path, diffmode='sideside',
518 target_repo=self.db_repo_name,))
518 target_repo=self.db_repo_name,))
519 raise HTTPFound(compare_url)
519 raise HTTPFound(compare_url)
520
520
521 @LoginRequired()
521 @LoginRequired()
522 @HasRepoPermissionAnyDecorator(
522 @HasRepoPermissionAnyDecorator(
523 'repository.read', 'repository.write', 'repository.admin')
523 'repository.read', 'repository.write', 'repository.admin')
524 @view_config(
524 @view_config(
525 route_name='repo_files', request_method='GET',
525 route_name='repo_files', request_method='GET',
526 renderer=None)
526 renderer=None)
527 @view_config(
527 @view_config(
528 route_name='repo_files:default_path', request_method='GET',
528 route_name='repo_files:default_path', request_method='GET',
529 renderer=None)
529 renderer=None)
530 @view_config(
530 @view_config(
531 route_name='repo_files:default_commit', request_method='GET',
531 route_name='repo_files:default_commit', request_method='GET',
532 renderer=None)
532 renderer=None)
533 @view_config(
533 @view_config(
534 route_name='repo_files:rendered', request_method='GET',
534 route_name='repo_files:rendered', request_method='GET',
535 renderer=None)
535 renderer=None)
536 @view_config(
536 @view_config(
537 route_name='repo_files:annotated', request_method='GET',
537 route_name='repo_files:annotated', request_method='GET',
538 renderer=None)
538 renderer=None)
539 def repo_files(self):
539 def repo_files(self):
540 c = self.load_default_context()
540 c = self.load_default_context()
541
541
542 view_name = getattr(self.request.matched_route, 'name', None)
542 view_name = getattr(self.request.matched_route, 'name', None)
543
543
544 c.annotate = view_name == 'repo_files:annotated'
544 c.annotate = view_name == 'repo_files:annotated'
545 # default is false, but .rst/.md files later are auto rendered, we can
545 # default is false, but .rst/.md files later are auto rendered, we can
546 # overwrite auto rendering by setting this GET flag
546 # overwrite auto rendering by setting this GET flag
547 c.renderer = view_name == 'repo_files:rendered' or \
547 c.renderer = view_name == 'repo_files:rendered' or \
548 not self.request.GET.get('no-render', False)
548 not self.request.GET.get('no-render', False)
549
549
550 # redirect to given commit_id from form if given
550 # redirect to given commit_id from form if given
551 get_commit_id = self.request.GET.get('at_rev', None)
551 get_commit_id = self.request.GET.get('at_rev', None)
552 if get_commit_id:
552 if get_commit_id:
553 self._get_commit_or_redirect(get_commit_id)
553 self._get_commit_or_redirect(get_commit_id)
554
554
555 commit_id, f_path = self._get_commit_and_path()
555 commit_id, f_path = self._get_commit_and_path()
556 c.commit = self._get_commit_or_redirect(commit_id)
556 c.commit = self._get_commit_or_redirect(commit_id)
557 c.branch = self.request.GET.get('branch', None)
557 c.branch = self.request.GET.get('branch', None)
558 c.f_path = f_path
558 c.f_path = f_path
559
559
560 # prev link
560 # prev link
561 try:
561 try:
562 prev_commit = c.commit.prev(c.branch)
562 prev_commit = c.commit.prev(c.branch)
563 c.prev_commit = prev_commit
563 c.prev_commit = prev_commit
564 c.url_prev = h.route_path(
564 c.url_prev = h.route_path(
565 'repo_files', repo_name=self.db_repo_name,
565 'repo_files', repo_name=self.db_repo_name,
566 commit_id=prev_commit.raw_id, f_path=f_path)
566 commit_id=prev_commit.raw_id, f_path=f_path)
567 if c.branch:
567 if c.branch:
568 c.url_prev += '?branch=%s' % c.branch
568 c.url_prev += '?branch=%s' % c.branch
569 except (CommitDoesNotExistError, VCSError):
569 except (CommitDoesNotExistError, VCSError):
570 c.url_prev = '#'
570 c.url_prev = '#'
571 c.prev_commit = EmptyCommit()
571 c.prev_commit = EmptyCommit()
572
572
573 # next link
573 # next link
574 try:
574 try:
575 next_commit = c.commit.next(c.branch)
575 next_commit = c.commit.next(c.branch)
576 c.next_commit = next_commit
576 c.next_commit = next_commit
577 c.url_next = h.route_path(
577 c.url_next = h.route_path(
578 'repo_files', repo_name=self.db_repo_name,
578 'repo_files', repo_name=self.db_repo_name,
579 commit_id=next_commit.raw_id, f_path=f_path)
579 commit_id=next_commit.raw_id, f_path=f_path)
580 if c.branch:
580 if c.branch:
581 c.url_next += '?branch=%s' % c.branch
581 c.url_next += '?branch=%s' % c.branch
582 except (CommitDoesNotExistError, VCSError):
582 except (CommitDoesNotExistError, VCSError):
583 c.url_next = '#'
583 c.url_next = '#'
584 c.next_commit = EmptyCommit()
584 c.next_commit = EmptyCommit()
585
585
586 # files or dirs
586 # files or dirs
587 try:
587 try:
588 c.file = c.commit.get_node(f_path)
588 c.file = c.commit.get_node(f_path)
589 c.file_author = True
589 c.file_author = True
590 c.file_tree = ''
590 c.file_tree = ''
591
591
592 # load file content
592 # load file content
593 if c.file.is_file():
593 if c.file.is_file():
594 c.lf_node = c.file.get_largefile_node()
594 c.lf_node = c.file.get_largefile_node()
595
595
596 c.file_source_page = 'true'
596 c.file_source_page = 'true'
597 c.file_last_commit = c.file.last_commit
597 c.file_last_commit = c.file.last_commit
598 if c.file.size < c.visual.cut_off_limit_diff:
598 if c.file.size < c.visual.cut_off_limit_diff:
599 if c.annotate: # annotation has precedence over renderer
599 if c.annotate: # annotation has precedence over renderer
600 c.annotated_lines = filenode_as_annotated_lines_tokens(
600 c.annotated_lines = filenode_as_annotated_lines_tokens(
601 c.file
601 c.file
602 )
602 )
603 else:
603 else:
604 c.renderer = (
604 c.renderer = (
605 c.renderer and h.renderer_from_filename(c.file.path)
605 c.renderer and h.renderer_from_filename(c.file.path)
606 )
606 )
607 if not c.renderer:
607 if not c.renderer:
608 c.lines = filenode_as_lines_tokens(c.file)
608 c.lines = filenode_as_lines_tokens(c.file)
609
609
610 _branch_name, _sha_commit_id, is_head = self._is_valid_head(
610 _branch_name, _sha_commit_id, is_head = self._is_valid_head(
611 commit_id, self.rhodecode_vcs_repo)
611 commit_id, self.rhodecode_vcs_repo)
612 c.on_branch_head = is_head
612 c.on_branch_head = is_head
613
613
614 branch = c.commit.branch if (
614 branch = c.commit.branch if (
615 c.commit.branch and '/' not in c.commit.branch) else None
615 c.commit.branch and '/' not in c.commit.branch) else None
616 c.branch_or_raw_id = branch or c.commit.raw_id
616 c.branch_or_raw_id = branch or c.commit.raw_id
617 c.branch_name = c.commit.branch or h.short_id(c.commit.raw_id)
617 c.branch_name = c.commit.branch or h.short_id(c.commit.raw_id)
618
618
619 author = c.file_last_commit.author
619 author = c.file_last_commit.author
620 c.authors = [[
620 c.authors = [[
621 h.email(author),
621 h.email(author),
622 h.person(author, 'username_or_name_or_email'),
622 h.person(author, 'username_or_name_or_email'),
623 1
623 1
624 ]]
624 ]]
625
625
626 else: # load tree content at path
626 else: # load tree content at path
627 c.file_source_page = 'false'
627 c.file_source_page = 'false'
628 c.authors = []
628 c.authors = []
629 # this loads a simple tree without metadata to speed things up
629 # this loads a simple tree without metadata to speed things up
630 # later via ajax we call repo_nodetree_full and fetch whole
630 # later via ajax we call repo_nodetree_full and fetch whole
631 c.file_tree = self._get_tree_at_commit(
631 c.file_tree = self._get_tree_at_commit(
632 c, c.commit.raw_id, f_path)
632 c, c.commit.raw_id, f_path)
633
633
634 except RepositoryError as e:
634 except RepositoryError as e:
635 h.flash(safe_str(h.escape(e)), category='error')
635 h.flash(safe_str(h.escape(e)), category='error')
636 raise HTTPNotFound()
636 raise HTTPNotFound()
637
637
638 if self.request.environ.get('HTTP_X_PJAX'):
638 if self.request.environ.get('HTTP_X_PJAX'):
639 html = render('rhodecode:templates/files/files_pjax.mako',
639 html = render('rhodecode:templates/files/files_pjax.mako',
640 self._get_template_context(c), self.request)
640 self._get_template_context(c), self.request)
641 else:
641 else:
642 html = render('rhodecode:templates/files/files.mako',
642 html = render('rhodecode:templates/files/files.mako',
643 self._get_template_context(c), self.request)
643 self._get_template_context(c), self.request)
644 return Response(html)
644 return Response(html)
645
645
646 @HasRepoPermissionAnyDecorator(
646 @HasRepoPermissionAnyDecorator(
647 'repository.read', 'repository.write', 'repository.admin')
647 'repository.read', 'repository.write', 'repository.admin')
648 @view_config(
648 @view_config(
649 route_name='repo_files:annotated_previous', request_method='GET',
649 route_name='repo_files:annotated_previous', request_method='GET',
650 renderer=None)
650 renderer=None)
651 def repo_files_annotated_previous(self):
651 def repo_files_annotated_previous(self):
652 self.load_default_context()
652 self.load_default_context()
653
653
654 commit_id, f_path = self._get_commit_and_path()
654 commit_id, f_path = self._get_commit_and_path()
655 commit = self._get_commit_or_redirect(commit_id)
655 commit = self._get_commit_or_redirect(commit_id)
656 prev_commit_id = commit.raw_id
656 prev_commit_id = commit.raw_id
657 line_anchor = self.request.GET.get('line_anchor')
657 line_anchor = self.request.GET.get('line_anchor')
658 is_file = False
658 is_file = False
659 try:
659 try:
660 _file = commit.get_node(f_path)
660 _file = commit.get_node(f_path)
661 is_file = _file.is_file()
661 is_file = _file.is_file()
662 except (NodeDoesNotExistError, CommitDoesNotExistError, VCSError):
662 except (NodeDoesNotExistError, CommitDoesNotExistError, VCSError):
663 pass
663 pass
664
664
665 if is_file:
665 if is_file:
666 history = commit.get_file_history(f_path)
666 history = commit.get_path_history(f_path)
667 prev_commit_id = history[1].raw_id \
667 prev_commit_id = history[1].raw_id \
668 if len(history) > 1 else prev_commit_id
668 if len(history) > 1 else prev_commit_id
669 prev_url = h.route_path(
669 prev_url = h.route_path(
670 'repo_files:annotated', repo_name=self.db_repo_name,
670 'repo_files:annotated', repo_name=self.db_repo_name,
671 commit_id=prev_commit_id, f_path=f_path,
671 commit_id=prev_commit_id, f_path=f_path,
672 _anchor='L{}'.format(line_anchor))
672 _anchor='L{}'.format(line_anchor))
673
673
674 raise HTTPFound(prev_url)
674 raise HTTPFound(prev_url)
675
675
676 @LoginRequired()
676 @LoginRequired()
677 @HasRepoPermissionAnyDecorator(
677 @HasRepoPermissionAnyDecorator(
678 'repository.read', 'repository.write', 'repository.admin')
678 'repository.read', 'repository.write', 'repository.admin')
679 @view_config(
679 @view_config(
680 route_name='repo_nodetree_full', request_method='GET',
680 route_name='repo_nodetree_full', request_method='GET',
681 renderer=None, xhr=True)
681 renderer=None, xhr=True)
682 @view_config(
682 @view_config(
683 route_name='repo_nodetree_full:default_path', request_method='GET',
683 route_name='repo_nodetree_full:default_path', request_method='GET',
684 renderer=None, xhr=True)
684 renderer=None, xhr=True)
685 def repo_nodetree_full(self):
685 def repo_nodetree_full(self):
686 """
686 """
687 Returns rendered html of file tree that contains commit date,
687 Returns rendered html of file tree that contains commit date,
688 author, commit_id for the specified combination of
688 author, commit_id for the specified combination of
689 repo, commit_id and file path
689 repo, commit_id and file path
690 """
690 """
691 c = self.load_default_context()
691 c = self.load_default_context()
692
692
693 commit_id, f_path = self._get_commit_and_path()
693 commit_id, f_path = self._get_commit_and_path()
694 commit = self._get_commit_or_redirect(commit_id)
694 commit = self._get_commit_or_redirect(commit_id)
695 try:
695 try:
696 dir_node = commit.get_node(f_path)
696 dir_node = commit.get_node(f_path)
697 except RepositoryError as e:
697 except RepositoryError as e:
698 return Response('error: {}'.format(h.escape(safe_str(e))))
698 return Response('error: {}'.format(h.escape(safe_str(e))))
699
699
700 if dir_node.is_file():
700 if dir_node.is_file():
701 return Response('')
701 return Response('')
702
702
703 c.file = dir_node
703 c.file = dir_node
704 c.commit = commit
704 c.commit = commit
705
705
706 html = self._get_tree_at_commit(
706 html = self._get_tree_at_commit(
707 c, commit.raw_id, dir_node.path, full_load=True)
707 c, commit.raw_id, dir_node.path, full_load=True)
708
708
709 return Response(html)
709 return Response(html)
710
710
711 def _get_attachement_disposition(self, f_path):
711 def _get_attachement_disposition(self, f_path):
712 return 'attachment; filename=%s' % \
712 return 'attachment; filename=%s' % \
713 safe_str(f_path.split(Repository.NAME_SEP)[-1])
713 safe_str(f_path.split(Repository.NAME_SEP)[-1])
714
714
715 @LoginRequired()
715 @LoginRequired()
716 @HasRepoPermissionAnyDecorator(
716 @HasRepoPermissionAnyDecorator(
717 'repository.read', 'repository.write', 'repository.admin')
717 'repository.read', 'repository.write', 'repository.admin')
718 @view_config(
718 @view_config(
719 route_name='repo_file_raw', request_method='GET',
719 route_name='repo_file_raw', request_method='GET',
720 renderer=None)
720 renderer=None)
721 def repo_file_raw(self):
721 def repo_file_raw(self):
722 """
722 """
723 Action for show as raw, some mimetypes are "rendered",
723 Action for show as raw, some mimetypes are "rendered",
724 those include images, icons.
724 those include images, icons.
725 """
725 """
726 c = self.load_default_context()
726 c = self.load_default_context()
727
727
728 commit_id, f_path = self._get_commit_and_path()
728 commit_id, f_path = self._get_commit_and_path()
729 commit = self._get_commit_or_redirect(commit_id)
729 commit = self._get_commit_or_redirect(commit_id)
730 file_node = self._get_filenode_or_redirect(commit, f_path)
730 file_node = self._get_filenode_or_redirect(commit, f_path)
731
731
732 raw_mimetype_mapping = {
732 raw_mimetype_mapping = {
733 # map original mimetype to a mimetype used for "show as raw"
733 # map original mimetype to a mimetype used for "show as raw"
734 # you can also provide a content-disposition to override the
734 # you can also provide a content-disposition to override the
735 # default "attachment" disposition.
735 # default "attachment" disposition.
736 # orig_type: (new_type, new_dispo)
736 # orig_type: (new_type, new_dispo)
737
737
738 # show images inline:
738 # show images inline:
739 # Do not re-add SVG: it is unsafe and permits XSS attacks. One can
739 # Do not re-add SVG: it is unsafe and permits XSS attacks. One can
740 # for example render an SVG with javascript inside or even render
740 # for example render an SVG with javascript inside or even render
741 # HTML.
741 # HTML.
742 'image/x-icon': ('image/x-icon', 'inline'),
742 'image/x-icon': ('image/x-icon', 'inline'),
743 'image/png': ('image/png', 'inline'),
743 'image/png': ('image/png', 'inline'),
744 'image/gif': ('image/gif', 'inline'),
744 'image/gif': ('image/gif', 'inline'),
745 'image/jpeg': ('image/jpeg', 'inline'),
745 'image/jpeg': ('image/jpeg', 'inline'),
746 'application/pdf': ('application/pdf', 'inline'),
746 'application/pdf': ('application/pdf', 'inline'),
747 }
747 }
748
748
749 mimetype = file_node.mimetype
749 mimetype = file_node.mimetype
750 try:
750 try:
751 mimetype, disposition = raw_mimetype_mapping[mimetype]
751 mimetype, disposition = raw_mimetype_mapping[mimetype]
752 except KeyError:
752 except KeyError:
753 # we don't know anything special about this, handle it safely
753 # we don't know anything special about this, handle it safely
754 if file_node.is_binary:
754 if file_node.is_binary:
755 # do same as download raw for binary files
755 # do same as download raw for binary files
756 mimetype, disposition = 'application/octet-stream', 'attachment'
756 mimetype, disposition = 'application/octet-stream', 'attachment'
757 else:
757 else:
758 # do not just use the original mimetype, but force text/plain,
758 # do not just use the original mimetype, but force text/plain,
759 # otherwise it would serve text/html and that might be unsafe.
759 # otherwise it would serve text/html and that might be unsafe.
760 # Note: underlying vcs library fakes text/plain mimetype if the
760 # Note: underlying vcs library fakes text/plain mimetype if the
761 # mimetype can not be determined and it thinks it is not
761 # mimetype can not be determined and it thinks it is not
762 # binary.This might lead to erroneous text display in some
762 # binary.This might lead to erroneous text display in some
763 # cases, but helps in other cases, like with text files
763 # cases, but helps in other cases, like with text files
764 # without extension.
764 # without extension.
765 mimetype, disposition = 'text/plain', 'inline'
765 mimetype, disposition = 'text/plain', 'inline'
766
766
767 if disposition == 'attachment':
767 if disposition == 'attachment':
768 disposition = self._get_attachement_disposition(f_path)
768 disposition = self._get_attachement_disposition(f_path)
769
769
770 def stream_node():
770 def stream_node():
771 yield file_node.raw_bytes
771 yield file_node.raw_bytes
772
772
773 response = Response(app_iter=stream_node())
773 response = Response(app_iter=stream_node())
774 response.content_disposition = disposition
774 response.content_disposition = disposition
775 response.content_type = mimetype
775 response.content_type = mimetype
776
776
777 charset = self._get_default_encoding(c)
777 charset = self._get_default_encoding(c)
778 if charset:
778 if charset:
779 response.charset = charset
779 response.charset = charset
780
780
781 return response
781 return response
782
782
783 @LoginRequired()
783 @LoginRequired()
784 @HasRepoPermissionAnyDecorator(
784 @HasRepoPermissionAnyDecorator(
785 'repository.read', 'repository.write', 'repository.admin')
785 'repository.read', 'repository.write', 'repository.admin')
786 @view_config(
786 @view_config(
787 route_name='repo_file_download', request_method='GET',
787 route_name='repo_file_download', request_method='GET',
788 renderer=None)
788 renderer=None)
789 @view_config(
789 @view_config(
790 route_name='repo_file_download:legacy', request_method='GET',
790 route_name='repo_file_download:legacy', request_method='GET',
791 renderer=None)
791 renderer=None)
792 def repo_file_download(self):
792 def repo_file_download(self):
793 c = self.load_default_context()
793 c = self.load_default_context()
794
794
795 commit_id, f_path = self._get_commit_and_path()
795 commit_id, f_path = self._get_commit_and_path()
796 commit = self._get_commit_or_redirect(commit_id)
796 commit = self._get_commit_or_redirect(commit_id)
797 file_node = self._get_filenode_or_redirect(commit, f_path)
797 file_node = self._get_filenode_or_redirect(commit, f_path)
798
798
799 if self.request.GET.get('lf'):
799 if self.request.GET.get('lf'):
800 # only if lf get flag is passed, we download this file
800 # only if lf get flag is passed, we download this file
801 # as LFS/Largefile
801 # as LFS/Largefile
802 lf_node = file_node.get_largefile_node()
802 lf_node = file_node.get_largefile_node()
803 if lf_node:
803 if lf_node:
804 # overwrite our pointer with the REAL large-file
804 # overwrite our pointer with the REAL large-file
805 file_node = lf_node
805 file_node = lf_node
806
806
807 disposition = self._get_attachement_disposition(f_path)
807 disposition = self._get_attachement_disposition(f_path)
808
808
809 def stream_node():
809 def stream_node():
810 yield file_node.raw_bytes
810 yield file_node.raw_bytes
811
811
812 response = Response(app_iter=stream_node())
812 response = Response(app_iter=stream_node())
813 response.content_disposition = disposition
813 response.content_disposition = disposition
814 response.content_type = file_node.mimetype
814 response.content_type = file_node.mimetype
815
815
816 charset = self._get_default_encoding(c)
816 charset = self._get_default_encoding(c)
817 if charset:
817 if charset:
818 response.charset = charset
818 response.charset = charset
819
819
820 return response
820 return response
821
821
822 def _get_nodelist_at_commit(self, repo_name, repo_id, commit_id, f_path):
822 def _get_nodelist_at_commit(self, repo_name, repo_id, commit_id, f_path):
823
823
824 cache_seconds = safe_int(
824 cache_seconds = safe_int(
825 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
825 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
826 cache_on = cache_seconds > 0
826 cache_on = cache_seconds > 0
827 log.debug(
827 log.debug(
828 'Computing FILE SEARCH for repo_id %s commit_id `%s` and path `%s`'
828 'Computing FILE SEARCH for repo_id %s commit_id `%s` and path `%s`'
829 'with caching: %s[TTL: %ss]' % (
829 'with caching: %s[TTL: %ss]' % (
830 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
830 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
831
831
832 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
832 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
833 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
833 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
834
834
835 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
835 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
836 condition=cache_on)
836 condition=cache_on)
837 def compute_file_search(repo_id, commit_id, f_path):
837 def compute_file_search(repo_id, commit_id, f_path):
838 log.debug('Generating cached nodelist for repo_id:%s, %s, %s',
838 log.debug('Generating cached nodelist for repo_id:%s, %s, %s',
839 repo_id, commit_id, f_path)
839 repo_id, commit_id, f_path)
840 try:
840 try:
841 _d, _f = ScmModel().get_nodes(
841 _d, _f = ScmModel().get_nodes(
842 repo_name, commit_id, f_path, flat=False)
842 repo_name, commit_id, f_path, flat=False)
843 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
843 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
844 log.exception(safe_str(e))
844 log.exception(safe_str(e))
845 h.flash(safe_str(h.escape(e)), category='error')
845 h.flash(safe_str(h.escape(e)), category='error')
846 raise HTTPFound(h.route_path(
846 raise HTTPFound(h.route_path(
847 'repo_files', repo_name=self.db_repo_name,
847 'repo_files', repo_name=self.db_repo_name,
848 commit_id='tip', f_path='/'))
848 commit_id='tip', f_path='/'))
849 return _d + _f
849 return _d + _f
850
850
851 return compute_file_search(self.db_repo.repo_id, commit_id, f_path)
851 return compute_file_search(self.db_repo.repo_id, commit_id, f_path)
852
852
853 @LoginRequired()
853 @LoginRequired()
854 @HasRepoPermissionAnyDecorator(
854 @HasRepoPermissionAnyDecorator(
855 'repository.read', 'repository.write', 'repository.admin')
855 'repository.read', 'repository.write', 'repository.admin')
856 @view_config(
856 @view_config(
857 route_name='repo_files_nodelist', request_method='GET',
857 route_name='repo_files_nodelist', request_method='GET',
858 renderer='json_ext', xhr=True)
858 renderer='json_ext', xhr=True)
859 def repo_nodelist(self):
859 def repo_nodelist(self):
860 self.load_default_context()
860 self.load_default_context()
861
861
862 commit_id, f_path = self._get_commit_and_path()
862 commit_id, f_path = self._get_commit_and_path()
863 commit = self._get_commit_or_redirect(commit_id)
863 commit = self._get_commit_or_redirect(commit_id)
864
864
865 metadata = self._get_nodelist_at_commit(
865 metadata = self._get_nodelist_at_commit(
866 self.db_repo_name, self.db_repo.repo_id, commit.raw_id, f_path)
866 self.db_repo_name, self.db_repo.repo_id, commit.raw_id, f_path)
867 return {'nodes': metadata}
867 return {'nodes': metadata}
868
868
869 def _create_references(
869 def _create_references(
870 self, branches_or_tags, symbolic_reference, f_path):
870 self, branches_or_tags, symbolic_reference, f_path):
871 items = []
871 items = []
872 for name, commit_id in branches_or_tags.items():
872 for name, commit_id in branches_or_tags.items():
873 sym_ref = symbolic_reference(commit_id, name, f_path)
873 sym_ref = symbolic_reference(commit_id, name, f_path)
874 items.append((sym_ref, name))
874 items.append((sym_ref, name))
875 return items
875 return items
876
876
877 def _symbolic_reference(self, commit_id, name, f_path):
877 def _symbolic_reference(self, commit_id, name, f_path):
878 return commit_id
878 return commit_id
879
879
880 def _symbolic_reference_svn(self, commit_id, name, f_path):
880 def _symbolic_reference_svn(self, commit_id, name, f_path):
881 new_f_path = vcspath.join(name, f_path)
881 new_f_path = vcspath.join(name, f_path)
882 return u'%s@%s' % (new_f_path, commit_id)
882 return u'%s@%s' % (new_f_path, commit_id)
883
883
884 def _get_node_history(self, commit_obj, f_path, commits=None):
884 def _get_node_history(self, commit_obj, f_path, commits=None):
885 """
885 """
886 get commit history for given node
886 get commit history for given node
887
887
888 :param commit_obj: commit to calculate history
888 :param commit_obj: commit to calculate history
889 :param f_path: path for node to calculate history for
889 :param f_path: path for node to calculate history for
890 :param commits: if passed don't calculate history and take
890 :param commits: if passed don't calculate history and take
891 commits defined in this list
891 commits defined in this list
892 """
892 """
893 _ = self.request.translate
893 _ = self.request.translate
894
894
895 # calculate history based on tip
895 # calculate history based on tip
896 tip = self.rhodecode_vcs_repo.get_commit()
896 tip = self.rhodecode_vcs_repo.get_commit()
897 if commits is None:
897 if commits is None:
898 pre_load = ["author", "branch"]
898 pre_load = ["author", "branch"]
899 try:
899 try:
900 commits = tip.get_file_history(f_path, pre_load=pre_load)
900 commits = tip.get_path_history(f_path, pre_load=pre_load)
901 except (NodeDoesNotExistError, CommitError):
901 except (NodeDoesNotExistError, CommitError):
902 # this node is not present at tip!
902 # this node is not present at tip!
903 commits = commit_obj.get_file_history(f_path, pre_load=pre_load)
903 commits = commit_obj.get_path_history(f_path, pre_load=pre_load)
904
904
905 history = []
905 history = []
906 commits_group = ([], _("Changesets"))
906 commits_group = ([], _("Changesets"))
907 for commit in commits:
907 for commit in commits:
908 branch = ' (%s)' % commit.branch if commit.branch else ''
908 branch = ' (%s)' % commit.branch if commit.branch else ''
909 n_desc = 'r%s:%s%s' % (commit.idx, commit.short_id, branch)
909 n_desc = 'r%s:%s%s' % (commit.idx, commit.short_id, branch)
910 commits_group[0].append((commit.raw_id, n_desc,))
910 commits_group[0].append((commit.raw_id, n_desc,))
911 history.append(commits_group)
911 history.append(commits_group)
912
912
913 symbolic_reference = self._symbolic_reference
913 symbolic_reference = self._symbolic_reference
914
914
915 if self.rhodecode_vcs_repo.alias == 'svn':
915 if self.rhodecode_vcs_repo.alias == 'svn':
916 adjusted_f_path = RepoFilesView.adjust_file_path_for_svn(
916 adjusted_f_path = RepoFilesView.adjust_file_path_for_svn(
917 f_path, self.rhodecode_vcs_repo)
917 f_path, self.rhodecode_vcs_repo)
918 if adjusted_f_path != f_path:
918 if adjusted_f_path != f_path:
919 log.debug(
919 log.debug(
920 'Recognized svn tag or branch in file "%s", using svn '
920 'Recognized svn tag or branch in file "%s", using svn '
921 'specific symbolic references', f_path)
921 'specific symbolic references', f_path)
922 f_path = adjusted_f_path
922 f_path = adjusted_f_path
923 symbolic_reference = self._symbolic_reference_svn
923 symbolic_reference = self._symbolic_reference_svn
924
924
925 branches = self._create_references(
925 branches = self._create_references(
926 self.rhodecode_vcs_repo.branches, symbolic_reference, f_path)
926 self.rhodecode_vcs_repo.branches, symbolic_reference, f_path)
927 branches_group = (branches, _("Branches"))
927 branches_group = (branches, _("Branches"))
928
928
929 tags = self._create_references(
929 tags = self._create_references(
930 self.rhodecode_vcs_repo.tags, symbolic_reference, f_path)
930 self.rhodecode_vcs_repo.tags, symbolic_reference, f_path)
931 tags_group = (tags, _("Tags"))
931 tags_group = (tags, _("Tags"))
932
932
933 history.append(branches_group)
933 history.append(branches_group)
934 history.append(tags_group)
934 history.append(tags_group)
935
935
936 return history, commits
936 return history, commits
937
937
938 @LoginRequired()
938 @LoginRequired()
939 @HasRepoPermissionAnyDecorator(
939 @HasRepoPermissionAnyDecorator(
940 'repository.read', 'repository.write', 'repository.admin')
940 'repository.read', 'repository.write', 'repository.admin')
941 @view_config(
941 @view_config(
942 route_name='repo_file_history', request_method='GET',
942 route_name='repo_file_history', request_method='GET',
943 renderer='json_ext')
943 renderer='json_ext')
944 def repo_file_history(self):
944 def repo_file_history(self):
945 self.load_default_context()
945 self.load_default_context()
946
946
947 commit_id, f_path = self._get_commit_and_path()
947 commit_id, f_path = self._get_commit_and_path()
948 commit = self._get_commit_or_redirect(commit_id)
948 commit = self._get_commit_or_redirect(commit_id)
949 file_node = self._get_filenode_or_redirect(commit, f_path)
949 file_node = self._get_filenode_or_redirect(commit, f_path)
950
950
951 if file_node.is_file():
951 if file_node.is_file():
952 file_history, _hist = self._get_node_history(commit, f_path)
952 file_history, _hist = self._get_node_history(commit, f_path)
953
953
954 res = []
954 res = []
955 for obj in file_history:
955 for obj in file_history:
956 res.append({
956 res.append({
957 'text': obj[1],
957 'text': obj[1],
958 'children': [{'id': o[0], 'text': o[1]} for o in obj[0]]
958 'children': [{'id': o[0], 'text': o[1]} for o in obj[0]]
959 })
959 })
960
960
961 data = {
961 data = {
962 'more': False,
962 'more': False,
963 'results': res
963 'results': res
964 }
964 }
965 return data
965 return data
966
966
967 log.warning('Cannot fetch history for directory')
967 log.warning('Cannot fetch history for directory')
968 raise HTTPBadRequest()
968 raise HTTPBadRequest()
969
969
970 @LoginRequired()
970 @LoginRequired()
971 @HasRepoPermissionAnyDecorator(
971 @HasRepoPermissionAnyDecorator(
972 'repository.read', 'repository.write', 'repository.admin')
972 'repository.read', 'repository.write', 'repository.admin')
973 @view_config(
973 @view_config(
974 route_name='repo_file_authors', request_method='GET',
974 route_name='repo_file_authors', request_method='GET',
975 renderer='rhodecode:templates/files/file_authors_box.mako')
975 renderer='rhodecode:templates/files/file_authors_box.mako')
976 def repo_file_authors(self):
976 def repo_file_authors(self):
977 c = self.load_default_context()
977 c = self.load_default_context()
978
978
979 commit_id, f_path = self._get_commit_and_path()
979 commit_id, f_path = self._get_commit_and_path()
980 commit = self._get_commit_or_redirect(commit_id)
980 commit = self._get_commit_or_redirect(commit_id)
981 file_node = self._get_filenode_or_redirect(commit, f_path)
981 file_node = self._get_filenode_or_redirect(commit, f_path)
982
982
983 if not file_node.is_file():
983 if not file_node.is_file():
984 raise HTTPBadRequest()
984 raise HTTPBadRequest()
985
985
986 c.file_last_commit = file_node.last_commit
986 c.file_last_commit = file_node.last_commit
987 if self.request.GET.get('annotate') == '1':
987 if self.request.GET.get('annotate') == '1':
988 # use _hist from annotation if annotation mode is on
988 # use _hist from annotation if annotation mode is on
989 commit_ids = set(x[1] for x in file_node.annotate)
989 commit_ids = set(x[1] for x in file_node.annotate)
990 _hist = (
990 _hist = (
991 self.rhodecode_vcs_repo.get_commit(commit_id)
991 self.rhodecode_vcs_repo.get_commit(commit_id)
992 for commit_id in commit_ids)
992 for commit_id in commit_ids)
993 else:
993 else:
994 _f_history, _hist = self._get_node_history(commit, f_path)
994 _f_history, _hist = self._get_node_history(commit, f_path)
995 c.file_author = False
995 c.file_author = False
996
996
997 unique = collections.OrderedDict()
997 unique = collections.OrderedDict()
998 for commit in _hist:
998 for commit in _hist:
999 author = commit.author
999 author = commit.author
1000 if author not in unique:
1000 if author not in unique:
1001 unique[commit.author] = [
1001 unique[commit.author] = [
1002 h.email(author),
1002 h.email(author),
1003 h.person(author, 'username_or_name_or_email'),
1003 h.person(author, 'username_or_name_or_email'),
1004 1 # counter
1004 1 # counter
1005 ]
1005 ]
1006
1006
1007 else:
1007 else:
1008 # increase counter
1008 # increase counter
1009 unique[commit.author][2] += 1
1009 unique[commit.author][2] += 1
1010
1010
1011 c.authors = [val for val in unique.values()]
1011 c.authors = [val for val in unique.values()]
1012
1012
1013 return self._get_template_context(c)
1013 return self._get_template_context(c)
1014
1014
1015 @LoginRequired()
1015 @LoginRequired()
1016 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1016 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1017 @view_config(
1017 @view_config(
1018 route_name='repo_files_remove_file', request_method='GET',
1018 route_name='repo_files_remove_file', request_method='GET',
1019 renderer='rhodecode:templates/files/files_delete.mako')
1019 renderer='rhodecode:templates/files/files_delete.mako')
1020 def repo_files_remove_file(self):
1020 def repo_files_remove_file(self):
1021 _ = self.request.translate
1021 _ = self.request.translate
1022 c = self.load_default_context()
1022 c = self.load_default_context()
1023 commit_id, f_path = self._get_commit_and_path()
1023 commit_id, f_path = self._get_commit_and_path()
1024
1024
1025 self._ensure_not_locked()
1025 self._ensure_not_locked()
1026 _branch_name, _sha_commit_id, is_head = \
1026 _branch_name, _sha_commit_id, is_head = \
1027 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1027 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1028
1028
1029 if not is_head:
1029 if not is_head:
1030 h.flash(_('You can only delete files with commit '
1030 h.flash(_('You can only delete files with commit '
1031 'being a valid branch head.'), category='warning')
1031 'being a valid branch head.'), category='warning')
1032 raise HTTPFound(
1032 raise HTTPFound(
1033 h.route_path('repo_files',
1033 h.route_path('repo_files',
1034 repo_name=self.db_repo_name, commit_id='tip',
1034 repo_name=self.db_repo_name, commit_id='tip',
1035 f_path=f_path))
1035 f_path=f_path))
1036
1036
1037 self.check_branch_permission(_branch_name)
1037 self.check_branch_permission(_branch_name)
1038 c.commit = self._get_commit_or_redirect(commit_id)
1038 c.commit = self._get_commit_or_redirect(commit_id)
1039 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1039 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1040
1040
1041 c.default_message = _(
1041 c.default_message = _(
1042 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1042 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1043 c.f_path = f_path
1043 c.f_path = f_path
1044
1044
1045 return self._get_template_context(c)
1045 return self._get_template_context(c)
1046
1046
1047 @LoginRequired()
1047 @LoginRequired()
1048 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1048 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1049 @CSRFRequired()
1049 @CSRFRequired()
1050 @view_config(
1050 @view_config(
1051 route_name='repo_files_delete_file', request_method='POST',
1051 route_name='repo_files_delete_file', request_method='POST',
1052 renderer=None)
1052 renderer=None)
1053 def repo_files_delete_file(self):
1053 def repo_files_delete_file(self):
1054 _ = self.request.translate
1054 _ = self.request.translate
1055
1055
1056 c = self.load_default_context()
1056 c = self.load_default_context()
1057 commit_id, f_path = self._get_commit_and_path()
1057 commit_id, f_path = self._get_commit_and_path()
1058
1058
1059 self._ensure_not_locked()
1059 self._ensure_not_locked()
1060 _branch_name, _sha_commit_id, is_head = \
1060 _branch_name, _sha_commit_id, is_head = \
1061 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1061 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1062
1062
1063 if not is_head:
1063 if not is_head:
1064 h.flash(_('You can only delete files with commit '
1064 h.flash(_('You can only delete files with commit '
1065 'being a valid branch head.'), category='warning')
1065 'being a valid branch head.'), category='warning')
1066 raise HTTPFound(
1066 raise HTTPFound(
1067 h.route_path('repo_files',
1067 h.route_path('repo_files',
1068 repo_name=self.db_repo_name, commit_id='tip',
1068 repo_name=self.db_repo_name, commit_id='tip',
1069 f_path=f_path))
1069 f_path=f_path))
1070 self.check_branch_permission(_branch_name)
1070 self.check_branch_permission(_branch_name)
1071
1071
1072 c.commit = self._get_commit_or_redirect(commit_id)
1072 c.commit = self._get_commit_or_redirect(commit_id)
1073 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1073 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1074
1074
1075 c.default_message = _(
1075 c.default_message = _(
1076 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1076 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1077 c.f_path = f_path
1077 c.f_path = f_path
1078 node_path = f_path
1078 node_path = f_path
1079 author = self._rhodecode_db_user.full_contact
1079 author = self._rhodecode_db_user.full_contact
1080 message = self.request.POST.get('message') or c.default_message
1080 message = self.request.POST.get('message') or c.default_message
1081 try:
1081 try:
1082 nodes = {
1082 nodes = {
1083 node_path: {
1083 node_path: {
1084 'content': ''
1084 'content': ''
1085 }
1085 }
1086 }
1086 }
1087 ScmModel().delete_nodes(
1087 ScmModel().delete_nodes(
1088 user=self._rhodecode_db_user.user_id, repo=self.db_repo,
1088 user=self._rhodecode_db_user.user_id, repo=self.db_repo,
1089 message=message,
1089 message=message,
1090 nodes=nodes,
1090 nodes=nodes,
1091 parent_commit=c.commit,
1091 parent_commit=c.commit,
1092 author=author,
1092 author=author,
1093 )
1093 )
1094
1094
1095 h.flash(
1095 h.flash(
1096 _('Successfully deleted file `{}`').format(
1096 _('Successfully deleted file `{}`').format(
1097 h.escape(f_path)), category='success')
1097 h.escape(f_path)), category='success')
1098 except Exception:
1098 except Exception:
1099 log.exception('Error during commit operation')
1099 log.exception('Error during commit operation')
1100 h.flash(_('Error occurred during commit'), category='error')
1100 h.flash(_('Error occurred during commit'), category='error')
1101 raise HTTPFound(
1101 raise HTTPFound(
1102 h.route_path('repo_commit', repo_name=self.db_repo_name,
1102 h.route_path('repo_commit', repo_name=self.db_repo_name,
1103 commit_id='tip'))
1103 commit_id='tip'))
1104
1104
1105 @LoginRequired()
1105 @LoginRequired()
1106 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1106 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1107 @view_config(
1107 @view_config(
1108 route_name='repo_files_edit_file', request_method='GET',
1108 route_name='repo_files_edit_file', request_method='GET',
1109 renderer='rhodecode:templates/files/files_edit.mako')
1109 renderer='rhodecode:templates/files/files_edit.mako')
1110 def repo_files_edit_file(self):
1110 def repo_files_edit_file(self):
1111 _ = self.request.translate
1111 _ = self.request.translate
1112 c = self.load_default_context()
1112 c = self.load_default_context()
1113 commit_id, f_path = self._get_commit_and_path()
1113 commit_id, f_path = self._get_commit_and_path()
1114
1114
1115 self._ensure_not_locked()
1115 self._ensure_not_locked()
1116 _branch_name, _sha_commit_id, is_head = \
1116 _branch_name, _sha_commit_id, is_head = \
1117 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1117 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1118
1118
1119 if not is_head:
1119 if not is_head:
1120 h.flash(_('You can only edit files with commit '
1120 h.flash(_('You can only edit files with commit '
1121 'being a valid branch head.'), category='warning')
1121 'being a valid branch head.'), category='warning')
1122 raise HTTPFound(
1122 raise HTTPFound(
1123 h.route_path('repo_files',
1123 h.route_path('repo_files',
1124 repo_name=self.db_repo_name, commit_id='tip',
1124 repo_name=self.db_repo_name, commit_id='tip',
1125 f_path=f_path))
1125 f_path=f_path))
1126 self.check_branch_permission(_branch_name)
1126 self.check_branch_permission(_branch_name)
1127
1127
1128 c.commit = self._get_commit_or_redirect(commit_id)
1128 c.commit = self._get_commit_or_redirect(commit_id)
1129 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1129 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1130
1130
1131 if c.file.is_binary:
1131 if c.file.is_binary:
1132 files_url = h.route_path(
1132 files_url = h.route_path(
1133 'repo_files',
1133 'repo_files',
1134 repo_name=self.db_repo_name,
1134 repo_name=self.db_repo_name,
1135 commit_id=c.commit.raw_id, f_path=f_path)
1135 commit_id=c.commit.raw_id, f_path=f_path)
1136 raise HTTPFound(files_url)
1136 raise HTTPFound(files_url)
1137
1137
1138 c.default_message = _(
1138 c.default_message = _(
1139 'Edited file {} via RhodeCode Enterprise').format(f_path)
1139 'Edited file {} via RhodeCode Enterprise').format(f_path)
1140 c.f_path = f_path
1140 c.f_path = f_path
1141
1141
1142 return self._get_template_context(c)
1142 return self._get_template_context(c)
1143
1143
1144 @LoginRequired()
1144 @LoginRequired()
1145 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1145 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1146 @CSRFRequired()
1146 @CSRFRequired()
1147 @view_config(
1147 @view_config(
1148 route_name='repo_files_update_file', request_method='POST',
1148 route_name='repo_files_update_file', request_method='POST',
1149 renderer=None)
1149 renderer=None)
1150 def repo_files_update_file(self):
1150 def repo_files_update_file(self):
1151 _ = self.request.translate
1151 _ = self.request.translate
1152 c = self.load_default_context()
1152 c = self.load_default_context()
1153 commit_id, f_path = self._get_commit_and_path()
1153 commit_id, f_path = self._get_commit_and_path()
1154
1154
1155 self._ensure_not_locked()
1155 self._ensure_not_locked()
1156 _branch_name, _sha_commit_id, is_head = \
1156 _branch_name, _sha_commit_id, is_head = \
1157 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1157 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1158
1158
1159 if not is_head:
1159 if not is_head:
1160 h.flash(_('You can only edit files with commit '
1160 h.flash(_('You can only edit files with commit '
1161 'being a valid branch head.'), category='warning')
1161 'being a valid branch head.'), category='warning')
1162 raise HTTPFound(
1162 raise HTTPFound(
1163 h.route_path('repo_files',
1163 h.route_path('repo_files',
1164 repo_name=self.db_repo_name, commit_id='tip',
1164 repo_name=self.db_repo_name, commit_id='tip',
1165 f_path=f_path))
1165 f_path=f_path))
1166
1166
1167 self.check_branch_permission(_branch_name)
1167 self.check_branch_permission(_branch_name)
1168
1168
1169 c.commit = self._get_commit_or_redirect(commit_id)
1169 c.commit = self._get_commit_or_redirect(commit_id)
1170 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1170 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1171
1171
1172 if c.file.is_binary:
1172 if c.file.is_binary:
1173 raise HTTPFound(
1173 raise HTTPFound(
1174 h.route_path('repo_files',
1174 h.route_path('repo_files',
1175 repo_name=self.db_repo_name,
1175 repo_name=self.db_repo_name,
1176 commit_id=c.commit.raw_id,
1176 commit_id=c.commit.raw_id,
1177 f_path=f_path))
1177 f_path=f_path))
1178
1178
1179 c.default_message = _(
1179 c.default_message = _(
1180 'Edited file {} via RhodeCode Enterprise').format(f_path)
1180 'Edited file {} via RhodeCode Enterprise').format(f_path)
1181 c.f_path = f_path
1181 c.f_path = f_path
1182 old_content = c.file.content
1182 old_content = c.file.content
1183 sl = old_content.splitlines(1)
1183 sl = old_content.splitlines(1)
1184 first_line = sl[0] if sl else ''
1184 first_line = sl[0] if sl else ''
1185
1185
1186 r_post = self.request.POST
1186 r_post = self.request.POST
1187 # modes: 0 - Unix, 1 - Mac, 2 - DOS
1187 # modes: 0 - Unix, 1 - Mac, 2 - DOS
1188 mode = detect_mode(first_line, 0)
1188 mode = detect_mode(first_line, 0)
1189 content = convert_line_endings(r_post.get('content', ''), mode)
1189 content = convert_line_endings(r_post.get('content', ''), mode)
1190
1190
1191 message = r_post.get('message') or c.default_message
1191 message = r_post.get('message') or c.default_message
1192 org_f_path = c.file.unicode_path
1192 org_f_path = c.file.unicode_path
1193 filename = r_post['filename']
1193 filename = r_post['filename']
1194 org_filename = c.file.name
1194 org_filename = c.file.name
1195
1195
1196 if content == old_content and filename == org_filename:
1196 if content == old_content and filename == org_filename:
1197 h.flash(_('No changes'), category='warning')
1197 h.flash(_('No changes'), category='warning')
1198 raise HTTPFound(
1198 raise HTTPFound(
1199 h.route_path('repo_commit', repo_name=self.db_repo_name,
1199 h.route_path('repo_commit', repo_name=self.db_repo_name,
1200 commit_id='tip'))
1200 commit_id='tip'))
1201 try:
1201 try:
1202 mapping = {
1202 mapping = {
1203 org_f_path: {
1203 org_f_path: {
1204 'org_filename': org_f_path,
1204 'org_filename': org_f_path,
1205 'filename': os.path.join(c.file.dir_path, filename),
1205 'filename': os.path.join(c.file.dir_path, filename),
1206 'content': content,
1206 'content': content,
1207 'lexer': '',
1207 'lexer': '',
1208 'op': 'mod',
1208 'op': 'mod',
1209 }
1209 }
1210 }
1210 }
1211
1211
1212 ScmModel().update_nodes(
1212 ScmModel().update_nodes(
1213 user=self._rhodecode_db_user.user_id,
1213 user=self._rhodecode_db_user.user_id,
1214 repo=self.db_repo,
1214 repo=self.db_repo,
1215 message=message,
1215 message=message,
1216 nodes=mapping,
1216 nodes=mapping,
1217 parent_commit=c.commit,
1217 parent_commit=c.commit,
1218 )
1218 )
1219
1219
1220 h.flash(
1220 h.flash(
1221 _('Successfully committed changes to file `{}`').format(
1221 _('Successfully committed changes to file `{}`').format(
1222 h.escape(f_path)), category='success')
1222 h.escape(f_path)), category='success')
1223 except Exception:
1223 except Exception:
1224 log.exception('Error occurred during commit')
1224 log.exception('Error occurred during commit')
1225 h.flash(_('Error occurred during commit'), category='error')
1225 h.flash(_('Error occurred during commit'), category='error')
1226 raise HTTPFound(
1226 raise HTTPFound(
1227 h.route_path('repo_commit', repo_name=self.db_repo_name,
1227 h.route_path('repo_commit', repo_name=self.db_repo_name,
1228 commit_id='tip'))
1228 commit_id='tip'))
1229
1229
1230 @LoginRequired()
1230 @LoginRequired()
1231 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1231 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1232 @view_config(
1232 @view_config(
1233 route_name='repo_files_add_file', request_method='GET',
1233 route_name='repo_files_add_file', request_method='GET',
1234 renderer='rhodecode:templates/files/files_add.mako')
1234 renderer='rhodecode:templates/files/files_add.mako')
1235 def repo_files_add_file(self):
1235 def repo_files_add_file(self):
1236 _ = self.request.translate
1236 _ = self.request.translate
1237 c = self.load_default_context()
1237 c = self.load_default_context()
1238 commit_id, f_path = self._get_commit_and_path()
1238 commit_id, f_path = self._get_commit_and_path()
1239
1239
1240 self._ensure_not_locked()
1240 self._ensure_not_locked()
1241
1241
1242 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1242 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1243 if c.commit is None:
1243 if c.commit is None:
1244 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1244 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1245 c.default_message = (_('Added file via RhodeCode Enterprise'))
1245 c.default_message = (_('Added file via RhodeCode Enterprise'))
1246 c.f_path = f_path.lstrip('/') # ensure not relative path
1246 c.f_path = f_path.lstrip('/') # ensure not relative path
1247
1247
1248 if self.rhodecode_vcs_repo.is_empty:
1248 if self.rhodecode_vcs_repo.is_empty:
1249 # for empty repository we cannot check for current branch, we rely on
1249 # for empty repository we cannot check for current branch, we rely on
1250 # c.commit.branch instead
1250 # c.commit.branch instead
1251 _branch_name = c.commit.branch
1251 _branch_name = c.commit.branch
1252 is_head = True
1252 is_head = True
1253 else:
1253 else:
1254 _branch_name, _sha_commit_id, is_head = \
1254 _branch_name, _sha_commit_id, is_head = \
1255 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1255 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1256
1256
1257 if not is_head:
1257 if not is_head:
1258 h.flash(_('You can only add files with commit '
1258 h.flash(_('You can only add files with commit '
1259 'being a valid branch head.'), category='warning')
1259 'being a valid branch head.'), category='warning')
1260 raise HTTPFound(
1260 raise HTTPFound(
1261 h.route_path('repo_files',
1261 h.route_path('repo_files',
1262 repo_name=self.db_repo_name, commit_id='tip',
1262 repo_name=self.db_repo_name, commit_id='tip',
1263 f_path=f_path))
1263 f_path=f_path))
1264
1264
1265 self.check_branch_permission(_branch_name)
1265 self.check_branch_permission(_branch_name)
1266
1266
1267 return self._get_template_context(c)
1267 return self._get_template_context(c)
1268
1268
1269 @LoginRequired()
1269 @LoginRequired()
1270 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1270 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1271 @CSRFRequired()
1271 @CSRFRequired()
1272 @view_config(
1272 @view_config(
1273 route_name='repo_files_create_file', request_method='POST',
1273 route_name='repo_files_create_file', request_method='POST',
1274 renderer=None)
1274 renderer=None)
1275 def repo_files_create_file(self):
1275 def repo_files_create_file(self):
1276 _ = self.request.translate
1276 _ = self.request.translate
1277 c = self.load_default_context()
1277 c = self.load_default_context()
1278 commit_id, f_path = self._get_commit_and_path()
1278 commit_id, f_path = self._get_commit_and_path()
1279
1279
1280 self._ensure_not_locked()
1280 self._ensure_not_locked()
1281
1281
1282 r_post = self.request.POST
1282 r_post = self.request.POST
1283
1283
1284 c.commit = self._get_commit_or_redirect(
1284 c.commit = self._get_commit_or_redirect(
1285 commit_id, redirect_after=False)
1285 commit_id, redirect_after=False)
1286 if c.commit is None:
1286 if c.commit is None:
1287 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1287 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1288
1288
1289 if self.rhodecode_vcs_repo.is_empty:
1289 if self.rhodecode_vcs_repo.is_empty:
1290 # for empty repository we cannot check for current branch, we rely on
1290 # for empty repository we cannot check for current branch, we rely on
1291 # c.commit.branch instead
1291 # c.commit.branch instead
1292 _branch_name = c.commit.branch
1292 _branch_name = c.commit.branch
1293 is_head = True
1293 is_head = True
1294 else:
1294 else:
1295 _branch_name, _sha_commit_id, is_head = \
1295 _branch_name, _sha_commit_id, is_head = \
1296 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1296 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1297
1297
1298 if not is_head:
1298 if not is_head:
1299 h.flash(_('You can only add files with commit '
1299 h.flash(_('You can only add files with commit '
1300 'being a valid branch head.'), category='warning')
1300 'being a valid branch head.'), category='warning')
1301 raise HTTPFound(
1301 raise HTTPFound(
1302 h.route_path('repo_files',
1302 h.route_path('repo_files',
1303 repo_name=self.db_repo_name, commit_id='tip',
1303 repo_name=self.db_repo_name, commit_id='tip',
1304 f_path=f_path))
1304 f_path=f_path))
1305
1305
1306 self.check_branch_permission(_branch_name)
1306 self.check_branch_permission(_branch_name)
1307
1307
1308 c.default_message = (_('Added file via RhodeCode Enterprise'))
1308 c.default_message = (_('Added file via RhodeCode Enterprise'))
1309 c.f_path = f_path
1309 c.f_path = f_path
1310 unix_mode = 0
1310 unix_mode = 0
1311 content = convert_line_endings(r_post.get('content', ''), unix_mode)
1311 content = convert_line_endings(r_post.get('content', ''), unix_mode)
1312
1312
1313 message = r_post.get('message') or c.default_message
1313 message = r_post.get('message') or c.default_message
1314 filename = r_post.get('filename')
1314 filename = r_post.get('filename')
1315 location = r_post.get('location', '') # dir location
1315 location = r_post.get('location', '') # dir location
1316 file_obj = r_post.get('upload_file', None)
1316 file_obj = r_post.get('upload_file', None)
1317
1317
1318 if file_obj is not None and hasattr(file_obj, 'filename'):
1318 if file_obj is not None and hasattr(file_obj, 'filename'):
1319 filename = r_post.get('filename_upload')
1319 filename = r_post.get('filename_upload')
1320 content = file_obj.file
1320 content = file_obj.file
1321
1321
1322 if hasattr(content, 'file'):
1322 if hasattr(content, 'file'):
1323 # non posix systems store real file under file attr
1323 # non posix systems store real file under file attr
1324 content = content.file
1324 content = content.file
1325
1325
1326 if self.rhodecode_vcs_repo.is_empty:
1326 if self.rhodecode_vcs_repo.is_empty:
1327 default_redirect_url = h.route_path(
1327 default_redirect_url = h.route_path(
1328 'repo_summary', repo_name=self.db_repo_name)
1328 'repo_summary', repo_name=self.db_repo_name)
1329 else:
1329 else:
1330 default_redirect_url = h.route_path(
1330 default_redirect_url = h.route_path(
1331 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1331 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1332
1332
1333 # If there's no commit, redirect to repo summary
1333 # If there's no commit, redirect to repo summary
1334 if type(c.commit) is EmptyCommit:
1334 if type(c.commit) is EmptyCommit:
1335 redirect_url = h.route_path(
1335 redirect_url = h.route_path(
1336 'repo_summary', repo_name=self.db_repo_name)
1336 'repo_summary', repo_name=self.db_repo_name)
1337 else:
1337 else:
1338 redirect_url = default_redirect_url
1338 redirect_url = default_redirect_url
1339
1339
1340 if not filename:
1340 if not filename:
1341 h.flash(_('No filename'), category='warning')
1341 h.flash(_('No filename'), category='warning')
1342 raise HTTPFound(redirect_url)
1342 raise HTTPFound(redirect_url)
1343
1343
1344 # extract the location from filename,
1344 # extract the location from filename,
1345 # allows using foo/bar.txt syntax to create subdirectories
1345 # allows using foo/bar.txt syntax to create subdirectories
1346 subdir_loc = filename.rsplit('/', 1)
1346 subdir_loc = filename.rsplit('/', 1)
1347 if len(subdir_loc) == 2:
1347 if len(subdir_loc) == 2:
1348 location = os.path.join(location, subdir_loc[0])
1348 location = os.path.join(location, subdir_loc[0])
1349
1349
1350 # strip all crap out of file, just leave the basename
1350 # strip all crap out of file, just leave the basename
1351 filename = os.path.basename(filename)
1351 filename = os.path.basename(filename)
1352 node_path = os.path.join(location, filename)
1352 node_path = os.path.join(location, filename)
1353 author = self._rhodecode_db_user.full_contact
1353 author = self._rhodecode_db_user.full_contact
1354
1354
1355 try:
1355 try:
1356 nodes = {
1356 nodes = {
1357 node_path: {
1357 node_path: {
1358 'content': content
1358 'content': content
1359 }
1359 }
1360 }
1360 }
1361 ScmModel().create_nodes(
1361 ScmModel().create_nodes(
1362 user=self._rhodecode_db_user.user_id,
1362 user=self._rhodecode_db_user.user_id,
1363 repo=self.db_repo,
1363 repo=self.db_repo,
1364 message=message,
1364 message=message,
1365 nodes=nodes,
1365 nodes=nodes,
1366 parent_commit=c.commit,
1366 parent_commit=c.commit,
1367 author=author,
1367 author=author,
1368 )
1368 )
1369
1369
1370 h.flash(
1370 h.flash(
1371 _('Successfully committed new file `{}`').format(
1371 _('Successfully committed new file `{}`').format(
1372 h.escape(node_path)), category='success')
1372 h.escape(node_path)), category='success')
1373 except NonRelativePathError:
1373 except NonRelativePathError:
1374 log.exception('Non Relative path found')
1374 log.exception('Non Relative path found')
1375 h.flash(_(
1375 h.flash(_(
1376 'The location specified must be a relative path and must not '
1376 'The location specified must be a relative path and must not '
1377 'contain .. in the path'), category='warning')
1377 'contain .. in the path'), category='warning')
1378 raise HTTPFound(default_redirect_url)
1378 raise HTTPFound(default_redirect_url)
1379 except (NodeError, NodeAlreadyExistsError) as e:
1379 except (NodeError, NodeAlreadyExistsError) as e:
1380 h.flash(_(h.escape(e)), category='error')
1380 h.flash(_(h.escape(e)), category='error')
1381 except Exception:
1381 except Exception:
1382 log.exception('Error occurred during commit')
1382 log.exception('Error occurred during commit')
1383 h.flash(_('Error occurred during commit'), category='error')
1383 h.flash(_('Error occurred during commit'), category='error')
1384
1384
1385 raise HTTPFound(default_redirect_url)
1385 raise HTTPFound(default_redirect_url)
@@ -1,1755 +1,1755 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2014-2018 RhodeCode GmbH
3 # Copyright (C) 2014-2018 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Base module for all VCS systems
22 Base module for all VCS systems
23 """
23 """
24
24
25 import collections
25 import collections
26 import datetime
26 import datetime
27 import fnmatch
27 import fnmatch
28 import itertools
28 import itertools
29 import logging
29 import logging
30 import os
30 import os
31 import re
31 import re
32 import time
32 import time
33 import warnings
33 import warnings
34 import shutil
34 import shutil
35
35
36 from zope.cachedescriptors.property import Lazy as LazyProperty
36 from zope.cachedescriptors.property import Lazy as LazyProperty
37
37
38 from rhodecode.lib.utils2 import safe_str, safe_unicode
38 from rhodecode.lib.utils2 import safe_str, safe_unicode
39 from rhodecode.lib.vcs import connection
39 from rhodecode.lib.vcs import connection
40 from rhodecode.lib.vcs.utils import author_name, author_email
40 from rhodecode.lib.vcs.utils import author_name, author_email
41 from rhodecode.lib.vcs.conf import settings
41 from rhodecode.lib.vcs.conf import settings
42 from rhodecode.lib.vcs.exceptions import (
42 from rhodecode.lib.vcs.exceptions import (
43 CommitError, EmptyRepositoryError, NodeAlreadyAddedError,
43 CommitError, EmptyRepositoryError, NodeAlreadyAddedError,
44 NodeAlreadyChangedError, NodeAlreadyExistsError, NodeAlreadyRemovedError,
44 NodeAlreadyChangedError, NodeAlreadyExistsError, NodeAlreadyRemovedError,
45 NodeDoesNotExistError, NodeNotChangedError, VCSError,
45 NodeDoesNotExistError, NodeNotChangedError, VCSError,
46 ImproperArchiveTypeError, BranchDoesNotExistError, CommitDoesNotExistError,
46 ImproperArchiveTypeError, BranchDoesNotExistError, CommitDoesNotExistError,
47 RepositoryError)
47 RepositoryError)
48
48
49
49
50 log = logging.getLogger(__name__)
50 log = logging.getLogger(__name__)
51
51
52
52
53 FILEMODE_DEFAULT = 0o100644
53 FILEMODE_DEFAULT = 0o100644
54 FILEMODE_EXECUTABLE = 0o100755
54 FILEMODE_EXECUTABLE = 0o100755
55
55
56 Reference = collections.namedtuple('Reference', ('type', 'name', 'commit_id'))
56 Reference = collections.namedtuple('Reference', ('type', 'name', 'commit_id'))
57 MergeResponse = collections.namedtuple(
57 MergeResponse = collections.namedtuple(
58 'MergeResponse',
58 'MergeResponse',
59 ('possible', 'executed', 'merge_ref', 'failure_reason'))
59 ('possible', 'executed', 'merge_ref', 'failure_reason'))
60
60
61
61
62 class MergeFailureReason(object):
62 class MergeFailureReason(object):
63 """
63 """
64 Enumeration with all the reasons why the server side merge could fail.
64 Enumeration with all the reasons why the server side merge could fail.
65
65
66 DO NOT change the number of the reasons, as they may be stored in the
66 DO NOT change the number of the reasons, as they may be stored in the
67 database.
67 database.
68
68
69 Changing the name of a reason is acceptable and encouraged to deprecate old
69 Changing the name of a reason is acceptable and encouraged to deprecate old
70 reasons.
70 reasons.
71 """
71 """
72
72
73 # Everything went well.
73 # Everything went well.
74 NONE = 0
74 NONE = 0
75
75
76 # An unexpected exception was raised. Check the logs for more details.
76 # An unexpected exception was raised. Check the logs for more details.
77 UNKNOWN = 1
77 UNKNOWN = 1
78
78
79 # The merge was not successful, there are conflicts.
79 # The merge was not successful, there are conflicts.
80 MERGE_FAILED = 2
80 MERGE_FAILED = 2
81
81
82 # The merge succeeded but we could not push it to the target repository.
82 # The merge succeeded but we could not push it to the target repository.
83 PUSH_FAILED = 3
83 PUSH_FAILED = 3
84
84
85 # The specified target is not a head in the target repository.
85 # The specified target is not a head in the target repository.
86 TARGET_IS_NOT_HEAD = 4
86 TARGET_IS_NOT_HEAD = 4
87
87
88 # The source repository contains more branches than the target. Pushing
88 # The source repository contains more branches than the target. Pushing
89 # the merge will create additional branches in the target.
89 # the merge will create additional branches in the target.
90 HG_SOURCE_HAS_MORE_BRANCHES = 5
90 HG_SOURCE_HAS_MORE_BRANCHES = 5
91
91
92 # The target reference has multiple heads. That does not allow to correctly
92 # The target reference has multiple heads. That does not allow to correctly
93 # identify the target location. This could only happen for mercurial
93 # identify the target location. This could only happen for mercurial
94 # branches.
94 # branches.
95 HG_TARGET_HAS_MULTIPLE_HEADS = 6
95 HG_TARGET_HAS_MULTIPLE_HEADS = 6
96
96
97 # The target repository is locked
97 # The target repository is locked
98 TARGET_IS_LOCKED = 7
98 TARGET_IS_LOCKED = 7
99
99
100 # Deprecated, use MISSING_TARGET_REF or MISSING_SOURCE_REF instead.
100 # Deprecated, use MISSING_TARGET_REF or MISSING_SOURCE_REF instead.
101 # A involved commit could not be found.
101 # A involved commit could not be found.
102 _DEPRECATED_MISSING_COMMIT = 8
102 _DEPRECATED_MISSING_COMMIT = 8
103
103
104 # The target repo reference is missing.
104 # The target repo reference is missing.
105 MISSING_TARGET_REF = 9
105 MISSING_TARGET_REF = 9
106
106
107 # The source repo reference is missing.
107 # The source repo reference is missing.
108 MISSING_SOURCE_REF = 10
108 MISSING_SOURCE_REF = 10
109
109
110 # The merge was not successful, there are conflicts related to sub
110 # The merge was not successful, there are conflicts related to sub
111 # repositories.
111 # repositories.
112 SUBREPO_MERGE_FAILED = 11
112 SUBREPO_MERGE_FAILED = 11
113
113
114
114
115 class UpdateFailureReason(object):
115 class UpdateFailureReason(object):
116 """
116 """
117 Enumeration with all the reasons why the pull request update could fail.
117 Enumeration with all the reasons why the pull request update could fail.
118
118
119 DO NOT change the number of the reasons, as they may be stored in the
119 DO NOT change the number of the reasons, as they may be stored in the
120 database.
120 database.
121
121
122 Changing the name of a reason is acceptable and encouraged to deprecate old
122 Changing the name of a reason is acceptable and encouraged to deprecate old
123 reasons.
123 reasons.
124 """
124 """
125
125
126 # Everything went well.
126 # Everything went well.
127 NONE = 0
127 NONE = 0
128
128
129 # An unexpected exception was raised. Check the logs for more details.
129 # An unexpected exception was raised. Check the logs for more details.
130 UNKNOWN = 1
130 UNKNOWN = 1
131
131
132 # The pull request is up to date.
132 # The pull request is up to date.
133 NO_CHANGE = 2
133 NO_CHANGE = 2
134
134
135 # The pull request has a reference type that is not supported for update.
135 # The pull request has a reference type that is not supported for update.
136 WRONG_REF_TYPE = 3
136 WRONG_REF_TYPE = 3
137
137
138 # Update failed because the target reference is missing.
138 # Update failed because the target reference is missing.
139 MISSING_TARGET_REF = 4
139 MISSING_TARGET_REF = 4
140
140
141 # Update failed because the source reference is missing.
141 # Update failed because the source reference is missing.
142 MISSING_SOURCE_REF = 5
142 MISSING_SOURCE_REF = 5
143
143
144
144
145 class BaseRepository(object):
145 class BaseRepository(object):
146 """
146 """
147 Base Repository for final backends
147 Base Repository for final backends
148
148
149 .. attribute:: DEFAULT_BRANCH_NAME
149 .. attribute:: DEFAULT_BRANCH_NAME
150
150
151 name of default branch (i.e. "trunk" for svn, "master" for git etc.
151 name of default branch (i.e. "trunk" for svn, "master" for git etc.
152
152
153 .. attribute:: commit_ids
153 .. attribute:: commit_ids
154
154
155 list of all available commit ids, in ascending order
155 list of all available commit ids, in ascending order
156
156
157 .. attribute:: path
157 .. attribute:: path
158
158
159 absolute path to the repository
159 absolute path to the repository
160
160
161 .. attribute:: bookmarks
161 .. attribute:: bookmarks
162
162
163 Mapping from name to :term:`Commit ID` of the bookmark. Empty in case
163 Mapping from name to :term:`Commit ID` of the bookmark. Empty in case
164 there are no bookmarks or the backend implementation does not support
164 there are no bookmarks or the backend implementation does not support
165 bookmarks.
165 bookmarks.
166
166
167 .. attribute:: tags
167 .. attribute:: tags
168
168
169 Mapping from name to :term:`Commit ID` of the tag.
169 Mapping from name to :term:`Commit ID` of the tag.
170
170
171 """
171 """
172
172
173 DEFAULT_BRANCH_NAME = None
173 DEFAULT_BRANCH_NAME = None
174 DEFAULT_CONTACT = u"Unknown"
174 DEFAULT_CONTACT = u"Unknown"
175 DEFAULT_DESCRIPTION = u"unknown"
175 DEFAULT_DESCRIPTION = u"unknown"
176 EMPTY_COMMIT_ID = '0' * 40
176 EMPTY_COMMIT_ID = '0' * 40
177
177
178 path = None
178 path = None
179
179
180 def __init__(self, repo_path, config=None, create=False, **kwargs):
180 def __init__(self, repo_path, config=None, create=False, **kwargs):
181 """
181 """
182 Initializes repository. Raises RepositoryError if repository could
182 Initializes repository. Raises RepositoryError if repository could
183 not be find at the given ``repo_path`` or directory at ``repo_path``
183 not be find at the given ``repo_path`` or directory at ``repo_path``
184 exists and ``create`` is set to True.
184 exists and ``create`` is set to True.
185
185
186 :param repo_path: local path of the repository
186 :param repo_path: local path of the repository
187 :param config: repository configuration
187 :param config: repository configuration
188 :param create=False: if set to True, would try to create repository.
188 :param create=False: if set to True, would try to create repository.
189 :param src_url=None: if set, should be proper url from which repository
189 :param src_url=None: if set, should be proper url from which repository
190 would be cloned; requires ``create`` parameter to be set to True -
190 would be cloned; requires ``create`` parameter to be set to True -
191 raises RepositoryError if src_url is set and create evaluates to
191 raises RepositoryError if src_url is set and create evaluates to
192 False
192 False
193 """
193 """
194 raise NotImplementedError
194 raise NotImplementedError
195
195
196 def __repr__(self):
196 def __repr__(self):
197 return '<%s at %s>' % (self.__class__.__name__, self.path)
197 return '<%s at %s>' % (self.__class__.__name__, self.path)
198
198
199 def __len__(self):
199 def __len__(self):
200 return self.count()
200 return self.count()
201
201
202 def __eq__(self, other):
202 def __eq__(self, other):
203 same_instance = isinstance(other, self.__class__)
203 same_instance = isinstance(other, self.__class__)
204 return same_instance and other.path == self.path
204 return same_instance and other.path == self.path
205
205
206 def __ne__(self, other):
206 def __ne__(self, other):
207 return not self.__eq__(other)
207 return not self.__eq__(other)
208
208
209 def get_create_shadow_cache_pr_path(self, db_repo):
209 def get_create_shadow_cache_pr_path(self, db_repo):
210 path = db_repo.cached_diffs_dir
210 path = db_repo.cached_diffs_dir
211 if not os.path.exists(path):
211 if not os.path.exists(path):
212 os.makedirs(path, 0o755)
212 os.makedirs(path, 0o755)
213 return path
213 return path
214
214
215 @classmethod
215 @classmethod
216 def get_default_config(cls, default=None):
216 def get_default_config(cls, default=None):
217 config = Config()
217 config = Config()
218 if default and isinstance(default, list):
218 if default and isinstance(default, list):
219 for section, key, val in default:
219 for section, key, val in default:
220 config.set(section, key, val)
220 config.set(section, key, val)
221 return config
221 return config
222
222
223 @LazyProperty
223 @LazyProperty
224 def _remote(self):
224 def _remote(self):
225 raise NotImplementedError
225 raise NotImplementedError
226
226
227 @LazyProperty
227 @LazyProperty
228 def EMPTY_COMMIT(self):
228 def EMPTY_COMMIT(self):
229 return EmptyCommit(self.EMPTY_COMMIT_ID)
229 return EmptyCommit(self.EMPTY_COMMIT_ID)
230
230
231 @LazyProperty
231 @LazyProperty
232 def alias(self):
232 def alias(self):
233 for k, v in settings.BACKENDS.items():
233 for k, v in settings.BACKENDS.items():
234 if v.split('.')[-1] == str(self.__class__.__name__):
234 if v.split('.')[-1] == str(self.__class__.__name__):
235 return k
235 return k
236
236
237 @LazyProperty
237 @LazyProperty
238 def name(self):
238 def name(self):
239 return safe_unicode(os.path.basename(self.path))
239 return safe_unicode(os.path.basename(self.path))
240
240
241 @LazyProperty
241 @LazyProperty
242 def description(self):
242 def description(self):
243 raise NotImplementedError
243 raise NotImplementedError
244
244
245 def refs(self):
245 def refs(self):
246 """
246 """
247 returns a `dict` with branches, bookmarks, tags, and closed_branches
247 returns a `dict` with branches, bookmarks, tags, and closed_branches
248 for this repository
248 for this repository
249 """
249 """
250 return dict(
250 return dict(
251 branches=self.branches,
251 branches=self.branches,
252 branches_closed=self.branches_closed,
252 branches_closed=self.branches_closed,
253 tags=self.tags,
253 tags=self.tags,
254 bookmarks=self.bookmarks
254 bookmarks=self.bookmarks
255 )
255 )
256
256
257 @LazyProperty
257 @LazyProperty
258 def branches(self):
258 def branches(self):
259 """
259 """
260 A `dict` which maps branch names to commit ids.
260 A `dict` which maps branch names to commit ids.
261 """
261 """
262 raise NotImplementedError
262 raise NotImplementedError
263
263
264 @LazyProperty
264 @LazyProperty
265 def branches_closed(self):
265 def branches_closed(self):
266 """
266 """
267 A `dict` which maps tags names to commit ids.
267 A `dict` which maps tags names to commit ids.
268 """
268 """
269 raise NotImplementedError
269 raise NotImplementedError
270
270
271 @LazyProperty
271 @LazyProperty
272 def bookmarks(self):
272 def bookmarks(self):
273 """
273 """
274 A `dict` which maps tags names to commit ids.
274 A `dict` which maps tags names to commit ids.
275 """
275 """
276 raise NotImplementedError
276 raise NotImplementedError
277
277
278 @LazyProperty
278 @LazyProperty
279 def tags(self):
279 def tags(self):
280 """
280 """
281 A `dict` which maps tags names to commit ids.
281 A `dict` which maps tags names to commit ids.
282 """
282 """
283 raise NotImplementedError
283 raise NotImplementedError
284
284
285 @LazyProperty
285 @LazyProperty
286 def size(self):
286 def size(self):
287 """
287 """
288 Returns combined size in bytes for all repository files
288 Returns combined size in bytes for all repository files
289 """
289 """
290 tip = self.get_commit()
290 tip = self.get_commit()
291 return tip.size
291 return tip.size
292
292
293 def size_at_commit(self, commit_id):
293 def size_at_commit(self, commit_id):
294 commit = self.get_commit(commit_id)
294 commit = self.get_commit(commit_id)
295 return commit.size
295 return commit.size
296
296
297 def is_empty(self):
297 def is_empty(self):
298 return not bool(self.commit_ids)
298 return not bool(self.commit_ids)
299
299
300 @staticmethod
300 @staticmethod
301 def check_url(url, config):
301 def check_url(url, config):
302 """
302 """
303 Function will check given url and try to verify if it's a valid
303 Function will check given url and try to verify if it's a valid
304 link.
304 link.
305 """
305 """
306 raise NotImplementedError
306 raise NotImplementedError
307
307
308 @staticmethod
308 @staticmethod
309 def is_valid_repository(path):
309 def is_valid_repository(path):
310 """
310 """
311 Check if given `path` contains a valid repository of this backend
311 Check if given `path` contains a valid repository of this backend
312 """
312 """
313 raise NotImplementedError
313 raise NotImplementedError
314
314
315 # ==========================================================================
315 # ==========================================================================
316 # COMMITS
316 # COMMITS
317 # ==========================================================================
317 # ==========================================================================
318
318
319 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
319 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
320 """
320 """
321 Returns instance of `BaseCommit` class. If `commit_id` and `commit_idx`
321 Returns instance of `BaseCommit` class. If `commit_id` and `commit_idx`
322 are both None, most recent commit is returned.
322 are both None, most recent commit is returned.
323
323
324 :param pre_load: Optional. List of commit attributes to load.
324 :param pre_load: Optional. List of commit attributes to load.
325
325
326 :raises ``EmptyRepositoryError``: if there are no commits
326 :raises ``EmptyRepositoryError``: if there are no commits
327 """
327 """
328 raise NotImplementedError
328 raise NotImplementedError
329
329
330 def __iter__(self):
330 def __iter__(self):
331 for commit_id in self.commit_ids:
331 for commit_id in self.commit_ids:
332 yield self.get_commit(commit_id=commit_id)
332 yield self.get_commit(commit_id=commit_id)
333
333
334 def get_commits(
334 def get_commits(
335 self, start_id=None, end_id=None, start_date=None, end_date=None,
335 self, start_id=None, end_id=None, start_date=None, end_date=None,
336 branch_name=None, show_hidden=False, pre_load=None):
336 branch_name=None, show_hidden=False, pre_load=None):
337 """
337 """
338 Returns iterator of `BaseCommit` objects from start to end
338 Returns iterator of `BaseCommit` objects from start to end
339 not inclusive. This should behave just like a list, ie. end is not
339 not inclusive. This should behave just like a list, ie. end is not
340 inclusive.
340 inclusive.
341
341
342 :param start_id: None or str, must be a valid commit id
342 :param start_id: None or str, must be a valid commit id
343 :param end_id: None or str, must be a valid commit id
343 :param end_id: None or str, must be a valid commit id
344 :param start_date:
344 :param start_date:
345 :param end_date:
345 :param end_date:
346 :param branch_name:
346 :param branch_name:
347 :param show_hidden:
347 :param show_hidden:
348 :param pre_load:
348 :param pre_load:
349 """
349 """
350 raise NotImplementedError
350 raise NotImplementedError
351
351
352 def __getitem__(self, key):
352 def __getitem__(self, key):
353 """
353 """
354 Allows index based access to the commit objects of this repository.
354 Allows index based access to the commit objects of this repository.
355 """
355 """
356 pre_load = ["author", "branch", "date", "message", "parents"]
356 pre_load = ["author", "branch", "date", "message", "parents"]
357 if isinstance(key, slice):
357 if isinstance(key, slice):
358 return self._get_range(key, pre_load)
358 return self._get_range(key, pre_load)
359 return self.get_commit(commit_idx=key, pre_load=pre_load)
359 return self.get_commit(commit_idx=key, pre_load=pre_load)
360
360
361 def _get_range(self, slice_obj, pre_load):
361 def _get_range(self, slice_obj, pre_load):
362 for commit_id in self.commit_ids.__getitem__(slice_obj):
362 for commit_id in self.commit_ids.__getitem__(slice_obj):
363 yield self.get_commit(commit_id=commit_id, pre_load=pre_load)
363 yield self.get_commit(commit_id=commit_id, pre_load=pre_load)
364
364
365 def count(self):
365 def count(self):
366 return len(self.commit_ids)
366 return len(self.commit_ids)
367
367
368 def tag(self, name, user, commit_id=None, message=None, date=None, **opts):
368 def tag(self, name, user, commit_id=None, message=None, date=None, **opts):
369 """
369 """
370 Creates and returns a tag for the given ``commit_id``.
370 Creates and returns a tag for the given ``commit_id``.
371
371
372 :param name: name for new tag
372 :param name: name for new tag
373 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
373 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
374 :param commit_id: commit id for which new tag would be created
374 :param commit_id: commit id for which new tag would be created
375 :param message: message of the tag's commit
375 :param message: message of the tag's commit
376 :param date: date of tag's commit
376 :param date: date of tag's commit
377
377
378 :raises TagAlreadyExistError: if tag with same name already exists
378 :raises TagAlreadyExistError: if tag with same name already exists
379 """
379 """
380 raise NotImplementedError
380 raise NotImplementedError
381
381
382 def remove_tag(self, name, user, message=None, date=None):
382 def remove_tag(self, name, user, message=None, date=None):
383 """
383 """
384 Removes tag with the given ``name``.
384 Removes tag with the given ``name``.
385
385
386 :param name: name of the tag to be removed
386 :param name: name of the tag to be removed
387 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
387 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
388 :param message: message of the tag's removal commit
388 :param message: message of the tag's removal commit
389 :param date: date of tag's removal commit
389 :param date: date of tag's removal commit
390
390
391 :raises TagDoesNotExistError: if tag with given name does not exists
391 :raises TagDoesNotExistError: if tag with given name does not exists
392 """
392 """
393 raise NotImplementedError
393 raise NotImplementedError
394
394
395 def get_diff(
395 def get_diff(
396 self, commit1, commit2, path=None, ignore_whitespace=False,
396 self, commit1, commit2, path=None, ignore_whitespace=False,
397 context=3, path1=None):
397 context=3, path1=None):
398 """
398 """
399 Returns (git like) *diff*, as plain text. Shows changes introduced by
399 Returns (git like) *diff*, as plain text. Shows changes introduced by
400 `commit2` since `commit1`.
400 `commit2` since `commit1`.
401
401
402 :param commit1: Entry point from which diff is shown. Can be
402 :param commit1: Entry point from which diff is shown. Can be
403 ``self.EMPTY_COMMIT`` - in this case, patch showing all
403 ``self.EMPTY_COMMIT`` - in this case, patch showing all
404 the changes since empty state of the repository until `commit2`
404 the changes since empty state of the repository until `commit2`
405 :param commit2: Until which commit changes should be shown.
405 :param commit2: Until which commit changes should be shown.
406 :param path: Can be set to a path of a file to create a diff of that
406 :param path: Can be set to a path of a file to create a diff of that
407 file. If `path1` is also set, this value is only associated to
407 file. If `path1` is also set, this value is only associated to
408 `commit2`.
408 `commit2`.
409 :param ignore_whitespace: If set to ``True``, would not show whitespace
409 :param ignore_whitespace: If set to ``True``, would not show whitespace
410 changes. Defaults to ``False``.
410 changes. Defaults to ``False``.
411 :param context: How many lines before/after changed lines should be
411 :param context: How many lines before/after changed lines should be
412 shown. Defaults to ``3``.
412 shown. Defaults to ``3``.
413 :param path1: Can be set to a path to associate with `commit1`. This
413 :param path1: Can be set to a path to associate with `commit1`. This
414 parameter works only for backends which support diff generation for
414 parameter works only for backends which support diff generation for
415 different paths. Other backends will raise a `ValueError` if `path1`
415 different paths. Other backends will raise a `ValueError` if `path1`
416 is set and has a different value than `path`.
416 is set and has a different value than `path`.
417 :param file_path: filter this diff by given path pattern
417 :param file_path: filter this diff by given path pattern
418 """
418 """
419 raise NotImplementedError
419 raise NotImplementedError
420
420
421 def strip(self, commit_id, branch=None):
421 def strip(self, commit_id, branch=None):
422 """
422 """
423 Strip given commit_id from the repository
423 Strip given commit_id from the repository
424 """
424 """
425 raise NotImplementedError
425 raise NotImplementedError
426
426
427 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
427 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
428 """
428 """
429 Return a latest common ancestor commit if one exists for this repo
429 Return a latest common ancestor commit if one exists for this repo
430 `commit_id1` vs `commit_id2` from `repo2`.
430 `commit_id1` vs `commit_id2` from `repo2`.
431
431
432 :param commit_id1: Commit it from this repository to use as a
432 :param commit_id1: Commit it from this repository to use as a
433 target for the comparison.
433 target for the comparison.
434 :param commit_id2: Source commit id to use for comparison.
434 :param commit_id2: Source commit id to use for comparison.
435 :param repo2: Source repository to use for comparison.
435 :param repo2: Source repository to use for comparison.
436 """
436 """
437 raise NotImplementedError
437 raise NotImplementedError
438
438
439 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
439 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
440 """
440 """
441 Compare this repository's revision `commit_id1` with `commit_id2`.
441 Compare this repository's revision `commit_id1` with `commit_id2`.
442
442
443 Returns a tuple(commits, ancestor) that would be merged from
443 Returns a tuple(commits, ancestor) that would be merged from
444 `commit_id2`. Doing a normal compare (``merge=False``), ``None``
444 `commit_id2`. Doing a normal compare (``merge=False``), ``None``
445 will be returned as ancestor.
445 will be returned as ancestor.
446
446
447 :param commit_id1: Commit it from this repository to use as a
447 :param commit_id1: Commit it from this repository to use as a
448 target for the comparison.
448 target for the comparison.
449 :param commit_id2: Source commit id to use for comparison.
449 :param commit_id2: Source commit id to use for comparison.
450 :param repo2: Source repository to use for comparison.
450 :param repo2: Source repository to use for comparison.
451 :param merge: If set to ``True`` will do a merge compare which also
451 :param merge: If set to ``True`` will do a merge compare which also
452 returns the common ancestor.
452 returns the common ancestor.
453 :param pre_load: Optional. List of commit attributes to load.
453 :param pre_load: Optional. List of commit attributes to load.
454 """
454 """
455 raise NotImplementedError
455 raise NotImplementedError
456
456
457 def merge(self, repo_id, workspace_id, target_ref, source_repo, source_ref,
457 def merge(self, repo_id, workspace_id, target_ref, source_repo, source_ref,
458 user_name='', user_email='', message='', dry_run=False,
458 user_name='', user_email='', message='', dry_run=False,
459 use_rebase=False, close_branch=False):
459 use_rebase=False, close_branch=False):
460 """
460 """
461 Merge the revisions specified in `source_ref` from `source_repo`
461 Merge the revisions specified in `source_ref` from `source_repo`
462 onto the `target_ref` of this repository.
462 onto the `target_ref` of this repository.
463
463
464 `source_ref` and `target_ref` are named tupls with the following
464 `source_ref` and `target_ref` are named tupls with the following
465 fields `type`, `name` and `commit_id`.
465 fields `type`, `name` and `commit_id`.
466
466
467 Returns a MergeResponse named tuple with the following fields
467 Returns a MergeResponse named tuple with the following fields
468 'possible', 'executed', 'source_commit', 'target_commit',
468 'possible', 'executed', 'source_commit', 'target_commit',
469 'merge_commit'.
469 'merge_commit'.
470
470
471 :param repo_id: `repo_id` target repo id.
471 :param repo_id: `repo_id` target repo id.
472 :param workspace_id: `workspace_id` unique identifier.
472 :param workspace_id: `workspace_id` unique identifier.
473 :param target_ref: `target_ref` points to the commit on top of which
473 :param target_ref: `target_ref` points to the commit on top of which
474 the `source_ref` should be merged.
474 the `source_ref` should be merged.
475 :param source_repo: The repository that contains the commits to be
475 :param source_repo: The repository that contains the commits to be
476 merged.
476 merged.
477 :param source_ref: `source_ref` points to the topmost commit from
477 :param source_ref: `source_ref` points to the topmost commit from
478 the `source_repo` which should be merged.
478 the `source_repo` which should be merged.
479 :param user_name: Merge commit `user_name`.
479 :param user_name: Merge commit `user_name`.
480 :param user_email: Merge commit `user_email`.
480 :param user_email: Merge commit `user_email`.
481 :param message: Merge commit `message`.
481 :param message: Merge commit `message`.
482 :param dry_run: If `True` the merge will not take place.
482 :param dry_run: If `True` the merge will not take place.
483 :param use_rebase: If `True` commits from the source will be rebased
483 :param use_rebase: If `True` commits from the source will be rebased
484 on top of the target instead of being merged.
484 on top of the target instead of being merged.
485 :param close_branch: If `True` branch will be close before merging it
485 :param close_branch: If `True` branch will be close before merging it
486 """
486 """
487 if dry_run:
487 if dry_run:
488 message = message or settings.MERGE_DRY_RUN_MESSAGE
488 message = message or settings.MERGE_DRY_RUN_MESSAGE
489 user_email = user_email or settings.MERGE_DRY_RUN_EMAIL
489 user_email = user_email or settings.MERGE_DRY_RUN_EMAIL
490 user_name = user_name or settings.MERGE_DRY_RUN_USER
490 user_name = user_name or settings.MERGE_DRY_RUN_USER
491 else:
491 else:
492 if not user_name:
492 if not user_name:
493 raise ValueError('user_name cannot be empty')
493 raise ValueError('user_name cannot be empty')
494 if not user_email:
494 if not user_email:
495 raise ValueError('user_email cannot be empty')
495 raise ValueError('user_email cannot be empty')
496 if not message:
496 if not message:
497 raise ValueError('message cannot be empty')
497 raise ValueError('message cannot be empty')
498
498
499 try:
499 try:
500 return self._merge_repo(
500 return self._merge_repo(
501 repo_id, workspace_id, target_ref, source_repo,
501 repo_id, workspace_id, target_ref, source_repo,
502 source_ref, message, user_name, user_email, dry_run=dry_run,
502 source_ref, message, user_name, user_email, dry_run=dry_run,
503 use_rebase=use_rebase, close_branch=close_branch)
503 use_rebase=use_rebase, close_branch=close_branch)
504 except RepositoryError:
504 except RepositoryError:
505 log.exception(
505 log.exception(
506 'Unexpected failure when running merge, dry-run=%s',
506 'Unexpected failure when running merge, dry-run=%s',
507 dry_run)
507 dry_run)
508 return MergeResponse(
508 return MergeResponse(
509 False, False, None, MergeFailureReason.UNKNOWN)
509 False, False, None, MergeFailureReason.UNKNOWN)
510
510
511 def _merge_repo(self, repo_id, workspace_id, target_ref,
511 def _merge_repo(self, repo_id, workspace_id, target_ref,
512 source_repo, source_ref, merge_message,
512 source_repo, source_ref, merge_message,
513 merger_name, merger_email, dry_run=False,
513 merger_name, merger_email, dry_run=False,
514 use_rebase=False, close_branch=False):
514 use_rebase=False, close_branch=False):
515 """Internal implementation of merge."""
515 """Internal implementation of merge."""
516 raise NotImplementedError
516 raise NotImplementedError
517
517
518 def _maybe_prepare_merge_workspace(
518 def _maybe_prepare_merge_workspace(
519 self, repo_id, workspace_id, target_ref, source_ref):
519 self, repo_id, workspace_id, target_ref, source_ref):
520 """
520 """
521 Create the merge workspace.
521 Create the merge workspace.
522
522
523 :param workspace_id: `workspace_id` unique identifier.
523 :param workspace_id: `workspace_id` unique identifier.
524 """
524 """
525 raise NotImplementedError
525 raise NotImplementedError
526
526
527 def _get_legacy_shadow_repository_path(self, workspace_id):
527 def _get_legacy_shadow_repository_path(self, workspace_id):
528 """
528 """
529 Legacy version that was used before. We still need it for
529 Legacy version that was used before. We still need it for
530 backward compat
530 backward compat
531 """
531 """
532 return os.path.join(
532 return os.path.join(
533 os.path.dirname(self.path),
533 os.path.dirname(self.path),
534 '.__shadow_%s_%s' % (os.path.basename(self.path), workspace_id))
534 '.__shadow_%s_%s' % (os.path.basename(self.path), workspace_id))
535
535
536 def _get_shadow_repository_path(self, repo_id, workspace_id):
536 def _get_shadow_repository_path(self, repo_id, workspace_id):
537 # The name of the shadow repository must start with '.', so it is
537 # The name of the shadow repository must start with '.', so it is
538 # skipped by 'rhodecode.lib.utils.get_filesystem_repos'.
538 # skipped by 'rhodecode.lib.utils.get_filesystem_repos'.
539 legacy_repository_path = self._get_legacy_shadow_repository_path(workspace_id)
539 legacy_repository_path = self._get_legacy_shadow_repository_path(workspace_id)
540 if os.path.exists(legacy_repository_path):
540 if os.path.exists(legacy_repository_path):
541 return legacy_repository_path
541 return legacy_repository_path
542 else:
542 else:
543 return os.path.join(
543 return os.path.join(
544 os.path.dirname(self.path),
544 os.path.dirname(self.path),
545 '.__shadow_repo_%s_%s' % (repo_id, workspace_id))
545 '.__shadow_repo_%s_%s' % (repo_id, workspace_id))
546
546
547 def cleanup_merge_workspace(self, repo_id, workspace_id):
547 def cleanup_merge_workspace(self, repo_id, workspace_id):
548 """
548 """
549 Remove merge workspace.
549 Remove merge workspace.
550
550
551 This function MUST not fail in case there is no workspace associated to
551 This function MUST not fail in case there is no workspace associated to
552 the given `workspace_id`.
552 the given `workspace_id`.
553
553
554 :param workspace_id: `workspace_id` unique identifier.
554 :param workspace_id: `workspace_id` unique identifier.
555 """
555 """
556 shadow_repository_path = self._get_shadow_repository_path(repo_id, workspace_id)
556 shadow_repository_path = self._get_shadow_repository_path(repo_id, workspace_id)
557 shadow_repository_path_del = '{}.{}.delete'.format(
557 shadow_repository_path_del = '{}.{}.delete'.format(
558 shadow_repository_path, time.time())
558 shadow_repository_path, time.time())
559
559
560 # move the shadow repo, so it never conflicts with the one used.
560 # move the shadow repo, so it never conflicts with the one used.
561 # we use this method because shutil.rmtree had some edge case problems
561 # we use this method because shutil.rmtree had some edge case problems
562 # removing symlinked repositories
562 # removing symlinked repositories
563 if not os.path.isdir(shadow_repository_path):
563 if not os.path.isdir(shadow_repository_path):
564 return
564 return
565
565
566 shutil.move(shadow_repository_path, shadow_repository_path_del)
566 shutil.move(shadow_repository_path, shadow_repository_path_del)
567 try:
567 try:
568 shutil.rmtree(shadow_repository_path_del, ignore_errors=False)
568 shutil.rmtree(shadow_repository_path_del, ignore_errors=False)
569 except Exception:
569 except Exception:
570 log.exception('Failed to gracefully remove shadow repo under %s',
570 log.exception('Failed to gracefully remove shadow repo under %s',
571 shadow_repository_path_del)
571 shadow_repository_path_del)
572 shutil.rmtree(shadow_repository_path_del, ignore_errors=True)
572 shutil.rmtree(shadow_repository_path_del, ignore_errors=True)
573
573
574 # ========== #
574 # ========== #
575 # COMMIT API #
575 # COMMIT API #
576 # ========== #
576 # ========== #
577
577
578 @LazyProperty
578 @LazyProperty
579 def in_memory_commit(self):
579 def in_memory_commit(self):
580 """
580 """
581 Returns :class:`InMemoryCommit` object for this repository.
581 Returns :class:`InMemoryCommit` object for this repository.
582 """
582 """
583 raise NotImplementedError
583 raise NotImplementedError
584
584
585 # ======================== #
585 # ======================== #
586 # UTILITIES FOR SUBCLASSES #
586 # UTILITIES FOR SUBCLASSES #
587 # ======================== #
587 # ======================== #
588
588
589 def _validate_diff_commits(self, commit1, commit2):
589 def _validate_diff_commits(self, commit1, commit2):
590 """
590 """
591 Validates that the given commits are related to this repository.
591 Validates that the given commits are related to this repository.
592
592
593 Intended as a utility for sub classes to have a consistent validation
593 Intended as a utility for sub classes to have a consistent validation
594 of input parameters in methods like :meth:`get_diff`.
594 of input parameters in methods like :meth:`get_diff`.
595 """
595 """
596 self._validate_commit(commit1)
596 self._validate_commit(commit1)
597 self._validate_commit(commit2)
597 self._validate_commit(commit2)
598 if (isinstance(commit1, EmptyCommit) and
598 if (isinstance(commit1, EmptyCommit) and
599 isinstance(commit2, EmptyCommit)):
599 isinstance(commit2, EmptyCommit)):
600 raise ValueError("Cannot compare two empty commits")
600 raise ValueError("Cannot compare two empty commits")
601
601
602 def _validate_commit(self, commit):
602 def _validate_commit(self, commit):
603 if not isinstance(commit, BaseCommit):
603 if not isinstance(commit, BaseCommit):
604 raise TypeError(
604 raise TypeError(
605 "%s is not of type BaseCommit" % repr(commit))
605 "%s is not of type BaseCommit" % repr(commit))
606 if commit.repository != self and not isinstance(commit, EmptyCommit):
606 if commit.repository != self and not isinstance(commit, EmptyCommit):
607 raise ValueError(
607 raise ValueError(
608 "Commit %s must be a valid commit from this repository %s, "
608 "Commit %s must be a valid commit from this repository %s, "
609 "related to this repository instead %s." %
609 "related to this repository instead %s." %
610 (commit, self, commit.repository))
610 (commit, self, commit.repository))
611
611
612 def _validate_commit_id(self, commit_id):
612 def _validate_commit_id(self, commit_id):
613 if not isinstance(commit_id, basestring):
613 if not isinstance(commit_id, basestring):
614 raise TypeError("commit_id must be a string value")
614 raise TypeError("commit_id must be a string value")
615
615
616 def _validate_commit_idx(self, commit_idx):
616 def _validate_commit_idx(self, commit_idx):
617 if not isinstance(commit_idx, (int, long)):
617 if not isinstance(commit_idx, (int, long)):
618 raise TypeError("commit_idx must be a numeric value")
618 raise TypeError("commit_idx must be a numeric value")
619
619
620 def _validate_branch_name(self, branch_name):
620 def _validate_branch_name(self, branch_name):
621 if branch_name and branch_name not in self.branches_all:
621 if branch_name and branch_name not in self.branches_all:
622 msg = ("Branch %s not found in %s" % (branch_name, self))
622 msg = ("Branch %s not found in %s" % (branch_name, self))
623 raise BranchDoesNotExistError(msg)
623 raise BranchDoesNotExistError(msg)
624
624
625 #
625 #
626 # Supporting deprecated API parts
626 # Supporting deprecated API parts
627 # TODO: johbo: consider to move this into a mixin
627 # TODO: johbo: consider to move this into a mixin
628 #
628 #
629
629
630 @property
630 @property
631 def EMPTY_CHANGESET(self):
631 def EMPTY_CHANGESET(self):
632 warnings.warn(
632 warnings.warn(
633 "Use EMPTY_COMMIT or EMPTY_COMMIT_ID instead", DeprecationWarning)
633 "Use EMPTY_COMMIT or EMPTY_COMMIT_ID instead", DeprecationWarning)
634 return self.EMPTY_COMMIT_ID
634 return self.EMPTY_COMMIT_ID
635
635
636 @property
636 @property
637 def revisions(self):
637 def revisions(self):
638 warnings.warn("Use commits attribute instead", DeprecationWarning)
638 warnings.warn("Use commits attribute instead", DeprecationWarning)
639 return self.commit_ids
639 return self.commit_ids
640
640
641 @revisions.setter
641 @revisions.setter
642 def revisions(self, value):
642 def revisions(self, value):
643 warnings.warn("Use commits attribute instead", DeprecationWarning)
643 warnings.warn("Use commits attribute instead", DeprecationWarning)
644 self.commit_ids = value
644 self.commit_ids = value
645
645
646 def get_changeset(self, revision=None, pre_load=None):
646 def get_changeset(self, revision=None, pre_load=None):
647 warnings.warn("Use get_commit instead", DeprecationWarning)
647 warnings.warn("Use get_commit instead", DeprecationWarning)
648 commit_id = None
648 commit_id = None
649 commit_idx = None
649 commit_idx = None
650 if isinstance(revision, basestring):
650 if isinstance(revision, basestring):
651 commit_id = revision
651 commit_id = revision
652 else:
652 else:
653 commit_idx = revision
653 commit_idx = revision
654 return self.get_commit(
654 return self.get_commit(
655 commit_id=commit_id, commit_idx=commit_idx, pre_load=pre_load)
655 commit_id=commit_id, commit_idx=commit_idx, pre_load=pre_load)
656
656
657 def get_changesets(
657 def get_changesets(
658 self, start=None, end=None, start_date=None, end_date=None,
658 self, start=None, end=None, start_date=None, end_date=None,
659 branch_name=None, pre_load=None):
659 branch_name=None, pre_load=None):
660 warnings.warn("Use get_commits instead", DeprecationWarning)
660 warnings.warn("Use get_commits instead", DeprecationWarning)
661 start_id = self._revision_to_commit(start)
661 start_id = self._revision_to_commit(start)
662 end_id = self._revision_to_commit(end)
662 end_id = self._revision_to_commit(end)
663 return self.get_commits(
663 return self.get_commits(
664 start_id=start_id, end_id=end_id, start_date=start_date,
664 start_id=start_id, end_id=end_id, start_date=start_date,
665 end_date=end_date, branch_name=branch_name, pre_load=pre_load)
665 end_date=end_date, branch_name=branch_name, pre_load=pre_load)
666
666
667 def _revision_to_commit(self, revision):
667 def _revision_to_commit(self, revision):
668 """
668 """
669 Translates a revision to a commit_id
669 Translates a revision to a commit_id
670
670
671 Helps to support the old changeset based API which allows to use
671 Helps to support the old changeset based API which allows to use
672 commit ids and commit indices interchangeable.
672 commit ids and commit indices interchangeable.
673 """
673 """
674 if revision is None:
674 if revision is None:
675 return revision
675 return revision
676
676
677 if isinstance(revision, basestring):
677 if isinstance(revision, basestring):
678 commit_id = revision
678 commit_id = revision
679 else:
679 else:
680 commit_id = self.commit_ids[revision]
680 commit_id = self.commit_ids[revision]
681 return commit_id
681 return commit_id
682
682
683 @property
683 @property
684 def in_memory_changeset(self):
684 def in_memory_changeset(self):
685 warnings.warn("Use in_memory_commit instead", DeprecationWarning)
685 warnings.warn("Use in_memory_commit instead", DeprecationWarning)
686 return self.in_memory_commit
686 return self.in_memory_commit
687
687
688 def get_path_permissions(self, username):
688 def get_path_permissions(self, username):
689 """
689 """
690 Returns a path permission checker or None if not supported
690 Returns a path permission checker or None if not supported
691
691
692 :param username: session user name
692 :param username: session user name
693 :return: an instance of BasePathPermissionChecker or None
693 :return: an instance of BasePathPermissionChecker or None
694 """
694 """
695 return None
695 return None
696
696
697 def install_hooks(self, force=False):
697 def install_hooks(self, force=False):
698 return self._remote.install_hooks(force)
698 return self._remote.install_hooks(force)
699
699
700
700
701 class BaseCommit(object):
701 class BaseCommit(object):
702 """
702 """
703 Each backend should implement it's commit representation.
703 Each backend should implement it's commit representation.
704
704
705 **Attributes**
705 **Attributes**
706
706
707 ``repository``
707 ``repository``
708 repository object within which commit exists
708 repository object within which commit exists
709
709
710 ``id``
710 ``id``
711 The commit id, may be ``raw_id`` or i.e. for mercurial's tip
711 The commit id, may be ``raw_id`` or i.e. for mercurial's tip
712 just ``tip``.
712 just ``tip``.
713
713
714 ``raw_id``
714 ``raw_id``
715 raw commit representation (i.e. full 40 length sha for git
715 raw commit representation (i.e. full 40 length sha for git
716 backend)
716 backend)
717
717
718 ``short_id``
718 ``short_id``
719 shortened (if apply) version of ``raw_id``; it would be simple
719 shortened (if apply) version of ``raw_id``; it would be simple
720 shortcut for ``raw_id[:12]`` for git/mercurial backends or same
720 shortcut for ``raw_id[:12]`` for git/mercurial backends or same
721 as ``raw_id`` for subversion
721 as ``raw_id`` for subversion
722
722
723 ``idx``
723 ``idx``
724 commit index
724 commit index
725
725
726 ``files``
726 ``files``
727 list of ``FileNode`` (``Node`` with NodeKind.FILE) objects
727 list of ``FileNode`` (``Node`` with NodeKind.FILE) objects
728
728
729 ``dirs``
729 ``dirs``
730 list of ``DirNode`` (``Node`` with NodeKind.DIR) objects
730 list of ``DirNode`` (``Node`` with NodeKind.DIR) objects
731
731
732 ``nodes``
732 ``nodes``
733 combined list of ``Node`` objects
733 combined list of ``Node`` objects
734
734
735 ``author``
735 ``author``
736 author of the commit, as unicode
736 author of the commit, as unicode
737
737
738 ``message``
738 ``message``
739 message of the commit, as unicode
739 message of the commit, as unicode
740
740
741 ``parents``
741 ``parents``
742 list of parent commits
742 list of parent commits
743
743
744 """
744 """
745
745
746 branch = None
746 branch = None
747 """
747 """
748 Depending on the backend this should be set to the branch name of the
748 Depending on the backend this should be set to the branch name of the
749 commit. Backends not supporting branches on commits should leave this
749 commit. Backends not supporting branches on commits should leave this
750 value as ``None``.
750 value as ``None``.
751 """
751 """
752
752
753 _ARCHIVE_PREFIX_TEMPLATE = b'{repo_name}-{short_id}'
753 _ARCHIVE_PREFIX_TEMPLATE = b'{repo_name}-{short_id}'
754 """
754 """
755 This template is used to generate a default prefix for repository archives
755 This template is used to generate a default prefix for repository archives
756 if no prefix has been specified.
756 if no prefix has been specified.
757 """
757 """
758
758
759 def __str__(self):
759 def __str__(self):
760 return '<%s at %s:%s>' % (
760 return '<%s at %s:%s>' % (
761 self.__class__.__name__, self.idx, self.short_id)
761 self.__class__.__name__, self.idx, self.short_id)
762
762
763 def __repr__(self):
763 def __repr__(self):
764 return self.__str__()
764 return self.__str__()
765
765
766 def __unicode__(self):
766 def __unicode__(self):
767 return u'%s:%s' % (self.idx, self.short_id)
767 return u'%s:%s' % (self.idx, self.short_id)
768
768
769 def __eq__(self, other):
769 def __eq__(self, other):
770 same_instance = isinstance(other, self.__class__)
770 same_instance = isinstance(other, self.__class__)
771 return same_instance and self.raw_id == other.raw_id
771 return same_instance and self.raw_id == other.raw_id
772
772
773 def __json__(self):
773 def __json__(self):
774 parents = []
774 parents = []
775 try:
775 try:
776 for parent in self.parents:
776 for parent in self.parents:
777 parents.append({'raw_id': parent.raw_id})
777 parents.append({'raw_id': parent.raw_id})
778 except NotImplementedError:
778 except NotImplementedError:
779 # empty commit doesn't have parents implemented
779 # empty commit doesn't have parents implemented
780 pass
780 pass
781
781
782 return {
782 return {
783 'short_id': self.short_id,
783 'short_id': self.short_id,
784 'raw_id': self.raw_id,
784 'raw_id': self.raw_id,
785 'revision': self.idx,
785 'revision': self.idx,
786 'message': self.message,
786 'message': self.message,
787 'date': self.date,
787 'date': self.date,
788 'author': self.author,
788 'author': self.author,
789 'parents': parents,
789 'parents': parents,
790 'branch': self.branch
790 'branch': self.branch
791 }
791 }
792
792
793 def __getstate__(self):
793 def __getstate__(self):
794 d = self.__dict__.copy()
794 d = self.__dict__.copy()
795 d.pop('_remote', None)
795 d.pop('_remote', None)
796 d.pop('repository', None)
796 d.pop('repository', None)
797 return d
797 return d
798
798
799 def _get_refs(self):
799 def _get_refs(self):
800 return {
800 return {
801 'branches': [self.branch] if self.branch else [],
801 'branches': [self.branch] if self.branch else [],
802 'bookmarks': getattr(self, 'bookmarks', []),
802 'bookmarks': getattr(self, 'bookmarks', []),
803 'tags': self.tags
803 'tags': self.tags
804 }
804 }
805
805
806 @LazyProperty
806 @LazyProperty
807 def last(self):
807 def last(self):
808 """
808 """
809 ``True`` if this is last commit in repository, ``False``
809 ``True`` if this is last commit in repository, ``False``
810 otherwise; trying to access this attribute while there is no
810 otherwise; trying to access this attribute while there is no
811 commits would raise `EmptyRepositoryError`
811 commits would raise `EmptyRepositoryError`
812 """
812 """
813 if self.repository is None:
813 if self.repository is None:
814 raise CommitError("Cannot check if it's most recent commit")
814 raise CommitError("Cannot check if it's most recent commit")
815 return self.raw_id == self.repository.commit_ids[-1]
815 return self.raw_id == self.repository.commit_ids[-1]
816
816
817 @LazyProperty
817 @LazyProperty
818 def parents(self):
818 def parents(self):
819 """
819 """
820 Returns list of parent commits.
820 Returns list of parent commits.
821 """
821 """
822 raise NotImplementedError
822 raise NotImplementedError
823
823
824 @LazyProperty
824 @LazyProperty
825 def first_parent(self):
825 def first_parent(self):
826 """
826 """
827 Returns list of parent commits.
827 Returns list of parent commits.
828 """
828 """
829 return self.parents[0] if self.parents else EmptyCommit()
829 return self.parents[0] if self.parents else EmptyCommit()
830
830
831 @property
831 @property
832 def merge(self):
832 def merge(self):
833 """
833 """
834 Returns boolean if commit is a merge.
834 Returns boolean if commit is a merge.
835 """
835 """
836 return len(self.parents) > 1
836 return len(self.parents) > 1
837
837
838 @LazyProperty
838 @LazyProperty
839 def children(self):
839 def children(self):
840 """
840 """
841 Returns list of child commits.
841 Returns list of child commits.
842 """
842 """
843 raise NotImplementedError
843 raise NotImplementedError
844
844
845 @LazyProperty
845 @LazyProperty
846 def id(self):
846 def id(self):
847 """
847 """
848 Returns string identifying this commit.
848 Returns string identifying this commit.
849 """
849 """
850 raise NotImplementedError
850 raise NotImplementedError
851
851
852 @LazyProperty
852 @LazyProperty
853 def raw_id(self):
853 def raw_id(self):
854 """
854 """
855 Returns raw string identifying this commit.
855 Returns raw string identifying this commit.
856 """
856 """
857 raise NotImplementedError
857 raise NotImplementedError
858
858
859 @LazyProperty
859 @LazyProperty
860 def short_id(self):
860 def short_id(self):
861 """
861 """
862 Returns shortened version of ``raw_id`` attribute, as string,
862 Returns shortened version of ``raw_id`` attribute, as string,
863 identifying this commit, useful for presentation to users.
863 identifying this commit, useful for presentation to users.
864 """
864 """
865 raise NotImplementedError
865 raise NotImplementedError
866
866
867 @LazyProperty
867 @LazyProperty
868 def idx(self):
868 def idx(self):
869 """
869 """
870 Returns integer identifying this commit.
870 Returns integer identifying this commit.
871 """
871 """
872 raise NotImplementedError
872 raise NotImplementedError
873
873
874 @LazyProperty
874 @LazyProperty
875 def committer(self):
875 def committer(self):
876 """
876 """
877 Returns committer for this commit
877 Returns committer for this commit
878 """
878 """
879 raise NotImplementedError
879 raise NotImplementedError
880
880
881 @LazyProperty
881 @LazyProperty
882 def committer_name(self):
882 def committer_name(self):
883 """
883 """
884 Returns committer name for this commit
884 Returns committer name for this commit
885 """
885 """
886
886
887 return author_name(self.committer)
887 return author_name(self.committer)
888
888
889 @LazyProperty
889 @LazyProperty
890 def committer_email(self):
890 def committer_email(self):
891 """
891 """
892 Returns committer email address for this commit
892 Returns committer email address for this commit
893 """
893 """
894
894
895 return author_email(self.committer)
895 return author_email(self.committer)
896
896
897 @LazyProperty
897 @LazyProperty
898 def author(self):
898 def author(self):
899 """
899 """
900 Returns author for this commit
900 Returns author for this commit
901 """
901 """
902
902
903 raise NotImplementedError
903 raise NotImplementedError
904
904
905 @LazyProperty
905 @LazyProperty
906 def author_name(self):
906 def author_name(self):
907 """
907 """
908 Returns author name for this commit
908 Returns author name for this commit
909 """
909 """
910
910
911 return author_name(self.author)
911 return author_name(self.author)
912
912
913 @LazyProperty
913 @LazyProperty
914 def author_email(self):
914 def author_email(self):
915 """
915 """
916 Returns author email address for this commit
916 Returns author email address for this commit
917 """
917 """
918
918
919 return author_email(self.author)
919 return author_email(self.author)
920
920
921 def get_file_mode(self, path):
921 def get_file_mode(self, path):
922 """
922 """
923 Returns stat mode of the file at `path`.
923 Returns stat mode of the file at `path`.
924 """
924 """
925 raise NotImplementedError
925 raise NotImplementedError
926
926
927 def is_link(self, path):
927 def is_link(self, path):
928 """
928 """
929 Returns ``True`` if given `path` is a symlink
929 Returns ``True`` if given `path` is a symlink
930 """
930 """
931 raise NotImplementedError
931 raise NotImplementedError
932
932
933 def get_file_content(self, path):
933 def get_file_content(self, path):
934 """
934 """
935 Returns content of the file at the given `path`.
935 Returns content of the file at the given `path`.
936 """
936 """
937 raise NotImplementedError
937 raise NotImplementedError
938
938
939 def get_file_size(self, path):
939 def get_file_size(self, path):
940 """
940 """
941 Returns size of the file at the given `path`.
941 Returns size of the file at the given `path`.
942 """
942 """
943 raise NotImplementedError
943 raise NotImplementedError
944
944
945 def get_file_commit(self, path, pre_load=None):
945 def get_path_commit(self, path, pre_load=None):
946 """
946 """
947 Returns last commit of the file at the given `path`.
947 Returns last commit of the file at the given `path`.
948
948
949 :param pre_load: Optional. List of commit attributes to load.
949 :param pre_load: Optional. List of commit attributes to load.
950 """
950 """
951 commits = self.get_file_history(path, limit=1, pre_load=pre_load)
951 commits = self.get_path_history(path, limit=1, pre_load=pre_load)
952 if not commits:
952 if not commits:
953 raise RepositoryError(
953 raise RepositoryError(
954 'Failed to fetch history for path {}. '
954 'Failed to fetch history for path {}. '
955 'Please check if such path exists in your repository'.format(
955 'Please check if such path exists in your repository'.format(
956 path))
956 path))
957 return commits[0]
957 return commits[0]
958
958
959 def get_file_history(self, path, limit=None, pre_load=None):
959 def get_path_history(self, path, limit=None, pre_load=None):
960 """
960 """
961 Returns history of file as reversed list of :class:`BaseCommit`
961 Returns history of file as reversed list of :class:`BaseCommit`
962 objects for which file at given `path` has been modified.
962 objects for which file at given `path` has been modified.
963
963
964 :param limit: Optional. Allows to limit the size of the returned
964 :param limit: Optional. Allows to limit the size of the returned
965 history. This is intended as a hint to the underlying backend, so
965 history. This is intended as a hint to the underlying backend, so
966 that it can apply optimizations depending on the limit.
966 that it can apply optimizations depending on the limit.
967 :param pre_load: Optional. List of commit attributes to load.
967 :param pre_load: Optional. List of commit attributes to load.
968 """
968 """
969 raise NotImplementedError
969 raise NotImplementedError
970
970
971 def get_file_annotate(self, path, pre_load=None):
971 def get_file_annotate(self, path, pre_load=None):
972 """
972 """
973 Returns a generator of four element tuples with
973 Returns a generator of four element tuples with
974 lineno, sha, commit lazy loader and line
974 lineno, sha, commit lazy loader and line
975
975
976 :param pre_load: Optional. List of commit attributes to load.
976 :param pre_load: Optional. List of commit attributes to load.
977 """
977 """
978 raise NotImplementedError
978 raise NotImplementedError
979
979
980 def get_nodes(self, path):
980 def get_nodes(self, path):
981 """
981 """
982 Returns combined ``DirNode`` and ``FileNode`` objects list representing
982 Returns combined ``DirNode`` and ``FileNode`` objects list representing
983 state of commit at the given ``path``.
983 state of commit at the given ``path``.
984
984
985 :raises ``CommitError``: if node at the given ``path`` is not
985 :raises ``CommitError``: if node at the given ``path`` is not
986 instance of ``DirNode``
986 instance of ``DirNode``
987 """
987 """
988 raise NotImplementedError
988 raise NotImplementedError
989
989
990 def get_node(self, path):
990 def get_node(self, path):
991 """
991 """
992 Returns ``Node`` object from the given ``path``.
992 Returns ``Node`` object from the given ``path``.
993
993
994 :raises ``NodeDoesNotExistError``: if there is no node at the given
994 :raises ``NodeDoesNotExistError``: if there is no node at the given
995 ``path``
995 ``path``
996 """
996 """
997 raise NotImplementedError
997 raise NotImplementedError
998
998
999 def get_largefile_node(self, path):
999 def get_largefile_node(self, path):
1000 """
1000 """
1001 Returns the path to largefile from Mercurial/Git-lfs storage.
1001 Returns the path to largefile from Mercurial/Git-lfs storage.
1002 or None if it's not a largefile node
1002 or None if it's not a largefile node
1003 """
1003 """
1004 return None
1004 return None
1005
1005
1006 def archive_repo(self, file_path, kind='tgz', subrepos=None,
1006 def archive_repo(self, file_path, kind='tgz', subrepos=None,
1007 prefix=None, write_metadata=False, mtime=None):
1007 prefix=None, write_metadata=False, mtime=None):
1008 """
1008 """
1009 Creates an archive containing the contents of the repository.
1009 Creates an archive containing the contents of the repository.
1010
1010
1011 :param file_path: path to the file which to create the archive.
1011 :param file_path: path to the file which to create the archive.
1012 :param kind: one of following: ``"tbz2"``, ``"tgz"``, ``"zip"``.
1012 :param kind: one of following: ``"tbz2"``, ``"tgz"``, ``"zip"``.
1013 :param prefix: name of root directory in archive.
1013 :param prefix: name of root directory in archive.
1014 Default is repository name and commit's short_id joined with dash:
1014 Default is repository name and commit's short_id joined with dash:
1015 ``"{repo_name}-{short_id}"``.
1015 ``"{repo_name}-{short_id}"``.
1016 :param write_metadata: write a metadata file into archive.
1016 :param write_metadata: write a metadata file into archive.
1017 :param mtime: custom modification time for archive creation, defaults
1017 :param mtime: custom modification time for archive creation, defaults
1018 to time.time() if not given.
1018 to time.time() if not given.
1019
1019
1020 :raise VCSError: If prefix has a problem.
1020 :raise VCSError: If prefix has a problem.
1021 """
1021 """
1022 allowed_kinds = settings.ARCHIVE_SPECS.keys()
1022 allowed_kinds = settings.ARCHIVE_SPECS.keys()
1023 if kind not in allowed_kinds:
1023 if kind not in allowed_kinds:
1024 raise ImproperArchiveTypeError(
1024 raise ImproperArchiveTypeError(
1025 'Archive kind (%s) not supported use one of %s' %
1025 'Archive kind (%s) not supported use one of %s' %
1026 (kind, allowed_kinds))
1026 (kind, allowed_kinds))
1027
1027
1028 prefix = self._validate_archive_prefix(prefix)
1028 prefix = self._validate_archive_prefix(prefix)
1029
1029
1030 mtime = mtime or time.mktime(self.date.timetuple())
1030 mtime = mtime or time.mktime(self.date.timetuple())
1031
1031
1032 file_info = []
1032 file_info = []
1033 cur_rev = self.repository.get_commit(commit_id=self.raw_id)
1033 cur_rev = self.repository.get_commit(commit_id=self.raw_id)
1034 for _r, _d, files in cur_rev.walk('/'):
1034 for _r, _d, files in cur_rev.walk('/'):
1035 for f in files:
1035 for f in files:
1036 f_path = os.path.join(prefix, f.path)
1036 f_path = os.path.join(prefix, f.path)
1037 file_info.append(
1037 file_info.append(
1038 (f_path, f.mode, f.is_link(), f.raw_bytes))
1038 (f_path, f.mode, f.is_link(), f.raw_bytes))
1039
1039
1040 if write_metadata:
1040 if write_metadata:
1041 metadata = [
1041 metadata = [
1042 ('repo_name', self.repository.name),
1042 ('repo_name', self.repository.name),
1043 ('rev', self.raw_id),
1043 ('rev', self.raw_id),
1044 ('create_time', mtime),
1044 ('create_time', mtime),
1045 ('branch', self.branch),
1045 ('branch', self.branch),
1046 ('tags', ','.join(self.tags)),
1046 ('tags', ','.join(self.tags)),
1047 ]
1047 ]
1048 meta = ["%s:%s" % (f_name, value) for f_name, value in metadata]
1048 meta = ["%s:%s" % (f_name, value) for f_name, value in metadata]
1049 file_info.append(('.archival.txt', 0o644, False, '\n'.join(meta)))
1049 file_info.append(('.archival.txt', 0o644, False, '\n'.join(meta)))
1050
1050
1051 connection.Hg.archive_repo(file_path, mtime, file_info, kind)
1051 connection.Hg.archive_repo(file_path, mtime, file_info, kind)
1052
1052
1053 def _validate_archive_prefix(self, prefix):
1053 def _validate_archive_prefix(self, prefix):
1054 if prefix is None:
1054 if prefix is None:
1055 prefix = self._ARCHIVE_PREFIX_TEMPLATE.format(
1055 prefix = self._ARCHIVE_PREFIX_TEMPLATE.format(
1056 repo_name=safe_str(self.repository.name),
1056 repo_name=safe_str(self.repository.name),
1057 short_id=self.short_id)
1057 short_id=self.short_id)
1058 elif not isinstance(prefix, str):
1058 elif not isinstance(prefix, str):
1059 raise ValueError("prefix not a bytes object: %s" % repr(prefix))
1059 raise ValueError("prefix not a bytes object: %s" % repr(prefix))
1060 elif prefix.startswith('/'):
1060 elif prefix.startswith('/'):
1061 raise VCSError("Prefix cannot start with leading slash")
1061 raise VCSError("Prefix cannot start with leading slash")
1062 elif prefix.strip() == '':
1062 elif prefix.strip() == '':
1063 raise VCSError("Prefix cannot be empty")
1063 raise VCSError("Prefix cannot be empty")
1064 return prefix
1064 return prefix
1065
1065
1066 @LazyProperty
1066 @LazyProperty
1067 def root(self):
1067 def root(self):
1068 """
1068 """
1069 Returns ``RootNode`` object for this commit.
1069 Returns ``RootNode`` object for this commit.
1070 """
1070 """
1071 return self.get_node('')
1071 return self.get_node('')
1072
1072
1073 def next(self, branch=None):
1073 def next(self, branch=None):
1074 """
1074 """
1075 Returns next commit from current, if branch is gives it will return
1075 Returns next commit from current, if branch is gives it will return
1076 next commit belonging to this branch
1076 next commit belonging to this branch
1077
1077
1078 :param branch: show commits within the given named branch
1078 :param branch: show commits within the given named branch
1079 """
1079 """
1080 indexes = xrange(self.idx + 1, self.repository.count())
1080 indexes = xrange(self.idx + 1, self.repository.count())
1081 return self._find_next(indexes, branch)
1081 return self._find_next(indexes, branch)
1082
1082
1083 def prev(self, branch=None):
1083 def prev(self, branch=None):
1084 """
1084 """
1085 Returns previous commit from current, if branch is gives it will
1085 Returns previous commit from current, if branch is gives it will
1086 return previous commit belonging to this branch
1086 return previous commit belonging to this branch
1087
1087
1088 :param branch: show commit within the given named branch
1088 :param branch: show commit within the given named branch
1089 """
1089 """
1090 indexes = xrange(self.idx - 1, -1, -1)
1090 indexes = xrange(self.idx - 1, -1, -1)
1091 return self._find_next(indexes, branch)
1091 return self._find_next(indexes, branch)
1092
1092
1093 def _find_next(self, indexes, branch=None):
1093 def _find_next(self, indexes, branch=None):
1094 if branch and self.branch != branch:
1094 if branch and self.branch != branch:
1095 raise VCSError('Branch option used on commit not belonging '
1095 raise VCSError('Branch option used on commit not belonging '
1096 'to that branch')
1096 'to that branch')
1097
1097
1098 for next_idx in indexes:
1098 for next_idx in indexes:
1099 commit = self.repository.get_commit(commit_idx=next_idx)
1099 commit = self.repository.get_commit(commit_idx=next_idx)
1100 if branch and branch != commit.branch:
1100 if branch and branch != commit.branch:
1101 continue
1101 continue
1102 return commit
1102 return commit
1103 raise CommitDoesNotExistError
1103 raise CommitDoesNotExistError
1104
1104
1105 def diff(self, ignore_whitespace=True, context=3):
1105 def diff(self, ignore_whitespace=True, context=3):
1106 """
1106 """
1107 Returns a `Diff` object representing the change made by this commit.
1107 Returns a `Diff` object representing the change made by this commit.
1108 """
1108 """
1109 parent = self.first_parent
1109 parent = self.first_parent
1110 diff = self.repository.get_diff(
1110 diff = self.repository.get_diff(
1111 parent, self,
1111 parent, self,
1112 ignore_whitespace=ignore_whitespace,
1112 ignore_whitespace=ignore_whitespace,
1113 context=context)
1113 context=context)
1114 return diff
1114 return diff
1115
1115
1116 @LazyProperty
1116 @LazyProperty
1117 def added(self):
1117 def added(self):
1118 """
1118 """
1119 Returns list of added ``FileNode`` objects.
1119 Returns list of added ``FileNode`` objects.
1120 """
1120 """
1121 raise NotImplementedError
1121 raise NotImplementedError
1122
1122
1123 @LazyProperty
1123 @LazyProperty
1124 def changed(self):
1124 def changed(self):
1125 """
1125 """
1126 Returns list of modified ``FileNode`` objects.
1126 Returns list of modified ``FileNode`` objects.
1127 """
1127 """
1128 raise NotImplementedError
1128 raise NotImplementedError
1129
1129
1130 @LazyProperty
1130 @LazyProperty
1131 def removed(self):
1131 def removed(self):
1132 """
1132 """
1133 Returns list of removed ``FileNode`` objects.
1133 Returns list of removed ``FileNode`` objects.
1134 """
1134 """
1135 raise NotImplementedError
1135 raise NotImplementedError
1136
1136
1137 @LazyProperty
1137 @LazyProperty
1138 def size(self):
1138 def size(self):
1139 """
1139 """
1140 Returns total number of bytes from contents of all filenodes.
1140 Returns total number of bytes from contents of all filenodes.
1141 """
1141 """
1142 return sum((node.size for node in self.get_filenodes_generator()))
1142 return sum((node.size for node in self.get_filenodes_generator()))
1143
1143
1144 def walk(self, topurl=''):
1144 def walk(self, topurl=''):
1145 """
1145 """
1146 Similar to os.walk method. Insted of filesystem it walks through
1146 Similar to os.walk method. Insted of filesystem it walks through
1147 commit starting at given ``topurl``. Returns generator of tuples
1147 commit starting at given ``topurl``. Returns generator of tuples
1148 (topnode, dirnodes, filenodes).
1148 (topnode, dirnodes, filenodes).
1149 """
1149 """
1150 topnode = self.get_node(topurl)
1150 topnode = self.get_node(topurl)
1151 if not topnode.is_dir():
1151 if not topnode.is_dir():
1152 return
1152 return
1153 yield (topnode, topnode.dirs, topnode.files)
1153 yield (topnode, topnode.dirs, topnode.files)
1154 for dirnode in topnode.dirs:
1154 for dirnode in topnode.dirs:
1155 for tup in self.walk(dirnode.path):
1155 for tup in self.walk(dirnode.path):
1156 yield tup
1156 yield tup
1157
1157
1158 def get_filenodes_generator(self):
1158 def get_filenodes_generator(self):
1159 """
1159 """
1160 Returns generator that yields *all* file nodes.
1160 Returns generator that yields *all* file nodes.
1161 """
1161 """
1162 for topnode, dirs, files in self.walk():
1162 for topnode, dirs, files in self.walk():
1163 for node in files:
1163 for node in files:
1164 yield node
1164 yield node
1165
1165
1166 #
1166 #
1167 # Utilities for sub classes to support consistent behavior
1167 # Utilities for sub classes to support consistent behavior
1168 #
1168 #
1169
1169
1170 def no_node_at_path(self, path):
1170 def no_node_at_path(self, path):
1171 return NodeDoesNotExistError(
1171 return NodeDoesNotExistError(
1172 u"There is no file nor directory at the given path: "
1172 u"There is no file nor directory at the given path: "
1173 u"`%s` at commit %s" % (safe_unicode(path), self.short_id))
1173 u"`%s` at commit %s" % (safe_unicode(path), self.short_id))
1174
1174
1175 def _fix_path(self, path):
1175 def _fix_path(self, path):
1176 """
1176 """
1177 Paths are stored without trailing slash so we need to get rid off it if
1177 Paths are stored without trailing slash so we need to get rid off it if
1178 needed.
1178 needed.
1179 """
1179 """
1180 return path.rstrip('/')
1180 return path.rstrip('/')
1181
1181
1182 #
1182 #
1183 # Deprecated API based on changesets
1183 # Deprecated API based on changesets
1184 #
1184 #
1185
1185
1186 @property
1186 @property
1187 def revision(self):
1187 def revision(self):
1188 warnings.warn("Use idx instead", DeprecationWarning)
1188 warnings.warn("Use idx instead", DeprecationWarning)
1189 return self.idx
1189 return self.idx
1190
1190
1191 @revision.setter
1191 @revision.setter
1192 def revision(self, value):
1192 def revision(self, value):
1193 warnings.warn("Use idx instead", DeprecationWarning)
1193 warnings.warn("Use idx instead", DeprecationWarning)
1194 self.idx = value
1194 self.idx = value
1195
1195
1196 def get_file_changeset(self, path):
1196 def get_file_changeset(self, path):
1197 warnings.warn("Use get_file_commit instead", DeprecationWarning)
1197 warnings.warn("Use get_path_commit instead", DeprecationWarning)
1198 return self.get_file_commit(path)
1198 return self.get_path_commit(path)
1199
1199
1200
1200
1201 class BaseChangesetClass(type):
1201 class BaseChangesetClass(type):
1202
1202
1203 def __instancecheck__(self, instance):
1203 def __instancecheck__(self, instance):
1204 return isinstance(instance, BaseCommit)
1204 return isinstance(instance, BaseCommit)
1205
1205
1206
1206
1207 class BaseChangeset(BaseCommit):
1207 class BaseChangeset(BaseCommit):
1208
1208
1209 __metaclass__ = BaseChangesetClass
1209 __metaclass__ = BaseChangesetClass
1210
1210
1211 def __new__(cls, *args, **kwargs):
1211 def __new__(cls, *args, **kwargs):
1212 warnings.warn(
1212 warnings.warn(
1213 "Use BaseCommit instead of BaseChangeset", DeprecationWarning)
1213 "Use BaseCommit instead of BaseChangeset", DeprecationWarning)
1214 return super(BaseChangeset, cls).__new__(cls, *args, **kwargs)
1214 return super(BaseChangeset, cls).__new__(cls, *args, **kwargs)
1215
1215
1216
1216
1217 class BaseInMemoryCommit(object):
1217 class BaseInMemoryCommit(object):
1218 """
1218 """
1219 Represents differences between repository's state (most recent head) and
1219 Represents differences between repository's state (most recent head) and
1220 changes made *in place*.
1220 changes made *in place*.
1221
1221
1222 **Attributes**
1222 **Attributes**
1223
1223
1224 ``repository``
1224 ``repository``
1225 repository object for this in-memory-commit
1225 repository object for this in-memory-commit
1226
1226
1227 ``added``
1227 ``added``
1228 list of ``FileNode`` objects marked as *added*
1228 list of ``FileNode`` objects marked as *added*
1229
1229
1230 ``changed``
1230 ``changed``
1231 list of ``FileNode`` objects marked as *changed*
1231 list of ``FileNode`` objects marked as *changed*
1232
1232
1233 ``removed``
1233 ``removed``
1234 list of ``FileNode`` or ``RemovedFileNode`` objects marked to be
1234 list of ``FileNode`` or ``RemovedFileNode`` objects marked to be
1235 *removed*
1235 *removed*
1236
1236
1237 ``parents``
1237 ``parents``
1238 list of :class:`BaseCommit` instances representing parents of
1238 list of :class:`BaseCommit` instances representing parents of
1239 in-memory commit. Should always be 2-element sequence.
1239 in-memory commit. Should always be 2-element sequence.
1240
1240
1241 """
1241 """
1242
1242
1243 def __init__(self, repository):
1243 def __init__(self, repository):
1244 self.repository = repository
1244 self.repository = repository
1245 self.added = []
1245 self.added = []
1246 self.changed = []
1246 self.changed = []
1247 self.removed = []
1247 self.removed = []
1248 self.parents = []
1248 self.parents = []
1249
1249
1250 def add(self, *filenodes):
1250 def add(self, *filenodes):
1251 """
1251 """
1252 Marks given ``FileNode`` objects as *to be committed*.
1252 Marks given ``FileNode`` objects as *to be committed*.
1253
1253
1254 :raises ``NodeAlreadyExistsError``: if node with same path exists at
1254 :raises ``NodeAlreadyExistsError``: if node with same path exists at
1255 latest commit
1255 latest commit
1256 :raises ``NodeAlreadyAddedError``: if node with same path is already
1256 :raises ``NodeAlreadyAddedError``: if node with same path is already
1257 marked as *added*
1257 marked as *added*
1258 """
1258 """
1259 # Check if not already marked as *added* first
1259 # Check if not already marked as *added* first
1260 for node in filenodes:
1260 for node in filenodes:
1261 if node.path in (n.path for n in self.added):
1261 if node.path in (n.path for n in self.added):
1262 raise NodeAlreadyAddedError(
1262 raise NodeAlreadyAddedError(
1263 "Such FileNode %s is already marked for addition"
1263 "Such FileNode %s is already marked for addition"
1264 % node.path)
1264 % node.path)
1265 for node in filenodes:
1265 for node in filenodes:
1266 self.added.append(node)
1266 self.added.append(node)
1267
1267
1268 def change(self, *filenodes):
1268 def change(self, *filenodes):
1269 """
1269 """
1270 Marks given ``FileNode`` objects to be *changed* in next commit.
1270 Marks given ``FileNode`` objects to be *changed* in next commit.
1271
1271
1272 :raises ``EmptyRepositoryError``: if there are no commits yet
1272 :raises ``EmptyRepositoryError``: if there are no commits yet
1273 :raises ``NodeAlreadyExistsError``: if node with same path is already
1273 :raises ``NodeAlreadyExistsError``: if node with same path is already
1274 marked to be *changed*
1274 marked to be *changed*
1275 :raises ``NodeAlreadyRemovedError``: if node with same path is already
1275 :raises ``NodeAlreadyRemovedError``: if node with same path is already
1276 marked to be *removed*
1276 marked to be *removed*
1277 :raises ``NodeDoesNotExistError``: if node doesn't exist in latest
1277 :raises ``NodeDoesNotExistError``: if node doesn't exist in latest
1278 commit
1278 commit
1279 :raises ``NodeNotChangedError``: if node hasn't really be changed
1279 :raises ``NodeNotChangedError``: if node hasn't really be changed
1280 """
1280 """
1281 for node in filenodes:
1281 for node in filenodes:
1282 if node.path in (n.path for n in self.removed):
1282 if node.path in (n.path for n in self.removed):
1283 raise NodeAlreadyRemovedError(
1283 raise NodeAlreadyRemovedError(
1284 "Node at %s is already marked as removed" % node.path)
1284 "Node at %s is already marked as removed" % node.path)
1285 try:
1285 try:
1286 self.repository.get_commit()
1286 self.repository.get_commit()
1287 except EmptyRepositoryError:
1287 except EmptyRepositoryError:
1288 raise EmptyRepositoryError(
1288 raise EmptyRepositoryError(
1289 "Nothing to change - try to *add* new nodes rather than "
1289 "Nothing to change - try to *add* new nodes rather than "
1290 "changing them")
1290 "changing them")
1291 for node in filenodes:
1291 for node in filenodes:
1292 if node.path in (n.path for n in self.changed):
1292 if node.path in (n.path for n in self.changed):
1293 raise NodeAlreadyChangedError(
1293 raise NodeAlreadyChangedError(
1294 "Node at '%s' is already marked as changed" % node.path)
1294 "Node at '%s' is already marked as changed" % node.path)
1295 self.changed.append(node)
1295 self.changed.append(node)
1296
1296
1297 def remove(self, *filenodes):
1297 def remove(self, *filenodes):
1298 """
1298 """
1299 Marks given ``FileNode`` (or ``RemovedFileNode``) objects to be
1299 Marks given ``FileNode`` (or ``RemovedFileNode``) objects to be
1300 *removed* in next commit.
1300 *removed* in next commit.
1301
1301
1302 :raises ``NodeAlreadyRemovedError``: if node has been already marked to
1302 :raises ``NodeAlreadyRemovedError``: if node has been already marked to
1303 be *removed*
1303 be *removed*
1304 :raises ``NodeAlreadyChangedError``: if node has been already marked to
1304 :raises ``NodeAlreadyChangedError``: if node has been already marked to
1305 be *changed*
1305 be *changed*
1306 """
1306 """
1307 for node in filenodes:
1307 for node in filenodes:
1308 if node.path in (n.path for n in self.removed):
1308 if node.path in (n.path for n in self.removed):
1309 raise NodeAlreadyRemovedError(
1309 raise NodeAlreadyRemovedError(
1310 "Node is already marked to for removal at %s" % node.path)
1310 "Node is already marked to for removal at %s" % node.path)
1311 if node.path in (n.path for n in self.changed):
1311 if node.path in (n.path for n in self.changed):
1312 raise NodeAlreadyChangedError(
1312 raise NodeAlreadyChangedError(
1313 "Node is already marked to be changed at %s" % node.path)
1313 "Node is already marked to be changed at %s" % node.path)
1314 # We only mark node as *removed* - real removal is done by
1314 # We only mark node as *removed* - real removal is done by
1315 # commit method
1315 # commit method
1316 self.removed.append(node)
1316 self.removed.append(node)
1317
1317
1318 def reset(self):
1318 def reset(self):
1319 """
1319 """
1320 Resets this instance to initial state (cleans ``added``, ``changed``
1320 Resets this instance to initial state (cleans ``added``, ``changed``
1321 and ``removed`` lists).
1321 and ``removed`` lists).
1322 """
1322 """
1323 self.added = []
1323 self.added = []
1324 self.changed = []
1324 self.changed = []
1325 self.removed = []
1325 self.removed = []
1326 self.parents = []
1326 self.parents = []
1327
1327
1328 def get_ipaths(self):
1328 def get_ipaths(self):
1329 """
1329 """
1330 Returns generator of paths from nodes marked as added, changed or
1330 Returns generator of paths from nodes marked as added, changed or
1331 removed.
1331 removed.
1332 """
1332 """
1333 for node in itertools.chain(self.added, self.changed, self.removed):
1333 for node in itertools.chain(self.added, self.changed, self.removed):
1334 yield node.path
1334 yield node.path
1335
1335
1336 def get_paths(self):
1336 def get_paths(self):
1337 """
1337 """
1338 Returns list of paths from nodes marked as added, changed or removed.
1338 Returns list of paths from nodes marked as added, changed or removed.
1339 """
1339 """
1340 return list(self.get_ipaths())
1340 return list(self.get_ipaths())
1341
1341
1342 def check_integrity(self, parents=None):
1342 def check_integrity(self, parents=None):
1343 """
1343 """
1344 Checks in-memory commit's integrity. Also, sets parents if not
1344 Checks in-memory commit's integrity. Also, sets parents if not
1345 already set.
1345 already set.
1346
1346
1347 :raises CommitError: if any error occurs (i.e.
1347 :raises CommitError: if any error occurs (i.e.
1348 ``NodeDoesNotExistError``).
1348 ``NodeDoesNotExistError``).
1349 """
1349 """
1350 if not self.parents:
1350 if not self.parents:
1351 parents = parents or []
1351 parents = parents or []
1352 if len(parents) == 0:
1352 if len(parents) == 0:
1353 try:
1353 try:
1354 parents = [self.repository.get_commit(), None]
1354 parents = [self.repository.get_commit(), None]
1355 except EmptyRepositoryError:
1355 except EmptyRepositoryError:
1356 parents = [None, None]
1356 parents = [None, None]
1357 elif len(parents) == 1:
1357 elif len(parents) == 1:
1358 parents += [None]
1358 parents += [None]
1359 self.parents = parents
1359 self.parents = parents
1360
1360
1361 # Local parents, only if not None
1361 # Local parents, only if not None
1362 parents = [p for p in self.parents if p]
1362 parents = [p for p in self.parents if p]
1363
1363
1364 # Check nodes marked as added
1364 # Check nodes marked as added
1365 for p in parents:
1365 for p in parents:
1366 for node in self.added:
1366 for node in self.added:
1367 try:
1367 try:
1368 p.get_node(node.path)
1368 p.get_node(node.path)
1369 except NodeDoesNotExistError:
1369 except NodeDoesNotExistError:
1370 pass
1370 pass
1371 else:
1371 else:
1372 raise NodeAlreadyExistsError(
1372 raise NodeAlreadyExistsError(
1373 "Node `%s` already exists at %s" % (node.path, p))
1373 "Node `%s` already exists at %s" % (node.path, p))
1374
1374
1375 # Check nodes marked as changed
1375 # Check nodes marked as changed
1376 missing = set(self.changed)
1376 missing = set(self.changed)
1377 not_changed = set(self.changed)
1377 not_changed = set(self.changed)
1378 if self.changed and not parents:
1378 if self.changed and not parents:
1379 raise NodeDoesNotExistError(str(self.changed[0].path))
1379 raise NodeDoesNotExistError(str(self.changed[0].path))
1380 for p in parents:
1380 for p in parents:
1381 for node in self.changed:
1381 for node in self.changed:
1382 try:
1382 try:
1383 old = p.get_node(node.path)
1383 old = p.get_node(node.path)
1384 missing.remove(node)
1384 missing.remove(node)
1385 # if content actually changed, remove node from not_changed
1385 # if content actually changed, remove node from not_changed
1386 if old.content != node.content:
1386 if old.content != node.content:
1387 not_changed.remove(node)
1387 not_changed.remove(node)
1388 except NodeDoesNotExistError:
1388 except NodeDoesNotExistError:
1389 pass
1389 pass
1390 if self.changed and missing:
1390 if self.changed and missing:
1391 raise NodeDoesNotExistError(
1391 raise NodeDoesNotExistError(
1392 "Node `%s` marked as modified but missing in parents: %s"
1392 "Node `%s` marked as modified but missing in parents: %s"
1393 % (node.path, parents))
1393 % (node.path, parents))
1394
1394
1395 if self.changed and not_changed:
1395 if self.changed and not_changed:
1396 raise NodeNotChangedError(
1396 raise NodeNotChangedError(
1397 "Node `%s` wasn't actually changed (parents: %s)"
1397 "Node `%s` wasn't actually changed (parents: %s)"
1398 % (not_changed.pop().path, parents))
1398 % (not_changed.pop().path, parents))
1399
1399
1400 # Check nodes marked as removed
1400 # Check nodes marked as removed
1401 if self.removed and not parents:
1401 if self.removed and not parents:
1402 raise NodeDoesNotExistError(
1402 raise NodeDoesNotExistError(
1403 "Cannot remove node at %s as there "
1403 "Cannot remove node at %s as there "
1404 "were no parents specified" % self.removed[0].path)
1404 "were no parents specified" % self.removed[0].path)
1405 really_removed = set()
1405 really_removed = set()
1406 for p in parents:
1406 for p in parents:
1407 for node in self.removed:
1407 for node in self.removed:
1408 try:
1408 try:
1409 p.get_node(node.path)
1409 p.get_node(node.path)
1410 really_removed.add(node)
1410 really_removed.add(node)
1411 except CommitError:
1411 except CommitError:
1412 pass
1412 pass
1413 not_removed = set(self.removed) - really_removed
1413 not_removed = set(self.removed) - really_removed
1414 if not_removed:
1414 if not_removed:
1415 # TODO: johbo: This code branch does not seem to be covered
1415 # TODO: johbo: This code branch does not seem to be covered
1416 raise NodeDoesNotExistError(
1416 raise NodeDoesNotExistError(
1417 "Cannot remove node at %s from "
1417 "Cannot remove node at %s from "
1418 "following parents: %s" % (not_removed, parents))
1418 "following parents: %s" % (not_removed, parents))
1419
1419
1420 def commit(
1420 def commit(
1421 self, message, author, parents=None, branch=None, date=None,
1421 self, message, author, parents=None, branch=None, date=None,
1422 **kwargs):
1422 **kwargs):
1423 """
1423 """
1424 Performs in-memory commit (doesn't check workdir in any way) and
1424 Performs in-memory commit (doesn't check workdir in any way) and
1425 returns newly created :class:`BaseCommit`. Updates repository's
1425 returns newly created :class:`BaseCommit`. Updates repository's
1426 attribute `commits`.
1426 attribute `commits`.
1427
1427
1428 .. note::
1428 .. note::
1429
1429
1430 While overriding this method each backend's should call
1430 While overriding this method each backend's should call
1431 ``self.check_integrity(parents)`` in the first place.
1431 ``self.check_integrity(parents)`` in the first place.
1432
1432
1433 :param message: message of the commit
1433 :param message: message of the commit
1434 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
1434 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
1435 :param parents: single parent or sequence of parents from which commit
1435 :param parents: single parent or sequence of parents from which commit
1436 would be derived
1436 would be derived
1437 :param date: ``datetime.datetime`` instance. Defaults to
1437 :param date: ``datetime.datetime`` instance. Defaults to
1438 ``datetime.datetime.now()``.
1438 ``datetime.datetime.now()``.
1439 :param branch: branch name, as string. If none given, default backend's
1439 :param branch: branch name, as string. If none given, default backend's
1440 branch would be used.
1440 branch would be used.
1441
1441
1442 :raises ``CommitError``: if any error occurs while committing
1442 :raises ``CommitError``: if any error occurs while committing
1443 """
1443 """
1444 raise NotImplementedError
1444 raise NotImplementedError
1445
1445
1446
1446
1447 class BaseInMemoryChangesetClass(type):
1447 class BaseInMemoryChangesetClass(type):
1448
1448
1449 def __instancecheck__(self, instance):
1449 def __instancecheck__(self, instance):
1450 return isinstance(instance, BaseInMemoryCommit)
1450 return isinstance(instance, BaseInMemoryCommit)
1451
1451
1452
1452
1453 class BaseInMemoryChangeset(BaseInMemoryCommit):
1453 class BaseInMemoryChangeset(BaseInMemoryCommit):
1454
1454
1455 __metaclass__ = BaseInMemoryChangesetClass
1455 __metaclass__ = BaseInMemoryChangesetClass
1456
1456
1457 def __new__(cls, *args, **kwargs):
1457 def __new__(cls, *args, **kwargs):
1458 warnings.warn(
1458 warnings.warn(
1459 "Use BaseCommit instead of BaseInMemoryCommit", DeprecationWarning)
1459 "Use BaseCommit instead of BaseInMemoryCommit", DeprecationWarning)
1460 return super(BaseInMemoryChangeset, cls).__new__(cls, *args, **kwargs)
1460 return super(BaseInMemoryChangeset, cls).__new__(cls, *args, **kwargs)
1461
1461
1462
1462
1463 class EmptyCommit(BaseCommit):
1463 class EmptyCommit(BaseCommit):
1464 """
1464 """
1465 An dummy empty commit. It's possible to pass hash when creating
1465 An dummy empty commit. It's possible to pass hash when creating
1466 an EmptyCommit
1466 an EmptyCommit
1467 """
1467 """
1468
1468
1469 def __init__(
1469 def __init__(
1470 self, commit_id='0' * 40, repo=None, alias=None, idx=-1,
1470 self, commit_id='0' * 40, repo=None, alias=None, idx=-1,
1471 message='', author='', date=None):
1471 message='', author='', date=None):
1472 self._empty_commit_id = commit_id
1472 self._empty_commit_id = commit_id
1473 # TODO: johbo: Solve idx parameter, default value does not make
1473 # TODO: johbo: Solve idx parameter, default value does not make
1474 # too much sense
1474 # too much sense
1475 self.idx = idx
1475 self.idx = idx
1476 self.message = message
1476 self.message = message
1477 self.author = author
1477 self.author = author
1478 self.date = date or datetime.datetime.fromtimestamp(0)
1478 self.date = date or datetime.datetime.fromtimestamp(0)
1479 self.repository = repo
1479 self.repository = repo
1480 self.alias = alias
1480 self.alias = alias
1481
1481
1482 @LazyProperty
1482 @LazyProperty
1483 def raw_id(self):
1483 def raw_id(self):
1484 """
1484 """
1485 Returns raw string identifying this commit, useful for web
1485 Returns raw string identifying this commit, useful for web
1486 representation.
1486 representation.
1487 """
1487 """
1488
1488
1489 return self._empty_commit_id
1489 return self._empty_commit_id
1490
1490
1491 @LazyProperty
1491 @LazyProperty
1492 def branch(self):
1492 def branch(self):
1493 if self.alias:
1493 if self.alias:
1494 from rhodecode.lib.vcs.backends import get_backend
1494 from rhodecode.lib.vcs.backends import get_backend
1495 return get_backend(self.alias).DEFAULT_BRANCH_NAME
1495 return get_backend(self.alias).DEFAULT_BRANCH_NAME
1496
1496
1497 @LazyProperty
1497 @LazyProperty
1498 def short_id(self):
1498 def short_id(self):
1499 return self.raw_id[:12]
1499 return self.raw_id[:12]
1500
1500
1501 @LazyProperty
1501 @LazyProperty
1502 def id(self):
1502 def id(self):
1503 return self.raw_id
1503 return self.raw_id
1504
1504
1505 def get_file_commit(self, path):
1505 def get_path_commit(self, path):
1506 return self
1506 return self
1507
1507
1508 def get_file_content(self, path):
1508 def get_file_content(self, path):
1509 return u''
1509 return u''
1510
1510
1511 def get_file_size(self, path):
1511 def get_file_size(self, path):
1512 return 0
1512 return 0
1513
1513
1514
1514
1515 class EmptyChangesetClass(type):
1515 class EmptyChangesetClass(type):
1516
1516
1517 def __instancecheck__(self, instance):
1517 def __instancecheck__(self, instance):
1518 return isinstance(instance, EmptyCommit)
1518 return isinstance(instance, EmptyCommit)
1519
1519
1520
1520
1521 class EmptyChangeset(EmptyCommit):
1521 class EmptyChangeset(EmptyCommit):
1522
1522
1523 __metaclass__ = EmptyChangesetClass
1523 __metaclass__ = EmptyChangesetClass
1524
1524
1525 def __new__(cls, *args, **kwargs):
1525 def __new__(cls, *args, **kwargs):
1526 warnings.warn(
1526 warnings.warn(
1527 "Use EmptyCommit instead of EmptyChangeset", DeprecationWarning)
1527 "Use EmptyCommit instead of EmptyChangeset", DeprecationWarning)
1528 return super(EmptyCommit, cls).__new__(cls, *args, **kwargs)
1528 return super(EmptyCommit, cls).__new__(cls, *args, **kwargs)
1529
1529
1530 def __init__(self, cs='0' * 40, repo=None, requested_revision=None,
1530 def __init__(self, cs='0' * 40, repo=None, requested_revision=None,
1531 alias=None, revision=-1, message='', author='', date=None):
1531 alias=None, revision=-1, message='', author='', date=None):
1532 if requested_revision is not None:
1532 if requested_revision is not None:
1533 warnings.warn(
1533 warnings.warn(
1534 "Parameter requested_revision not supported anymore",
1534 "Parameter requested_revision not supported anymore",
1535 DeprecationWarning)
1535 DeprecationWarning)
1536 super(EmptyChangeset, self).__init__(
1536 super(EmptyChangeset, self).__init__(
1537 commit_id=cs, repo=repo, alias=alias, idx=revision,
1537 commit_id=cs, repo=repo, alias=alias, idx=revision,
1538 message=message, author=author, date=date)
1538 message=message, author=author, date=date)
1539
1539
1540 @property
1540 @property
1541 def revision(self):
1541 def revision(self):
1542 warnings.warn("Use idx instead", DeprecationWarning)
1542 warnings.warn("Use idx instead", DeprecationWarning)
1543 return self.idx
1543 return self.idx
1544
1544
1545 @revision.setter
1545 @revision.setter
1546 def revision(self, value):
1546 def revision(self, value):
1547 warnings.warn("Use idx instead", DeprecationWarning)
1547 warnings.warn("Use idx instead", DeprecationWarning)
1548 self.idx = value
1548 self.idx = value
1549
1549
1550
1550
1551 class EmptyRepository(BaseRepository):
1551 class EmptyRepository(BaseRepository):
1552 def __init__(self, repo_path=None, config=None, create=False, **kwargs):
1552 def __init__(self, repo_path=None, config=None, create=False, **kwargs):
1553 pass
1553 pass
1554
1554
1555 def get_diff(self, *args, **kwargs):
1555 def get_diff(self, *args, **kwargs):
1556 from rhodecode.lib.vcs.backends.git.diff import GitDiff
1556 from rhodecode.lib.vcs.backends.git.diff import GitDiff
1557 return GitDiff('')
1557 return GitDiff('')
1558
1558
1559
1559
1560 class CollectionGenerator(object):
1560 class CollectionGenerator(object):
1561
1561
1562 def __init__(self, repo, commit_ids, collection_size=None, pre_load=None):
1562 def __init__(self, repo, commit_ids, collection_size=None, pre_load=None):
1563 self.repo = repo
1563 self.repo = repo
1564 self.commit_ids = commit_ids
1564 self.commit_ids = commit_ids
1565 # TODO: (oliver) this isn't currently hooked up
1565 # TODO: (oliver) this isn't currently hooked up
1566 self.collection_size = None
1566 self.collection_size = None
1567 self.pre_load = pre_load
1567 self.pre_load = pre_load
1568
1568
1569 def __len__(self):
1569 def __len__(self):
1570 if self.collection_size is not None:
1570 if self.collection_size is not None:
1571 return self.collection_size
1571 return self.collection_size
1572 return self.commit_ids.__len__()
1572 return self.commit_ids.__len__()
1573
1573
1574 def __iter__(self):
1574 def __iter__(self):
1575 for commit_id in self.commit_ids:
1575 for commit_id in self.commit_ids:
1576 # TODO: johbo: Mercurial passes in commit indices or commit ids
1576 # TODO: johbo: Mercurial passes in commit indices or commit ids
1577 yield self._commit_factory(commit_id)
1577 yield self._commit_factory(commit_id)
1578
1578
1579 def _commit_factory(self, commit_id):
1579 def _commit_factory(self, commit_id):
1580 """
1580 """
1581 Allows backends to override the way commits are generated.
1581 Allows backends to override the way commits are generated.
1582 """
1582 """
1583 return self.repo.get_commit(commit_id=commit_id,
1583 return self.repo.get_commit(commit_id=commit_id,
1584 pre_load=self.pre_load)
1584 pre_load=self.pre_load)
1585
1585
1586 def __getslice__(self, i, j):
1586 def __getslice__(self, i, j):
1587 """
1587 """
1588 Returns an iterator of sliced repository
1588 Returns an iterator of sliced repository
1589 """
1589 """
1590 commit_ids = self.commit_ids[i:j]
1590 commit_ids = self.commit_ids[i:j]
1591 return self.__class__(
1591 return self.__class__(
1592 self.repo, commit_ids, pre_load=self.pre_load)
1592 self.repo, commit_ids, pre_load=self.pre_load)
1593
1593
1594 def __repr__(self):
1594 def __repr__(self):
1595 return '<CollectionGenerator[len:%s]>' % (self.__len__())
1595 return '<CollectionGenerator[len:%s]>' % (self.__len__())
1596
1596
1597
1597
1598 class Config(object):
1598 class Config(object):
1599 """
1599 """
1600 Represents the configuration for a repository.
1600 Represents the configuration for a repository.
1601
1601
1602 The API is inspired by :class:`ConfigParser.ConfigParser` from the
1602 The API is inspired by :class:`ConfigParser.ConfigParser` from the
1603 standard library. It implements only the needed subset.
1603 standard library. It implements only the needed subset.
1604 """
1604 """
1605
1605
1606 def __init__(self):
1606 def __init__(self):
1607 self._values = {}
1607 self._values = {}
1608
1608
1609 def copy(self):
1609 def copy(self):
1610 clone = Config()
1610 clone = Config()
1611 for section, values in self._values.items():
1611 for section, values in self._values.items():
1612 clone._values[section] = values.copy()
1612 clone._values[section] = values.copy()
1613 return clone
1613 return clone
1614
1614
1615 def __repr__(self):
1615 def __repr__(self):
1616 return '<Config(%s sections) at %s>' % (
1616 return '<Config(%s sections) at %s>' % (
1617 len(self._values), hex(id(self)))
1617 len(self._values), hex(id(self)))
1618
1618
1619 def items(self, section):
1619 def items(self, section):
1620 return self._values.get(section, {}).iteritems()
1620 return self._values.get(section, {}).iteritems()
1621
1621
1622 def get(self, section, option):
1622 def get(self, section, option):
1623 return self._values.get(section, {}).get(option)
1623 return self._values.get(section, {}).get(option)
1624
1624
1625 def set(self, section, option, value):
1625 def set(self, section, option, value):
1626 section_values = self._values.setdefault(section, {})
1626 section_values = self._values.setdefault(section, {})
1627 section_values[option] = value
1627 section_values[option] = value
1628
1628
1629 def clear_section(self, section):
1629 def clear_section(self, section):
1630 self._values[section] = {}
1630 self._values[section] = {}
1631
1631
1632 def serialize(self):
1632 def serialize(self):
1633 """
1633 """
1634 Creates a list of three tuples (section, key, value) representing
1634 Creates a list of three tuples (section, key, value) representing
1635 this config object.
1635 this config object.
1636 """
1636 """
1637 items = []
1637 items = []
1638 for section in self._values:
1638 for section in self._values:
1639 for option, value in self._values[section].items():
1639 for option, value in self._values[section].items():
1640 items.append(
1640 items.append(
1641 (safe_str(section), safe_str(option), safe_str(value)))
1641 (safe_str(section), safe_str(option), safe_str(value)))
1642 return items
1642 return items
1643
1643
1644
1644
1645 class Diff(object):
1645 class Diff(object):
1646 """
1646 """
1647 Represents a diff result from a repository backend.
1647 Represents a diff result from a repository backend.
1648
1648
1649 Subclasses have to provide a backend specific value for
1649 Subclasses have to provide a backend specific value for
1650 :attr:`_header_re` and :attr:`_meta_re`.
1650 :attr:`_header_re` and :attr:`_meta_re`.
1651 """
1651 """
1652 _meta_re = None
1652 _meta_re = None
1653 _header_re = None
1653 _header_re = None
1654
1654
1655 def __init__(self, raw_diff):
1655 def __init__(self, raw_diff):
1656 self.raw = raw_diff
1656 self.raw = raw_diff
1657
1657
1658 def chunks(self):
1658 def chunks(self):
1659 """
1659 """
1660 split the diff in chunks of separate --git a/file b/file chunks
1660 split the diff in chunks of separate --git a/file b/file chunks
1661 to make diffs consistent we must prepend with \n, and make sure
1661 to make diffs consistent we must prepend with \n, and make sure
1662 we can detect last chunk as this was also has special rule
1662 we can detect last chunk as this was also has special rule
1663 """
1663 """
1664
1664
1665 diff_parts = ('\n' + self.raw).split('\ndiff --git')
1665 diff_parts = ('\n' + self.raw).split('\ndiff --git')
1666 header = diff_parts[0]
1666 header = diff_parts[0]
1667
1667
1668 if self._meta_re:
1668 if self._meta_re:
1669 match = self._meta_re.match(header)
1669 match = self._meta_re.match(header)
1670
1670
1671 chunks = diff_parts[1:]
1671 chunks = diff_parts[1:]
1672 total_chunks = len(chunks)
1672 total_chunks = len(chunks)
1673
1673
1674 return (
1674 return (
1675 DiffChunk(chunk, self, cur_chunk == total_chunks)
1675 DiffChunk(chunk, self, cur_chunk == total_chunks)
1676 for cur_chunk, chunk in enumerate(chunks, start=1))
1676 for cur_chunk, chunk in enumerate(chunks, start=1))
1677
1677
1678
1678
1679 class DiffChunk(object):
1679 class DiffChunk(object):
1680
1680
1681 def __init__(self, chunk, diff, last_chunk):
1681 def __init__(self, chunk, diff, last_chunk):
1682 self._diff = diff
1682 self._diff = diff
1683
1683
1684 # since we split by \ndiff --git that part is lost from original diff
1684 # since we split by \ndiff --git that part is lost from original diff
1685 # we need to re-apply it at the end, EXCEPT ! if it's last chunk
1685 # we need to re-apply it at the end, EXCEPT ! if it's last chunk
1686 if not last_chunk:
1686 if not last_chunk:
1687 chunk += '\n'
1687 chunk += '\n'
1688
1688
1689 match = self._diff._header_re.match(chunk)
1689 match = self._diff._header_re.match(chunk)
1690 self.header = match.groupdict()
1690 self.header = match.groupdict()
1691 self.diff = chunk[match.end():]
1691 self.diff = chunk[match.end():]
1692 self.raw = chunk
1692 self.raw = chunk
1693
1693
1694
1694
1695 class BasePathPermissionChecker(object):
1695 class BasePathPermissionChecker(object):
1696
1696
1697 @staticmethod
1697 @staticmethod
1698 def create_from_patterns(includes, excludes):
1698 def create_from_patterns(includes, excludes):
1699 if includes and '*' in includes and not excludes:
1699 if includes and '*' in includes and not excludes:
1700 return AllPathPermissionChecker()
1700 return AllPathPermissionChecker()
1701 elif excludes and '*' in excludes:
1701 elif excludes and '*' in excludes:
1702 return NonePathPermissionChecker()
1702 return NonePathPermissionChecker()
1703 else:
1703 else:
1704 return PatternPathPermissionChecker(includes, excludes)
1704 return PatternPathPermissionChecker(includes, excludes)
1705
1705
1706 @property
1706 @property
1707 def has_full_access(self):
1707 def has_full_access(self):
1708 raise NotImplemented()
1708 raise NotImplemented()
1709
1709
1710 def has_access(self, path):
1710 def has_access(self, path):
1711 raise NotImplemented()
1711 raise NotImplemented()
1712
1712
1713
1713
1714 class AllPathPermissionChecker(BasePathPermissionChecker):
1714 class AllPathPermissionChecker(BasePathPermissionChecker):
1715
1715
1716 @property
1716 @property
1717 def has_full_access(self):
1717 def has_full_access(self):
1718 return True
1718 return True
1719
1719
1720 def has_access(self, path):
1720 def has_access(self, path):
1721 return True
1721 return True
1722
1722
1723
1723
1724 class NonePathPermissionChecker(BasePathPermissionChecker):
1724 class NonePathPermissionChecker(BasePathPermissionChecker):
1725
1725
1726 @property
1726 @property
1727 def has_full_access(self):
1727 def has_full_access(self):
1728 return False
1728 return False
1729
1729
1730 def has_access(self, path):
1730 def has_access(self, path):
1731 return False
1731 return False
1732
1732
1733
1733
1734 class PatternPathPermissionChecker(BasePathPermissionChecker):
1734 class PatternPathPermissionChecker(BasePathPermissionChecker):
1735
1735
1736 def __init__(self, includes, excludes):
1736 def __init__(self, includes, excludes):
1737 self.includes = includes
1737 self.includes = includes
1738 self.excludes = excludes
1738 self.excludes = excludes
1739 self.includes_re = [] if not includes else [
1739 self.includes_re = [] if not includes else [
1740 re.compile(fnmatch.translate(pattern)) for pattern in includes]
1740 re.compile(fnmatch.translate(pattern)) for pattern in includes]
1741 self.excludes_re = [] if not excludes else [
1741 self.excludes_re = [] if not excludes else [
1742 re.compile(fnmatch.translate(pattern)) for pattern in excludes]
1742 re.compile(fnmatch.translate(pattern)) for pattern in excludes]
1743
1743
1744 @property
1744 @property
1745 def has_full_access(self):
1745 def has_full_access(self):
1746 return '*' in self.includes and not self.excludes
1746 return '*' in self.includes and not self.excludes
1747
1747
1748 def has_access(self, path):
1748 def has_access(self, path):
1749 for regex in self.excludes_re:
1749 for regex in self.excludes_re:
1750 if regex.match(path):
1750 if regex.match(path):
1751 return False
1751 return False
1752 for regex in self.includes_re:
1752 for regex in self.includes_re:
1753 if regex.match(path):
1753 if regex.match(path):
1754 return True
1754 return True
1755 return False
1755 return False
@@ -1,545 +1,530 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2014-2018 RhodeCode GmbH
3 # Copyright (C) 2014-2018 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 GIT commit module
22 GIT commit module
23 """
23 """
24
24
25 import re
25 import re
26 import stat
26 import stat
27 from itertools import chain
27 from itertools import chain
28 from StringIO import StringIO
28 from StringIO import StringIO
29
29
30 from zope.cachedescriptors.property import Lazy as LazyProperty
30 from zope.cachedescriptors.property import Lazy as LazyProperty
31
31
32 from rhodecode.lib.datelib import utcdate_fromtimestamp
32 from rhodecode.lib.datelib import utcdate_fromtimestamp
33 from rhodecode.lib.utils import safe_unicode, safe_str
33 from rhodecode.lib.utils import safe_unicode, safe_str
34 from rhodecode.lib.utils2 import safe_int
34 from rhodecode.lib.utils2 import safe_int
35 from rhodecode.lib.vcs.conf import settings
35 from rhodecode.lib.vcs.conf import settings
36 from rhodecode.lib.vcs.backends import base
36 from rhodecode.lib.vcs.backends import base
37 from rhodecode.lib.vcs.exceptions import CommitError, NodeDoesNotExistError
37 from rhodecode.lib.vcs.exceptions import CommitError, NodeDoesNotExistError
38 from rhodecode.lib.vcs.nodes import (
38 from rhodecode.lib.vcs.nodes import (
39 FileNode, DirNode, NodeKind, RootNode, SubModuleNode,
39 FileNode, DirNode, NodeKind, RootNode, SubModuleNode,
40 ChangedFileNodesGenerator, AddedFileNodesGenerator,
40 ChangedFileNodesGenerator, AddedFileNodesGenerator,
41 RemovedFileNodesGenerator, LargeFileNode)
41 RemovedFileNodesGenerator, LargeFileNode)
42 from rhodecode.lib.vcs.compat import configparser
42 from rhodecode.lib.vcs.compat import configparser
43
43
44
44
45 class GitCommit(base.BaseCommit):
45 class GitCommit(base.BaseCommit):
46 """
46 """
47 Represents state of the repository at single commit id.
47 Represents state of the repository at single commit id.
48 """
48 """
49 _author_property = 'author'
49 _author_property = 'author'
50 _committer_property = 'committer'
50 _committer_property = 'committer'
51 _date_property = 'commit_time'
51 _date_property = 'commit_time'
52 _date_tz_property = 'commit_timezone'
52 _date_tz_property = 'commit_timezone'
53 _message_property = 'message'
53 _message_property = 'message'
54 _parents_property = 'parents'
54 _parents_property = 'parents'
55
55
56 _filter_pre_load = [
56 _filter_pre_load = [
57 # done through a more complex tree walk on parents
57 # done through a more complex tree walk on parents
58 "affected_files",
58 "affected_files",
59 # based on repository cached property
59 # based on repository cached property
60 "branch",
60 "branch",
61 # done through subprocess not remote call
61 # done through subprocess not remote call
62 "children",
62 "children",
63 # done through a more complex tree walk on parents
63 # done through a more complex tree walk on parents
64 "status",
64 "status",
65 # mercurial specific property not supported here
65 # mercurial specific property not supported here
66 "_file_paths",
66 "_file_paths",
67 # mercurial specific property not supported here
67 # mercurial specific property not supported here
68 'obsolete',
68 'obsolete',
69 # mercurial specific property not supported here
69 # mercurial specific property not supported here
70 'phase',
70 'phase',
71 # mercurial specific property not supported here
71 # mercurial specific property not supported here
72 'hidden'
72 'hidden'
73 ]
73 ]
74
74
75 def __init__(self, repository, raw_id, idx, pre_load=None):
75 def __init__(self, repository, raw_id, idx, pre_load=None):
76 self.repository = repository
76 self.repository = repository
77 self._remote = repository._remote
77 self._remote = repository._remote
78 # TODO: johbo: Tweak of raw_id should not be necessary
78 # TODO: johbo: Tweak of raw_id should not be necessary
79 self.raw_id = safe_str(raw_id)
79 self.raw_id = safe_str(raw_id)
80 self.idx = idx
80 self.idx = idx
81
81
82 self._set_bulk_properties(pre_load)
82 self._set_bulk_properties(pre_load)
83
83
84 # caches
84 # caches
85 self._stat_modes = {} # stat info for paths
85 self._stat_modes = {} # stat info for paths
86 self._paths = {} # path processed with parse_tree
86 self._paths = {} # path processed with parse_tree
87 self.nodes = {}
87 self.nodes = {}
88 self._submodules = None
88 self._submodules = None
89
89
90 def _set_bulk_properties(self, pre_load):
90 def _set_bulk_properties(self, pre_load):
91 if not pre_load:
91 if not pre_load:
92 return
92 return
93 pre_load = [entry for entry in pre_load
93 pre_load = [entry for entry in pre_load
94 if entry not in self._filter_pre_load]
94 if entry not in self._filter_pre_load]
95 if not pre_load:
95 if not pre_load:
96 return
96 return
97
97
98 result = self._remote.bulk_request(self.raw_id, pre_load)
98 result = self._remote.bulk_request(self.raw_id, pre_load)
99 for attr, value in result.items():
99 for attr, value in result.items():
100 if attr in ["author", "message"]:
100 if attr in ["author", "message"]:
101 if value:
101 if value:
102 value = safe_unicode(value)
102 value = safe_unicode(value)
103 elif attr == "date":
103 elif attr == "date":
104 value = utcdate_fromtimestamp(*value)
104 value = utcdate_fromtimestamp(*value)
105 elif attr == "parents":
105 elif attr == "parents":
106 value = self._make_commits(value)
106 value = self._make_commits(value)
107 self.__dict__[attr] = value
107 self.__dict__[attr] = value
108
108
109 @LazyProperty
109 @LazyProperty
110 def _commit(self):
110 def _commit(self):
111 return self._remote[self.raw_id]
111 return self._remote[self.raw_id]
112
112
113 @LazyProperty
113 @LazyProperty
114 def _tree_id(self):
114 def _tree_id(self):
115 return self._remote[self._commit['tree']]['id']
115 return self._remote[self._commit['tree']]['id']
116
116
117 @LazyProperty
117 @LazyProperty
118 def id(self):
118 def id(self):
119 return self.raw_id
119 return self.raw_id
120
120
121 @LazyProperty
121 @LazyProperty
122 def short_id(self):
122 def short_id(self):
123 return self.raw_id[:12]
123 return self.raw_id[:12]
124
124
125 @LazyProperty
125 @LazyProperty
126 def message(self):
126 def message(self):
127 return safe_unicode(
127 return safe_unicode(
128 self._remote.commit_attribute(self.id, self._message_property))
128 self._remote.commit_attribute(self.id, self._message_property))
129
129
130 @LazyProperty
130 @LazyProperty
131 def committer(self):
131 def committer(self):
132 return safe_unicode(
132 return safe_unicode(
133 self._remote.commit_attribute(self.id, self._committer_property))
133 self._remote.commit_attribute(self.id, self._committer_property))
134
134
135 @LazyProperty
135 @LazyProperty
136 def author(self):
136 def author(self):
137 return safe_unicode(
137 return safe_unicode(
138 self._remote.commit_attribute(self.id, self._author_property))
138 self._remote.commit_attribute(self.id, self._author_property))
139
139
140 @LazyProperty
140 @LazyProperty
141 def date(self):
141 def date(self):
142 unix_ts, tz = self._remote.get_object_attrs(
142 unix_ts, tz = self._remote.get_object_attrs(
143 self.raw_id, self._date_property, self._date_tz_property)
143 self.raw_id, self._date_property, self._date_tz_property)
144 return utcdate_fromtimestamp(unix_ts, tz)
144 return utcdate_fromtimestamp(unix_ts, tz)
145
145
146 @LazyProperty
146 @LazyProperty
147 def status(self):
147 def status(self):
148 """
148 """
149 Returns modified, added, removed, deleted files for current commit
149 Returns modified, added, removed, deleted files for current commit
150 """
150 """
151 return self.changed, self.added, self.removed
151 return self.changed, self.added, self.removed
152
152
153 @LazyProperty
153 @LazyProperty
154 def tags(self):
154 def tags(self):
155 tags = [safe_unicode(name) for name,
155 tags = [safe_unicode(name) for name,
156 commit_id in self.repository.tags.iteritems()
156 commit_id in self.repository.tags.iteritems()
157 if commit_id == self.raw_id]
157 if commit_id == self.raw_id]
158 return tags
158 return tags
159
159
160 @LazyProperty
160 @LazyProperty
161 def branch(self):
161 def branch(self):
162 for name, commit_id in self.repository.branches.iteritems():
162 for name, commit_id in self.repository.branches.iteritems():
163 if commit_id == self.raw_id:
163 if commit_id == self.raw_id:
164 return safe_unicode(name)
164 return safe_unicode(name)
165 return None
165 return None
166
166
167 def _get_id_for_path(self, path):
167 def _get_id_for_path(self, path):
168 path = safe_str(path)
168 path = safe_str(path)
169 if path in self._paths:
169 if path in self._paths:
170 return self._paths[path]
170 return self._paths[path]
171
171
172 tree_id = self._tree_id
172 tree_id = self._tree_id
173
173
174 path = path.strip('/')
174 path = path.strip('/')
175 if path == '':
175 if path == '':
176 data = [tree_id, "tree"]
176 data = [tree_id, "tree"]
177 self._paths[''] = data
177 self._paths[''] = data
178 return data
178 return data
179
179
180 parts = path.split('/')
180 parts = path.split('/')
181 dirs, name = parts[:-1], parts[-1]
181 dirs, name = parts[:-1], parts[-1]
182 cur_dir = ''
182 cur_dir = ''
183
183
184 # initially extract things from root dir
184 # initially extract things from root dir
185 tree_items = self._remote.tree_items(tree_id)
185 tree_items = self._remote.tree_items(tree_id)
186 self._process_tree_items(tree_items, cur_dir)
186 self._process_tree_items(tree_items, cur_dir)
187
187
188 for dir in dirs:
188 for dir in dirs:
189 if cur_dir:
189 if cur_dir:
190 cur_dir = '/'.join((cur_dir, dir))
190 cur_dir = '/'.join((cur_dir, dir))
191 else:
191 else:
192 cur_dir = dir
192 cur_dir = dir
193 dir_id = None
193 dir_id = None
194 for item, stat_, id_, type_ in tree_items:
194 for item, stat_, id_, type_ in tree_items:
195 if item == dir:
195 if item == dir:
196 dir_id = id_
196 dir_id = id_
197 break
197 break
198 if dir_id:
198 if dir_id:
199 if type_ != "tree":
199 if type_ != "tree":
200 raise CommitError('%s is not a directory' % cur_dir)
200 raise CommitError('%s is not a directory' % cur_dir)
201 # update tree
201 # update tree
202 tree_items = self._remote.tree_items(dir_id)
202 tree_items = self._remote.tree_items(dir_id)
203 else:
203 else:
204 raise CommitError('%s have not been found' % cur_dir)
204 raise CommitError('%s have not been found' % cur_dir)
205
205
206 # cache all items from the given traversed tree
206 # cache all items from the given traversed tree
207 self._process_tree_items(tree_items, cur_dir)
207 self._process_tree_items(tree_items, cur_dir)
208
208
209 if path not in self._paths:
209 if path not in self._paths:
210 raise self.no_node_at_path(path)
210 raise self.no_node_at_path(path)
211
211
212 return self._paths[path]
212 return self._paths[path]
213
213
214 def _process_tree_items(self, items, cur_dir):
214 def _process_tree_items(self, items, cur_dir):
215 for item, stat_, id_, type_ in items:
215 for item, stat_, id_, type_ in items:
216 if cur_dir:
216 if cur_dir:
217 name = '/'.join((cur_dir, item))
217 name = '/'.join((cur_dir, item))
218 else:
218 else:
219 name = item
219 name = item
220 self._paths[name] = [id_, type_]
220 self._paths[name] = [id_, type_]
221 self._stat_modes[name] = stat_
221 self._stat_modes[name] = stat_
222
222
223 def _get_kind(self, path):
223 def _get_kind(self, path):
224 path_id, type_ = self._get_id_for_path(path)
224 path_id, type_ = self._get_id_for_path(path)
225 if type_ == 'blob':
225 if type_ == 'blob':
226 return NodeKind.FILE
226 return NodeKind.FILE
227 elif type_ == 'tree':
227 elif type_ == 'tree':
228 return NodeKind.DIR
228 return NodeKind.DIR
229 elif type == 'link':
229 elif type == 'link':
230 return NodeKind.SUBMODULE
230 return NodeKind.SUBMODULE
231 return None
231 return None
232
232
233 def _get_filectx(self, path):
233 def _get_filectx(self, path):
234 path = self._fix_path(path)
234 path = self._fix_path(path)
235 if self._get_kind(path) != NodeKind.FILE:
235 if self._get_kind(path) != NodeKind.FILE:
236 raise CommitError(
236 raise CommitError(
237 "File does not exist for commit %s at '%s'" %
237 "File does not exist for commit %s at '%s'" %
238 (self.raw_id, path))
238 (self.raw_id, path))
239 return path
239 return path
240
240
241 def _get_file_nodes(self):
241 def _get_file_nodes(self):
242 return chain(*(t[2] for t in self.walk()))
242 return chain(*(t[2] for t in self.walk()))
243
243
244 @LazyProperty
244 @LazyProperty
245 def parents(self):
245 def parents(self):
246 """
246 """
247 Returns list of parent commits.
247 Returns list of parent commits.
248 """
248 """
249 parent_ids = self._remote.commit_attribute(
249 parent_ids = self._remote.commit_attribute(
250 self.id, self._parents_property)
250 self.id, self._parents_property)
251 return self._make_commits(parent_ids)
251 return self._make_commits(parent_ids)
252
252
253 @LazyProperty
253 @LazyProperty
254 def children(self):
254 def children(self):
255 """
255 """
256 Returns list of child commits.
256 Returns list of child commits.
257 """
257 """
258 rev_filter = settings.GIT_REV_FILTER
258 rev_filter = settings.GIT_REV_FILTER
259 output, __ = self.repository.run_git_command(
259 output, __ = self.repository.run_git_command(
260 ['rev-list', '--children'] + rev_filter)
260 ['rev-list', '--children'] + rev_filter)
261
261
262 child_ids = []
262 child_ids = []
263 pat = re.compile(r'^%s' % self.raw_id)
263 pat = re.compile(r'^%s' % self.raw_id)
264 for l in output.splitlines():
264 for l in output.splitlines():
265 if pat.match(l):
265 if pat.match(l):
266 found_ids = l.split(' ')[1:]
266 found_ids = l.split(' ')[1:]
267 child_ids.extend(found_ids)
267 child_ids.extend(found_ids)
268 return self._make_commits(child_ids)
268 return self._make_commits(child_ids)
269
269
270 def _make_commits(self, commit_ids, pre_load=None):
270 def _make_commits(self, commit_ids, pre_load=None):
271 return [
271 return [
272 self.repository.get_commit(commit_id=commit_id, pre_load=pre_load)
272 self.repository.get_commit(commit_id=commit_id, pre_load=pre_load)
273 for commit_id in commit_ids]
273 for commit_id in commit_ids]
274
274
275 def get_file_mode(self, path):
275 def get_file_mode(self, path):
276 """
276 """
277 Returns stat mode of the file at the given `path`.
277 Returns stat mode of the file at the given `path`.
278 """
278 """
279 path = safe_str(path)
279 path = safe_str(path)
280 # ensure path is traversed
280 # ensure path is traversed
281 self._get_id_for_path(path)
281 self._get_id_for_path(path)
282 return self._stat_modes[path]
282 return self._stat_modes[path]
283
283
284 def is_link(self, path):
284 def is_link(self, path):
285 return stat.S_ISLNK(self.get_file_mode(path))
285 return stat.S_ISLNK(self.get_file_mode(path))
286
286
287 def get_file_content(self, path):
287 def get_file_content(self, path):
288 """
288 """
289 Returns content of the file at given `path`.
289 Returns content of the file at given `path`.
290 """
290 """
291 id_, _ = self._get_id_for_path(path)
291 id_, _ = self._get_id_for_path(path)
292 return self._remote.blob_as_pretty_string(id_)
292 return self._remote.blob_as_pretty_string(id_)
293
293
294 def get_file_size(self, path):
294 def get_file_size(self, path):
295 """
295 """
296 Returns size of the file at given `path`.
296 Returns size of the file at given `path`.
297 """
297 """
298 id_, _ = self._get_id_for_path(path)
298 id_, _ = self._get_id_for_path(path)
299 return self._remote.blob_raw_length(id_)
299 return self._remote.blob_raw_length(id_)
300
300
301 def get_file_history(self, path, limit=None, pre_load=None):
301 def get_path_history(self, path, limit=None, pre_load=None):
302 """
302 """
303 Returns history of file as reversed list of `GitCommit` objects for
303 Returns history of file as reversed list of `GitCommit` objects for
304 which file at given `path` has been modified.
304 which file at given `path` has been modified.
305
305
306 TODO: This function now uses an underlying 'git' command which works
306 TODO: This function now uses an underlying 'git' command which works
307 quickly but ideally we should replace with an algorithm.
307 quickly but ideally we should replace with an algorithm.
308 """
308 """
309 self._get_filectx(path)
309 self._get_filectx(path)
310 f_path = safe_str(path)
310 f_path = safe_str(path)
311
311
312 cmd = ['log']
312 cmd = ['log']
313 if limit:
313 if limit:
314 cmd.extend(['-n', str(safe_int(limit, 0))])
314 cmd.extend(['-n', str(safe_int(limit, 0))])
315 cmd.extend(['--pretty=format: %H', '-s', self.raw_id, '--', f_path])
315 cmd.extend(['--pretty=format: %H', '-s', self.raw_id, '--', f_path])
316
316
317 output, __ = self.repository.run_git_command(cmd)
317 output, __ = self.repository.run_git_command(cmd)
318 commit_ids = re.findall(r'[0-9a-fA-F]{40}', output)
318 commit_ids = re.findall(r'[0-9a-fA-F]{40}', output)
319
319
320 return [
320 return [
321 self.repository.get_commit(commit_id=commit_id, pre_load=pre_load)
321 self.repository.get_commit(commit_id=commit_id, pre_load=pre_load)
322 for commit_id in commit_ids]
322 for commit_id in commit_ids]
323
323
324 # TODO: unused for now potential replacement for subprocess
325 def get_file_history_2(self, path, limit=None, pre_load=None):
326 """
327 Returns history of file as reversed list of `Commit` objects for
328 which file at given `path` has been modified.
329 """
330 self._get_filectx(path)
331 f_path = safe_str(path)
332
333 commit_ids = self._remote.get_file_history(f_path, self.id, limit)
334
335 return [
336 self.repository.get_commit(commit_id=commit_id, pre_load=pre_load)
337 for commit_id in commit_ids]
338
339 def get_file_annotate(self, path, pre_load=None):
324 def get_file_annotate(self, path, pre_load=None):
340 """
325 """
341 Returns a generator of four element tuples with
326 Returns a generator of four element tuples with
342 lineno, commit_id, commit lazy loader and line
327 lineno, commit_id, commit lazy loader and line
343
328
344 TODO: This function now uses os underlying 'git' command which is
329 TODO: This function now uses os underlying 'git' command which is
345 generally not good. Should be replaced with algorithm iterating
330 generally not good. Should be replaced with algorithm iterating
346 commits.
331 commits.
347 """
332 """
348 cmd = ['blame', '-l', '--root', '-r', self.raw_id, '--', path]
333 cmd = ['blame', '-l', '--root', '-r', self.raw_id, '--', path]
349 # -l ==> outputs long shas (and we need all 40 characters)
334 # -l ==> outputs long shas (and we need all 40 characters)
350 # --root ==> doesn't put '^' character for bounderies
335 # --root ==> doesn't put '^' character for bounderies
351 # -r commit_id ==> blames for the given commit
336 # -r commit_id ==> blames for the given commit
352 output, __ = self.repository.run_git_command(cmd)
337 output, __ = self.repository.run_git_command(cmd)
353
338
354 for i, blame_line in enumerate(output.split('\n')[:-1]):
339 for i, blame_line in enumerate(output.split('\n')[:-1]):
355 line_no = i + 1
340 line_no = i + 1
356 commit_id, line = re.split(r' ', blame_line, 1)
341 commit_id, line = re.split(r' ', blame_line, 1)
357 yield (
342 yield (
358 line_no, commit_id,
343 line_no, commit_id,
359 lambda: self.repository.get_commit(commit_id=commit_id,
344 lambda: self.repository.get_commit(commit_id=commit_id,
360 pre_load=pre_load),
345 pre_load=pre_load),
361 line)
346 line)
362
347
363 def get_nodes(self, path):
348 def get_nodes(self, path):
364 if self._get_kind(path) != NodeKind.DIR:
349 if self._get_kind(path) != NodeKind.DIR:
365 raise CommitError(
350 raise CommitError(
366 "Directory does not exist for commit %s at "
351 "Directory does not exist for commit %s at "
367 " '%s'" % (self.raw_id, path))
352 " '%s'" % (self.raw_id, path))
368 path = self._fix_path(path)
353 path = self._fix_path(path)
369 id_, _ = self._get_id_for_path(path)
354 id_, _ = self._get_id_for_path(path)
370 tree_id = self._remote[id_]['id']
355 tree_id = self._remote[id_]['id']
371 dirnodes = []
356 dirnodes = []
372 filenodes = []
357 filenodes = []
373 alias = self.repository.alias
358 alias = self.repository.alias
374 for name, stat_, id_, type_ in self._remote.tree_items(tree_id):
359 for name, stat_, id_, type_ in self._remote.tree_items(tree_id):
375 if type_ == 'link':
360 if type_ == 'link':
376 url = self._get_submodule_url('/'.join((path, name)))
361 url = self._get_submodule_url('/'.join((path, name)))
377 dirnodes.append(SubModuleNode(
362 dirnodes.append(SubModuleNode(
378 name, url=url, commit=id_, alias=alias))
363 name, url=url, commit=id_, alias=alias))
379 continue
364 continue
380
365
381 if path != '':
366 if path != '':
382 obj_path = '/'.join((path, name))
367 obj_path = '/'.join((path, name))
383 else:
368 else:
384 obj_path = name
369 obj_path = name
385 if obj_path not in self._stat_modes:
370 if obj_path not in self._stat_modes:
386 self._stat_modes[obj_path] = stat_
371 self._stat_modes[obj_path] = stat_
387
372
388 if type_ == 'tree':
373 if type_ == 'tree':
389 dirnodes.append(DirNode(obj_path, commit=self))
374 dirnodes.append(DirNode(obj_path, commit=self))
390 elif type_ == 'blob':
375 elif type_ == 'blob':
391 filenodes.append(FileNode(obj_path, commit=self, mode=stat_))
376 filenodes.append(FileNode(obj_path, commit=self, mode=stat_))
392 else:
377 else:
393 raise CommitError(
378 raise CommitError(
394 "Requested object should be Tree or Blob, is %s", type_)
379 "Requested object should be Tree or Blob, is %s", type_)
395
380
396 nodes = dirnodes + filenodes
381 nodes = dirnodes + filenodes
397 for node in nodes:
382 for node in nodes:
398 if node.path not in self.nodes:
383 if node.path not in self.nodes:
399 self.nodes[node.path] = node
384 self.nodes[node.path] = node
400 nodes.sort()
385 nodes.sort()
401 return nodes
386 return nodes
402
387
403 def get_node(self, path, pre_load=None):
388 def get_node(self, path, pre_load=None):
404 if isinstance(path, unicode):
389 if isinstance(path, unicode):
405 path = path.encode('utf-8')
390 path = path.encode('utf-8')
406 path = self._fix_path(path)
391 path = self._fix_path(path)
407 if path not in self.nodes:
392 if path not in self.nodes:
408 try:
393 try:
409 id_, type_ = self._get_id_for_path(path)
394 id_, type_ = self._get_id_for_path(path)
410 except CommitError:
395 except CommitError:
411 raise NodeDoesNotExistError(
396 raise NodeDoesNotExistError(
412 "Cannot find one of parents' directories for a given "
397 "Cannot find one of parents' directories for a given "
413 "path: %s" % path)
398 "path: %s" % path)
414
399
415 if type_ == 'link':
400 if type_ == 'link':
416 url = self._get_submodule_url(path)
401 url = self._get_submodule_url(path)
417 node = SubModuleNode(path, url=url, commit=id_,
402 node = SubModuleNode(path, url=url, commit=id_,
418 alias=self.repository.alias)
403 alias=self.repository.alias)
419 elif type_ == 'tree':
404 elif type_ == 'tree':
420 if path == '':
405 if path == '':
421 node = RootNode(commit=self)
406 node = RootNode(commit=self)
422 else:
407 else:
423 node = DirNode(path, commit=self)
408 node = DirNode(path, commit=self)
424 elif type_ == 'blob':
409 elif type_ == 'blob':
425 node = FileNode(path, commit=self, pre_load=pre_load)
410 node = FileNode(path, commit=self, pre_load=pre_load)
426 else:
411 else:
427 raise self.no_node_at_path(path)
412 raise self.no_node_at_path(path)
428
413
429 # cache node
414 # cache node
430 self.nodes[path] = node
415 self.nodes[path] = node
431 return self.nodes[path]
416 return self.nodes[path]
432
417
433 def get_largefile_node(self, path):
418 def get_largefile_node(self, path):
434 id_, _ = self._get_id_for_path(path)
419 id_, _ = self._get_id_for_path(path)
435 pointer_spec = self._remote.is_large_file(id_)
420 pointer_spec = self._remote.is_large_file(id_)
436
421
437 if pointer_spec:
422 if pointer_spec:
438 # content of that file regular FileNode is the hash of largefile
423 # content of that file regular FileNode is the hash of largefile
439 file_id = pointer_spec.get('oid_hash')
424 file_id = pointer_spec.get('oid_hash')
440 if self._remote.in_largefiles_store(file_id):
425 if self._remote.in_largefiles_store(file_id):
441 lf_path = self._remote.store_path(file_id)
426 lf_path = self._remote.store_path(file_id)
442 return LargeFileNode(lf_path, commit=self, org_path=path)
427 return LargeFileNode(lf_path, commit=self, org_path=path)
443
428
444 @LazyProperty
429 @LazyProperty
445 def affected_files(self):
430 def affected_files(self):
446 """
431 """
447 Gets a fast accessible file changes for given commit
432 Gets a fast accessible file changes for given commit
448 """
433 """
449 added, modified, deleted = self._changes_cache
434 added, modified, deleted = self._changes_cache
450 return list(added.union(modified).union(deleted))
435 return list(added.union(modified).union(deleted))
451
436
452 @LazyProperty
437 @LazyProperty
453 def _changes_cache(self):
438 def _changes_cache(self):
454 added = set()
439 added = set()
455 modified = set()
440 modified = set()
456 deleted = set()
441 deleted = set()
457 _r = self._remote
442 _r = self._remote
458
443
459 parents = self.parents
444 parents = self.parents
460 if not self.parents:
445 if not self.parents:
461 parents = [base.EmptyCommit()]
446 parents = [base.EmptyCommit()]
462 for parent in parents:
447 for parent in parents:
463 if isinstance(parent, base.EmptyCommit):
448 if isinstance(parent, base.EmptyCommit):
464 oid = None
449 oid = None
465 else:
450 else:
466 oid = parent.raw_id
451 oid = parent.raw_id
467 changes = _r.tree_changes(oid, self.raw_id)
452 changes = _r.tree_changes(oid, self.raw_id)
468 for (oldpath, newpath), (_, _), (_, _) in changes:
453 for (oldpath, newpath), (_, _), (_, _) in changes:
469 if newpath and oldpath:
454 if newpath and oldpath:
470 modified.add(newpath)
455 modified.add(newpath)
471 elif newpath and not oldpath:
456 elif newpath and not oldpath:
472 added.add(newpath)
457 added.add(newpath)
473 elif not newpath and oldpath:
458 elif not newpath and oldpath:
474 deleted.add(oldpath)
459 deleted.add(oldpath)
475 return added, modified, deleted
460 return added, modified, deleted
476
461
477 def _get_paths_for_status(self, status):
462 def _get_paths_for_status(self, status):
478 """
463 """
479 Returns sorted list of paths for given ``status``.
464 Returns sorted list of paths for given ``status``.
480
465
481 :param status: one of: *added*, *modified* or *deleted*
466 :param status: one of: *added*, *modified* or *deleted*
482 """
467 """
483 added, modified, deleted = self._changes_cache
468 added, modified, deleted = self._changes_cache
484 return sorted({
469 return sorted({
485 'added': list(added),
470 'added': list(added),
486 'modified': list(modified),
471 'modified': list(modified),
487 'deleted': list(deleted)}[status]
472 'deleted': list(deleted)}[status]
488 )
473 )
489
474
490 @LazyProperty
475 @LazyProperty
491 def added(self):
476 def added(self):
492 """
477 """
493 Returns list of added ``FileNode`` objects.
478 Returns list of added ``FileNode`` objects.
494 """
479 """
495 if not self.parents:
480 if not self.parents:
496 return list(self._get_file_nodes())
481 return list(self._get_file_nodes())
497 return AddedFileNodesGenerator(
482 return AddedFileNodesGenerator(
498 [n for n in self._get_paths_for_status('added')], self)
483 [n for n in self._get_paths_for_status('added')], self)
499
484
500 @LazyProperty
485 @LazyProperty
501 def changed(self):
486 def changed(self):
502 """
487 """
503 Returns list of modified ``FileNode`` objects.
488 Returns list of modified ``FileNode`` objects.
504 """
489 """
505 if not self.parents:
490 if not self.parents:
506 return []
491 return []
507 return ChangedFileNodesGenerator(
492 return ChangedFileNodesGenerator(
508 [n for n in self._get_paths_for_status('modified')], self)
493 [n for n in self._get_paths_for_status('modified')], self)
509
494
510 @LazyProperty
495 @LazyProperty
511 def removed(self):
496 def removed(self):
512 """
497 """
513 Returns list of removed ``FileNode`` objects.
498 Returns list of removed ``FileNode`` objects.
514 """
499 """
515 if not self.parents:
500 if not self.parents:
516 return []
501 return []
517 return RemovedFileNodesGenerator(
502 return RemovedFileNodesGenerator(
518 [n for n in self._get_paths_for_status('deleted')], self)
503 [n for n in self._get_paths_for_status('deleted')], self)
519
504
520 def _get_submodule_url(self, submodule_path):
505 def _get_submodule_url(self, submodule_path):
521 git_modules_path = '.gitmodules'
506 git_modules_path = '.gitmodules'
522
507
523 if self._submodules is None:
508 if self._submodules is None:
524 self._submodules = {}
509 self._submodules = {}
525
510
526 try:
511 try:
527 submodules_node = self.get_node(git_modules_path)
512 submodules_node = self.get_node(git_modules_path)
528 except NodeDoesNotExistError:
513 except NodeDoesNotExistError:
529 return None
514 return None
530
515
531 content = submodules_node.content
516 content = submodules_node.content
532
517
533 # ConfigParser fails if there are whitespaces
518 # ConfigParser fails if there are whitespaces
534 content = '\n'.join(l.strip() for l in content.split('\n'))
519 content = '\n'.join(l.strip() for l in content.split('\n'))
535
520
536 parser = configparser.ConfigParser()
521 parser = configparser.ConfigParser()
537 parser.readfp(StringIO(content))
522 parser.readfp(StringIO(content))
538
523
539 for section in parser.sections():
524 for section in parser.sections():
540 path = parser.get(section, 'path')
525 path = parser.get(section, 'path')
541 url = parser.get(section, 'url')
526 url = parser.get(section, 'url')
542 if path and url:
527 if path and url:
543 self._submodules[path.strip('/')] = url
528 self._submodules[path.strip('/')] = url
544
529
545 return self._submodules.get(submodule_path.strip('/'))
530 return self._submodules.get(submodule_path.strip('/'))
@@ -1,388 +1,388 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2014-2018 RhodeCode GmbH
3 # Copyright (C) 2014-2018 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 HG commit module
22 HG commit module
23 """
23 """
24
24
25 import os
25 import os
26
26
27 from zope.cachedescriptors.property import Lazy as LazyProperty
27 from zope.cachedescriptors.property import Lazy as LazyProperty
28
28
29 from rhodecode.lib.datelib import utcdate_fromtimestamp
29 from rhodecode.lib.datelib import utcdate_fromtimestamp
30 from rhodecode.lib.utils import safe_str, safe_unicode
30 from rhodecode.lib.utils import safe_str, safe_unicode
31 from rhodecode.lib.vcs import path as vcspath
31 from rhodecode.lib.vcs import path as vcspath
32 from rhodecode.lib.vcs.backends import base
32 from rhodecode.lib.vcs.backends import base
33 from rhodecode.lib.vcs.backends.hg.diff import MercurialDiff
33 from rhodecode.lib.vcs.backends.hg.diff import MercurialDiff
34 from rhodecode.lib.vcs.exceptions import CommitError
34 from rhodecode.lib.vcs.exceptions import CommitError
35 from rhodecode.lib.vcs.nodes import (
35 from rhodecode.lib.vcs.nodes import (
36 AddedFileNodesGenerator, ChangedFileNodesGenerator, DirNode, FileNode,
36 AddedFileNodesGenerator, ChangedFileNodesGenerator, DirNode, FileNode,
37 NodeKind, RemovedFileNodesGenerator, RootNode, SubModuleNode,
37 NodeKind, RemovedFileNodesGenerator, RootNode, SubModuleNode,
38 LargeFileNode, LARGEFILE_PREFIX)
38 LargeFileNode, LARGEFILE_PREFIX)
39 from rhodecode.lib.vcs.utils.paths import get_dirs_for_path
39 from rhodecode.lib.vcs.utils.paths import get_dirs_for_path
40
40
41
41
42 class MercurialCommit(base.BaseCommit):
42 class MercurialCommit(base.BaseCommit):
43 """
43 """
44 Represents state of the repository at the single commit.
44 Represents state of the repository at the single commit.
45 """
45 """
46
46
47 _filter_pre_load = [
47 _filter_pre_load = [
48 # git specific property not supported here
48 # git specific property not supported here
49 "_commit",
49 "_commit",
50 ]
50 ]
51
51
52 def __init__(self, repository, raw_id, idx, pre_load=None):
52 def __init__(self, repository, raw_id, idx, pre_load=None):
53 raw_id = safe_str(raw_id)
53 raw_id = safe_str(raw_id)
54
54
55 self.repository = repository
55 self.repository = repository
56 self._remote = repository._remote
56 self._remote = repository._remote
57
57
58 self.raw_id = raw_id
58 self.raw_id = raw_id
59 self.idx = idx
59 self.idx = idx
60
60
61 self._set_bulk_properties(pre_load)
61 self._set_bulk_properties(pre_load)
62
62
63 # caches
63 # caches
64 self.nodes = {}
64 self.nodes = {}
65
65
66 def _set_bulk_properties(self, pre_load):
66 def _set_bulk_properties(self, pre_load):
67 if not pre_load:
67 if not pre_load:
68 return
68 return
69 pre_load = [entry for entry in pre_load
69 pre_load = [entry for entry in pre_load
70 if entry not in self._filter_pre_load]
70 if entry not in self._filter_pre_load]
71 if not pre_load:
71 if not pre_load:
72 return
72 return
73
73
74 result = self._remote.bulk_request(self.idx, pre_load)
74 result = self._remote.bulk_request(self.idx, pre_load)
75 for attr, value in result.items():
75 for attr, value in result.items():
76 if attr in ["author", "branch", "message"]:
76 if attr in ["author", "branch", "message"]:
77 value = safe_unicode(value)
77 value = safe_unicode(value)
78 elif attr == "affected_files":
78 elif attr == "affected_files":
79 value = map(safe_unicode, value)
79 value = map(safe_unicode, value)
80 elif attr == "date":
80 elif attr == "date":
81 value = utcdate_fromtimestamp(*value)
81 value = utcdate_fromtimestamp(*value)
82 elif attr in ["children", "parents"]:
82 elif attr in ["children", "parents"]:
83 value = self._make_commits(value)
83 value = self._make_commits(value)
84 elif attr in ["phase"]:
84 elif attr in ["phase"]:
85 value = self._get_phase_text(value)
85 value = self._get_phase_text(value)
86 self.__dict__[attr] = value
86 self.__dict__[attr] = value
87
87
88 @LazyProperty
88 @LazyProperty
89 def tags(self):
89 def tags(self):
90 tags = [name for name, commit_id in self.repository.tags.iteritems()
90 tags = [name for name, commit_id in self.repository.tags.iteritems()
91 if commit_id == self.raw_id]
91 if commit_id == self.raw_id]
92 return tags
92 return tags
93
93
94 @LazyProperty
94 @LazyProperty
95 def branch(self):
95 def branch(self):
96 return safe_unicode(self._remote.ctx_branch(self.idx))
96 return safe_unicode(self._remote.ctx_branch(self.idx))
97
97
98 @LazyProperty
98 @LazyProperty
99 def bookmarks(self):
99 def bookmarks(self):
100 bookmarks = [
100 bookmarks = [
101 name for name, commit_id in self.repository.bookmarks.iteritems()
101 name for name, commit_id in self.repository.bookmarks.iteritems()
102 if commit_id == self.raw_id]
102 if commit_id == self.raw_id]
103 return bookmarks
103 return bookmarks
104
104
105 @LazyProperty
105 @LazyProperty
106 def message(self):
106 def message(self):
107 return safe_unicode(self._remote.ctx_description(self.idx))
107 return safe_unicode(self._remote.ctx_description(self.idx))
108
108
109 @LazyProperty
109 @LazyProperty
110 def committer(self):
110 def committer(self):
111 return safe_unicode(self.author)
111 return safe_unicode(self.author)
112
112
113 @LazyProperty
113 @LazyProperty
114 def author(self):
114 def author(self):
115 return safe_unicode(self._remote.ctx_user(self.idx))
115 return safe_unicode(self._remote.ctx_user(self.idx))
116
116
117 @LazyProperty
117 @LazyProperty
118 def date(self):
118 def date(self):
119 return utcdate_fromtimestamp(*self._remote.ctx_date(self.idx))
119 return utcdate_fromtimestamp(*self._remote.ctx_date(self.idx))
120
120
121 @LazyProperty
121 @LazyProperty
122 def status(self):
122 def status(self):
123 """
123 """
124 Returns modified, added, removed, deleted files for current commit
124 Returns modified, added, removed, deleted files for current commit
125 """
125 """
126 return self._remote.ctx_status(self.idx)
126 return self._remote.ctx_status(self.idx)
127
127
128 @LazyProperty
128 @LazyProperty
129 def _file_paths(self):
129 def _file_paths(self):
130 return self._remote.ctx_list(self.idx)
130 return self._remote.ctx_list(self.idx)
131
131
132 @LazyProperty
132 @LazyProperty
133 def _dir_paths(self):
133 def _dir_paths(self):
134 p = list(set(get_dirs_for_path(*self._file_paths)))
134 p = list(set(get_dirs_for_path(*self._file_paths)))
135 p.insert(0, '')
135 p.insert(0, '')
136 return p
136 return p
137
137
138 @LazyProperty
138 @LazyProperty
139 def _paths(self):
139 def _paths(self):
140 return self._dir_paths + self._file_paths
140 return self._dir_paths + self._file_paths
141
141
142 @LazyProperty
142 @LazyProperty
143 def id(self):
143 def id(self):
144 if self.last:
144 if self.last:
145 return u'tip'
145 return u'tip'
146 return self.short_id
146 return self.short_id
147
147
148 @LazyProperty
148 @LazyProperty
149 def short_id(self):
149 def short_id(self):
150 return self.raw_id[:12]
150 return self.raw_id[:12]
151
151
152 def _make_commits(self, indexes, pre_load=None):
152 def _make_commits(self, indexes, pre_load=None):
153 return [self.repository.get_commit(commit_idx=idx, pre_load=pre_load)
153 return [self.repository.get_commit(commit_idx=idx, pre_load=pre_load)
154 for idx in indexes if idx >= 0]
154 for idx in indexes if idx >= 0]
155
155
156 @LazyProperty
156 @LazyProperty
157 def parents(self):
157 def parents(self):
158 """
158 """
159 Returns list of parent commits.
159 Returns list of parent commits.
160 """
160 """
161 parents = self._remote.ctx_parents(self.idx)
161 parents = self._remote.ctx_parents(self.idx)
162 return self._make_commits(parents)
162 return self._make_commits(parents)
163
163
164 def _get_phase_text(self, phase_id):
164 def _get_phase_text(self, phase_id):
165 return {
165 return {
166 0: 'public',
166 0: 'public',
167 1: 'draft',
167 1: 'draft',
168 2: 'secret',
168 2: 'secret',
169 }.get(phase_id) or ''
169 }.get(phase_id) or ''
170
170
171 @LazyProperty
171 @LazyProperty
172 def phase(self):
172 def phase(self):
173 phase_id = self._remote.ctx_phase(self.idx)
173 phase_id = self._remote.ctx_phase(self.idx)
174 phase_text = self._get_phase_text(phase_id)
174 phase_text = self._get_phase_text(phase_id)
175
175
176 return safe_unicode(phase_text)
176 return safe_unicode(phase_text)
177
177
178 @LazyProperty
178 @LazyProperty
179 def obsolete(self):
179 def obsolete(self):
180 obsolete = self._remote.ctx_obsolete(self.idx)
180 obsolete = self._remote.ctx_obsolete(self.idx)
181 return obsolete
181 return obsolete
182
182
183 @LazyProperty
183 @LazyProperty
184 def hidden(self):
184 def hidden(self):
185 hidden = self._remote.ctx_hidden(self.idx)
185 hidden = self._remote.ctx_hidden(self.idx)
186 return hidden
186 return hidden
187
187
188 @LazyProperty
188 @LazyProperty
189 def children(self):
189 def children(self):
190 """
190 """
191 Returns list of child commits.
191 Returns list of child commits.
192 """
192 """
193 children = self._remote.ctx_children(self.idx)
193 children = self._remote.ctx_children(self.idx)
194 return self._make_commits(children)
194 return self._make_commits(children)
195
195
196 def diff(self, ignore_whitespace=True, context=3):
196 def diff(self, ignore_whitespace=True, context=3):
197 result = self._remote.ctx_diff(
197 result = self._remote.ctx_diff(
198 self.idx,
198 self.idx,
199 git=True, ignore_whitespace=ignore_whitespace, context=context)
199 git=True, ignore_whitespace=ignore_whitespace, context=context)
200 diff = ''.join(result)
200 diff = ''.join(result)
201 return MercurialDiff(diff)
201 return MercurialDiff(diff)
202
202
203 def _fix_path(self, path):
203 def _fix_path(self, path):
204 """
204 """
205 Mercurial keeps filenodes as str so we need to encode from unicode
205 Mercurial keeps filenodes as str so we need to encode from unicode
206 to str.
206 to str.
207 """
207 """
208 return safe_str(super(MercurialCommit, self)._fix_path(path))
208 return safe_str(super(MercurialCommit, self)._fix_path(path))
209
209
210 def _get_kind(self, path):
210 def _get_kind(self, path):
211 path = self._fix_path(path)
211 path = self._fix_path(path)
212 if path in self._file_paths:
212 if path in self._file_paths:
213 return NodeKind.FILE
213 return NodeKind.FILE
214 elif path in self._dir_paths:
214 elif path in self._dir_paths:
215 return NodeKind.DIR
215 return NodeKind.DIR
216 else:
216 else:
217 raise CommitError(
217 raise CommitError(
218 "Node does not exist at the given path '%s'" % (path, ))
218 "Node does not exist at the given path '%s'" % (path, ))
219
219
220 def _get_filectx(self, path):
220 def _get_filectx(self, path):
221 path = self._fix_path(path)
221 path = self._fix_path(path)
222 if self._get_kind(path) != NodeKind.FILE:
222 if self._get_kind(path) != NodeKind.FILE:
223 raise CommitError(
223 raise CommitError(
224 "File does not exist for idx %s at '%s'" % (self.raw_id, path))
224 "File does not exist for idx %s at '%s'" % (self.raw_id, path))
225 return path
225 return path
226
226
227 def get_file_mode(self, path):
227 def get_file_mode(self, path):
228 """
228 """
229 Returns stat mode of the file at the given ``path``.
229 Returns stat mode of the file at the given ``path``.
230 """
230 """
231 path = self._get_filectx(path)
231 path = self._get_filectx(path)
232 if 'x' in self._remote.fctx_flags(self.idx, path):
232 if 'x' in self._remote.fctx_flags(self.idx, path):
233 return base.FILEMODE_EXECUTABLE
233 return base.FILEMODE_EXECUTABLE
234 else:
234 else:
235 return base.FILEMODE_DEFAULT
235 return base.FILEMODE_DEFAULT
236
236
237 def is_link(self, path):
237 def is_link(self, path):
238 path = self._get_filectx(path)
238 path = self._get_filectx(path)
239 return 'l' in self._remote.fctx_flags(self.idx, path)
239 return 'l' in self._remote.fctx_flags(self.idx, path)
240
240
241 def get_file_content(self, path):
241 def get_file_content(self, path):
242 """
242 """
243 Returns content of the file at given ``path``.
243 Returns content of the file at given ``path``.
244 """
244 """
245 path = self._get_filectx(path)
245 path = self._get_filectx(path)
246 return self._remote.fctx_data(self.idx, path)
246 return self._remote.fctx_data(self.idx, path)
247
247
248 def get_file_size(self, path):
248 def get_file_size(self, path):
249 """
249 """
250 Returns size of the file at given ``path``.
250 Returns size of the file at given ``path``.
251 """
251 """
252 path = self._get_filectx(path)
252 path = self._get_filectx(path)
253 return self._remote.fctx_size(self.idx, path)
253 return self._remote.fctx_size(self.idx, path)
254
254
255 def get_file_history(self, path, limit=None, pre_load=None):
255 def get_path_history(self, path, limit=None, pre_load=None):
256 """
256 """
257 Returns history of file as reversed list of `MercurialCommit` objects
257 Returns history of file as reversed list of `MercurialCommit` objects
258 for which file at given ``path`` has been modified.
258 for which file at given ``path`` has been modified.
259 """
259 """
260 path = self._get_filectx(path)
260 path = self._get_filectx(path)
261 hist = self._remote.file_history(self.idx, path, limit)
261 hist = self._remote.node_history(self.idx, path, limit)
262 return [
262 return [
263 self.repository.get_commit(commit_id=commit_id, pre_load=pre_load)
263 self.repository.get_commit(commit_id=commit_id, pre_load=pre_load)
264 for commit_id in hist]
264 for commit_id in hist]
265
265
266 def get_file_annotate(self, path, pre_load=None):
266 def get_file_annotate(self, path, pre_load=None):
267 """
267 """
268 Returns a generator of four element tuples with
268 Returns a generator of four element tuples with
269 lineno, commit_id, commit lazy loader and line
269 lineno, commit_id, commit lazy loader and line
270 """
270 """
271 result = self._remote.fctx_annotate(self.idx, path)
271 result = self._remote.fctx_annotate(self.idx, path)
272
272
273 for ln_no, commit_id, content in result:
273 for ln_no, commit_id, content in result:
274 yield (
274 yield (
275 ln_no, commit_id,
275 ln_no, commit_id,
276 lambda: self.repository.get_commit(commit_id=commit_id,
276 lambda: self.repository.get_commit(commit_id=commit_id,
277 pre_load=pre_load),
277 pre_load=pre_load),
278 content)
278 content)
279
279
280 def get_nodes(self, path):
280 def get_nodes(self, path):
281 """
281 """
282 Returns combined ``DirNode`` and ``FileNode`` objects list representing
282 Returns combined ``DirNode`` and ``FileNode`` objects list representing
283 state of commit at the given ``path``. If node at the given ``path``
283 state of commit at the given ``path``. If node at the given ``path``
284 is not instance of ``DirNode``, CommitError would be raised.
284 is not instance of ``DirNode``, CommitError would be raised.
285 """
285 """
286
286
287 if self._get_kind(path) != NodeKind.DIR:
287 if self._get_kind(path) != NodeKind.DIR:
288 raise CommitError(
288 raise CommitError(
289 "Directory does not exist for idx %s at '%s'" %
289 "Directory does not exist for idx %s at '%s'" %
290 (self.idx, path))
290 (self.idx, path))
291 path = self._fix_path(path)
291 path = self._fix_path(path)
292
292
293 filenodes = [
293 filenodes = [
294 FileNode(f, commit=self) for f in self._file_paths
294 FileNode(f, commit=self) for f in self._file_paths
295 if os.path.dirname(f) == path]
295 if os.path.dirname(f) == path]
296 # TODO: johbo: Check if this can be done in a more obvious way
296 # TODO: johbo: Check if this can be done in a more obvious way
297 dirs = path == '' and '' or [
297 dirs = path == '' and '' or [
298 d for d in self._dir_paths
298 d for d in self._dir_paths
299 if d and vcspath.dirname(d) == path]
299 if d and vcspath.dirname(d) == path]
300 dirnodes = [
300 dirnodes = [
301 DirNode(d, commit=self) for d in dirs
301 DirNode(d, commit=self) for d in dirs
302 if os.path.dirname(d) == path]
302 if os.path.dirname(d) == path]
303
303
304 alias = self.repository.alias
304 alias = self.repository.alias
305 for k, vals in self._submodules.iteritems():
305 for k, vals in self._submodules.iteritems():
306 loc = vals[0]
306 loc = vals[0]
307 commit = vals[1]
307 commit = vals[1]
308 dirnodes.append(
308 dirnodes.append(
309 SubModuleNode(k, url=loc, commit=commit, alias=alias))
309 SubModuleNode(k, url=loc, commit=commit, alias=alias))
310 nodes = dirnodes + filenodes
310 nodes = dirnodes + filenodes
311 # cache nodes
311 # cache nodes
312 for node in nodes:
312 for node in nodes:
313 self.nodes[node.path] = node
313 self.nodes[node.path] = node
314 nodes.sort()
314 nodes.sort()
315
315
316 return nodes
316 return nodes
317
317
318 def get_node(self, path, pre_load=None):
318 def get_node(self, path, pre_load=None):
319 """
319 """
320 Returns `Node` object from the given `path`. If there is no node at
320 Returns `Node` object from the given `path`. If there is no node at
321 the given `path`, `NodeDoesNotExistError` would be raised.
321 the given `path`, `NodeDoesNotExistError` would be raised.
322 """
322 """
323 path = self._fix_path(path)
323 path = self._fix_path(path)
324
324
325 if path not in self.nodes:
325 if path not in self.nodes:
326 if path in self._file_paths:
326 if path in self._file_paths:
327 node = FileNode(path, commit=self, pre_load=pre_load)
327 node = FileNode(path, commit=self, pre_load=pre_load)
328 elif path in self._dir_paths:
328 elif path in self._dir_paths:
329 if path == '':
329 if path == '':
330 node = RootNode(commit=self)
330 node = RootNode(commit=self)
331 else:
331 else:
332 node = DirNode(path, commit=self)
332 node = DirNode(path, commit=self)
333 else:
333 else:
334 raise self.no_node_at_path(path)
334 raise self.no_node_at_path(path)
335
335
336 # cache node
336 # cache node
337 self.nodes[path] = node
337 self.nodes[path] = node
338 return self.nodes[path]
338 return self.nodes[path]
339
339
340 def get_largefile_node(self, path):
340 def get_largefile_node(self, path):
341
341
342 if self._remote.is_large_file(path):
342 if self._remote.is_large_file(path):
343 # content of that file regular FileNode is the hash of largefile
343 # content of that file regular FileNode is the hash of largefile
344 file_id = self.get_file_content(path).strip()
344 file_id = self.get_file_content(path).strip()
345
345
346 if self._remote.in_largefiles_store(file_id):
346 if self._remote.in_largefiles_store(file_id):
347 lf_path = self._remote.store_path(file_id)
347 lf_path = self._remote.store_path(file_id)
348 return LargeFileNode(lf_path, commit=self, org_path=path)
348 return LargeFileNode(lf_path, commit=self, org_path=path)
349 elif self._remote.in_user_cache(file_id):
349 elif self._remote.in_user_cache(file_id):
350 lf_path = self._remote.store_path(file_id)
350 lf_path = self._remote.store_path(file_id)
351 self._remote.link(file_id, path)
351 self._remote.link(file_id, path)
352 return LargeFileNode(lf_path, commit=self, org_path=path)
352 return LargeFileNode(lf_path, commit=self, org_path=path)
353
353
354 @LazyProperty
354 @LazyProperty
355 def _submodules(self):
355 def _submodules(self):
356 """
356 """
357 Returns a dictionary with submodule information from substate file
357 Returns a dictionary with submodule information from substate file
358 of hg repository.
358 of hg repository.
359 """
359 """
360 return self._remote.ctx_substate(self.idx)
360 return self._remote.ctx_substate(self.idx)
361
361
362 @LazyProperty
362 @LazyProperty
363 def affected_files(self):
363 def affected_files(self):
364 """
364 """
365 Gets a fast accessible file changes for given commit
365 Gets a fast accessible file changes for given commit
366 """
366 """
367 return self._remote.ctx_files(self.idx)
367 return self._remote.ctx_files(self.idx)
368
368
369 @property
369 @property
370 def added(self):
370 def added(self):
371 """
371 """
372 Returns list of added ``FileNode`` objects.
372 Returns list of added ``FileNode`` objects.
373 """
373 """
374 return AddedFileNodesGenerator([n for n in self.status[1]], self)
374 return AddedFileNodesGenerator([n for n in self.status[1]], self)
375
375
376 @property
376 @property
377 def changed(self):
377 def changed(self):
378 """
378 """
379 Returns list of modified ``FileNode`` objects.
379 Returns list of modified ``FileNode`` objects.
380 """
380 """
381 return ChangedFileNodesGenerator([n for n in self.status[0]], self)
381 return ChangedFileNodesGenerator([n for n in self.status[0]], self)
382
382
383 @property
383 @property
384 def removed(self):
384 def removed(self):
385 """
385 """
386 Returns list of removed ``FileNode`` objects.
386 Returns list of removed ``FileNode`` objects.
387 """
387 """
388 return RemovedFileNodesGenerator([n for n in self.status[2]], self)
388 return RemovedFileNodesGenerator([n for n in self.status[2]], self)
@@ -1,236 +1,236 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2014-2018 RhodeCode GmbH
3 # Copyright (C) 2014-2018 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 SVN commit module
22 SVN commit module
23 """
23 """
24
24
25
25
26 import dateutil.parser
26 import dateutil.parser
27 from zope.cachedescriptors.property import Lazy as LazyProperty
27 from zope.cachedescriptors.property import Lazy as LazyProperty
28
28
29 from rhodecode.lib.utils import safe_str, safe_unicode
29 from rhodecode.lib.utils import safe_str, safe_unicode
30 from rhodecode.lib.vcs import nodes, path as vcspath
30 from rhodecode.lib.vcs import nodes, path as vcspath
31 from rhodecode.lib.vcs.backends import base
31 from rhodecode.lib.vcs.backends import base
32 from rhodecode.lib.vcs.exceptions import CommitError, NodeDoesNotExistError
32 from rhodecode.lib.vcs.exceptions import CommitError, NodeDoesNotExistError
33
33
34
34
35 _SVN_PROP_TRUE = '*'
35 _SVN_PROP_TRUE = '*'
36
36
37
37
38 class SubversionCommit(base.BaseCommit):
38 class SubversionCommit(base.BaseCommit):
39 """
39 """
40 Subversion specific implementation of commits
40 Subversion specific implementation of commits
41
41
42 .. attribute:: branch
42 .. attribute:: branch
43
43
44 The Subversion backend does not support to assign branches to
44 The Subversion backend does not support to assign branches to
45 specific commits. This attribute has always the value `None`.
45 specific commits. This attribute has always the value `None`.
46
46
47 """
47 """
48
48
49 def __init__(self, repository, commit_id):
49 def __init__(self, repository, commit_id):
50 self.repository = repository
50 self.repository = repository
51 self.idx = self.repository._get_commit_idx(commit_id)
51 self.idx = self.repository._get_commit_idx(commit_id)
52 self._svn_rev = self.idx + 1
52 self._svn_rev = self.idx + 1
53 self._remote = repository._remote
53 self._remote = repository._remote
54 # TODO: handling of raw_id should be a method on repository itself,
54 # TODO: handling of raw_id should be a method on repository itself,
55 # which knows how to translate commit index and commit id
55 # which knows how to translate commit index and commit id
56 self.raw_id = commit_id
56 self.raw_id = commit_id
57 self.short_id = commit_id
57 self.short_id = commit_id
58 self.id = 'r%s' % (commit_id, )
58 self.id = 'r%s' % (commit_id, )
59
59
60 # TODO: Implement the following placeholder attributes
60 # TODO: Implement the following placeholder attributes
61 self.nodes = {}
61 self.nodes = {}
62 self.tags = []
62 self.tags = []
63
63
64 @property
64 @property
65 def author(self):
65 def author(self):
66 return safe_unicode(self._properties.get('svn:author'))
66 return safe_unicode(self._properties.get('svn:author'))
67
67
68 @property
68 @property
69 def date(self):
69 def date(self):
70 return _date_from_svn_properties(self._properties)
70 return _date_from_svn_properties(self._properties)
71
71
72 @property
72 @property
73 def message(self):
73 def message(self):
74 return safe_unicode(self._properties.get('svn:log'))
74 return safe_unicode(self._properties.get('svn:log'))
75
75
76 @LazyProperty
76 @LazyProperty
77 def _properties(self):
77 def _properties(self):
78 return self._remote.revision_properties(self._svn_rev)
78 return self._remote.revision_properties(self._svn_rev)
79
79
80 @LazyProperty
80 @LazyProperty
81 def parents(self):
81 def parents(self):
82 parent_idx = self.idx - 1
82 parent_idx = self.idx - 1
83 if parent_idx >= 0:
83 if parent_idx >= 0:
84 parent = self.repository.get_commit(commit_idx=parent_idx)
84 parent = self.repository.get_commit(commit_idx=parent_idx)
85 return [parent]
85 return [parent]
86 return []
86 return []
87
87
88 @LazyProperty
88 @LazyProperty
89 def children(self):
89 def children(self):
90 child_idx = self.idx + 1
90 child_idx = self.idx + 1
91 if child_idx < len(self.repository.commit_ids):
91 if child_idx < len(self.repository.commit_ids):
92 child = self.repository.get_commit(commit_idx=child_idx)
92 child = self.repository.get_commit(commit_idx=child_idx)
93 return [child]
93 return [child]
94 return []
94 return []
95
95
96 def get_file_mode(self, path):
96 def get_file_mode(self, path):
97 # Note: Subversion flags files which are executable with a special
97 # Note: Subversion flags files which are executable with a special
98 # property `svn:executable` which is set to the value ``"*"``.
98 # property `svn:executable` which is set to the value ``"*"``.
99 if self._get_file_property(path, 'svn:executable') == _SVN_PROP_TRUE:
99 if self._get_file_property(path, 'svn:executable') == _SVN_PROP_TRUE:
100 return base.FILEMODE_EXECUTABLE
100 return base.FILEMODE_EXECUTABLE
101 else:
101 else:
102 return base.FILEMODE_DEFAULT
102 return base.FILEMODE_DEFAULT
103
103
104 def is_link(self, path):
104 def is_link(self, path):
105 # Note: Subversion has a flag for special files, the content of the
105 # Note: Subversion has a flag for special files, the content of the
106 # file contains the type of that file.
106 # file contains the type of that file.
107 if self._get_file_property(path, 'svn:special') == _SVN_PROP_TRUE:
107 if self._get_file_property(path, 'svn:special') == _SVN_PROP_TRUE:
108 return self.get_file_content(path).startswith('link')
108 return self.get_file_content(path).startswith('link')
109 return False
109 return False
110
110
111 def _get_file_property(self, path, name):
111 def _get_file_property(self, path, name):
112 file_properties = self._remote.node_properties(
112 file_properties = self._remote.node_properties(
113 safe_str(path), self._svn_rev)
113 safe_str(path), self._svn_rev)
114 return file_properties.get(name)
114 return file_properties.get(name)
115
115
116 def get_file_content(self, path):
116 def get_file_content(self, path):
117 path = self._fix_path(path)
117 path = self._fix_path(path)
118 return self._remote.get_file_content(safe_str(path), self._svn_rev)
118 return self._remote.get_file_content(safe_str(path), self._svn_rev)
119
119
120 def get_file_size(self, path):
120 def get_file_size(self, path):
121 path = self._fix_path(path)
121 path = self._fix_path(path)
122 return self._remote.get_file_size(safe_str(path), self._svn_rev)
122 return self._remote.get_file_size(safe_str(path), self._svn_rev)
123
123
124 def get_file_history(self, path, limit=None, pre_load=None):
124 def get_path_history(self, path, limit=None, pre_load=None):
125 path = safe_str(self._fix_path(path))
125 path = safe_str(self._fix_path(path))
126 history = self._remote.node_history(path, self._svn_rev, limit)
126 history = self._remote.node_history(path, self._svn_rev, limit)
127 return [
127 return [
128 self.repository.get_commit(commit_id=str(svn_rev))
128 self.repository.get_commit(commit_id=str(svn_rev))
129 for svn_rev in history]
129 for svn_rev in history]
130
130
131 def get_file_annotate(self, path, pre_load=None):
131 def get_file_annotate(self, path, pre_load=None):
132 result = self._remote.file_annotate(safe_str(path), self._svn_rev)
132 result = self._remote.file_annotate(safe_str(path), self._svn_rev)
133
133
134 for zero_based_line_no, svn_rev, content in result:
134 for zero_based_line_no, svn_rev, content in result:
135 commit_id = str(svn_rev)
135 commit_id = str(svn_rev)
136 line_no = zero_based_line_no + 1
136 line_no = zero_based_line_no + 1
137 yield (
137 yield (
138 line_no,
138 line_no,
139 commit_id,
139 commit_id,
140 lambda: self.repository.get_commit(commit_id=commit_id),
140 lambda: self.repository.get_commit(commit_id=commit_id),
141 content)
141 content)
142
142
143 def get_node(self, path, pre_load=None):
143 def get_node(self, path, pre_load=None):
144 path = self._fix_path(path)
144 path = self._fix_path(path)
145 if path not in self.nodes:
145 if path not in self.nodes:
146
146
147 if path == '':
147 if path == '':
148 node = nodes.RootNode(commit=self)
148 node = nodes.RootNode(commit=self)
149 else:
149 else:
150 node_type = self._remote.get_node_type(
150 node_type = self._remote.get_node_type(
151 safe_str(path), self._svn_rev)
151 safe_str(path), self._svn_rev)
152 if node_type == 'dir':
152 if node_type == 'dir':
153 node = nodes.DirNode(path, commit=self)
153 node = nodes.DirNode(path, commit=self)
154 elif node_type == 'file':
154 elif node_type == 'file':
155 node = nodes.FileNode(path, commit=self, pre_load=pre_load)
155 node = nodes.FileNode(path, commit=self, pre_load=pre_load)
156 else:
156 else:
157 raise self.no_node_at_path(path)
157 raise self.no_node_at_path(path)
158
158
159 self.nodes[path] = node
159 self.nodes[path] = node
160 return self.nodes[path]
160 return self.nodes[path]
161
161
162 def get_nodes(self, path):
162 def get_nodes(self, path):
163 if self._get_kind(path) != nodes.NodeKind.DIR:
163 if self._get_kind(path) != nodes.NodeKind.DIR:
164 raise CommitError(
164 raise CommitError(
165 "Directory does not exist for commit %s at "
165 "Directory does not exist for commit %s at "
166 " '%s'" % (self.raw_id, path))
166 " '%s'" % (self.raw_id, path))
167 path = self._fix_path(path)
167 path = self._fix_path(path)
168
168
169 path_nodes = []
169 path_nodes = []
170 for name, kind in self._remote.get_nodes(
170 for name, kind in self._remote.get_nodes(
171 safe_str(path), revision=self._svn_rev):
171 safe_str(path), revision=self._svn_rev):
172 node_path = vcspath.join(path, name)
172 node_path = vcspath.join(path, name)
173 if kind == 'dir':
173 if kind == 'dir':
174 node = nodes.DirNode(node_path, commit=self)
174 node = nodes.DirNode(node_path, commit=self)
175 elif kind == 'file':
175 elif kind == 'file':
176 node = nodes.FileNode(node_path, commit=self)
176 node = nodes.FileNode(node_path, commit=self)
177 else:
177 else:
178 raise ValueError("Node kind %s not supported." % (kind, ))
178 raise ValueError("Node kind %s not supported." % (kind, ))
179 self.nodes[node_path] = node
179 self.nodes[node_path] = node
180 path_nodes.append(node)
180 path_nodes.append(node)
181
181
182 return path_nodes
182 return path_nodes
183
183
184 def _get_kind(self, path):
184 def _get_kind(self, path):
185 path = self._fix_path(path)
185 path = self._fix_path(path)
186 kind = self._remote.get_node_type(path, self._svn_rev)
186 kind = self._remote.get_node_type(path, self._svn_rev)
187 if kind == 'file':
187 if kind == 'file':
188 return nodes.NodeKind.FILE
188 return nodes.NodeKind.FILE
189 elif kind == 'dir':
189 elif kind == 'dir':
190 return nodes.NodeKind.DIR
190 return nodes.NodeKind.DIR
191 else:
191 else:
192 raise CommitError(
192 raise CommitError(
193 "Node does not exist at the given path '%s'" % (path, ))
193 "Node does not exist at the given path '%s'" % (path, ))
194
194
195 @LazyProperty
195 @LazyProperty
196 def _changes_cache(self):
196 def _changes_cache(self):
197 return self._remote.revision_changes(self._svn_rev)
197 return self._remote.revision_changes(self._svn_rev)
198
198
199 @LazyProperty
199 @LazyProperty
200 def affected_files(self):
200 def affected_files(self):
201 changed_files = set()
201 changed_files = set()
202 for files in self._changes_cache.itervalues():
202 for files in self._changes_cache.itervalues():
203 changed_files.update(files)
203 changed_files.update(files)
204 return list(changed_files)
204 return list(changed_files)
205
205
206 @LazyProperty
206 @LazyProperty
207 def id(self):
207 def id(self):
208 return self.raw_id
208 return self.raw_id
209
209
210 @property
210 @property
211 def added(self):
211 def added(self):
212 return nodes.AddedFileNodesGenerator(
212 return nodes.AddedFileNodesGenerator(
213 self._changes_cache['added'], self)
213 self._changes_cache['added'], self)
214
214
215 @property
215 @property
216 def changed(self):
216 def changed(self):
217 return nodes.ChangedFileNodesGenerator(
217 return nodes.ChangedFileNodesGenerator(
218 self._changes_cache['changed'], self)
218 self._changes_cache['changed'], self)
219
219
220 @property
220 @property
221 def removed(self):
221 def removed(self):
222 return nodes.RemovedFileNodesGenerator(
222 return nodes.RemovedFileNodesGenerator(
223 self._changes_cache['removed'], self)
223 self._changes_cache['removed'], self)
224
224
225
225
226 def _date_from_svn_properties(properties):
226 def _date_from_svn_properties(properties):
227 """
227 """
228 Parses the date out of given svn properties.
228 Parses the date out of given svn properties.
229
229
230 :return: :class:`datetime.datetime` instance. The object is naive.
230 :return: :class:`datetime.datetime` instance. The object is naive.
231 """
231 """
232
232
233 aware_date = dateutil.parser.parse(properties.get('svn:date'))
233 aware_date = dateutil.parser.parse(properties.get('svn:date'))
234 # final_date = aware_date.astimezone(dateutil.tz.tzlocal())
234 # final_date = aware_date.astimezone(dateutil.tz.tzlocal())
235 final_date = aware_date
235 final_date = aware_date
236 return final_date.replace(tzinfo=None)
236 return final_date.replace(tzinfo=None)
@@ -1,812 +1,821 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2014-2018 RhodeCode GmbH
3 # Copyright (C) 2014-2018 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 Module holding everything related to vcs nodes, with vcs2 architecture.
22 Module holding everything related to vcs nodes, with vcs2 architecture.
23 """
23 """
24
24
25 import os
25 import os
26 import stat
26 import stat
27
27
28 from zope.cachedescriptors.property import Lazy as LazyProperty
28 from zope.cachedescriptors.property import Lazy as LazyProperty
29
29
30 from rhodecode.config.conf import LANGUAGES_EXTENSIONS_MAP
30 from rhodecode.config.conf import LANGUAGES_EXTENSIONS_MAP
31 from rhodecode.lib.utils import safe_unicode, safe_str
31 from rhodecode.lib.utils import safe_unicode, safe_str
32 from rhodecode.lib.utils2 import md5
32 from rhodecode.lib.utils2 import md5
33 from rhodecode.lib.vcs import path as vcspath
33 from rhodecode.lib.vcs import path as vcspath
34 from rhodecode.lib.vcs.backends.base import EmptyCommit, FILEMODE_DEFAULT
34 from rhodecode.lib.vcs.backends.base import EmptyCommit, FILEMODE_DEFAULT
35 from rhodecode.lib.vcs.conf.mtypes import get_mimetypes_db
35 from rhodecode.lib.vcs.conf.mtypes import get_mimetypes_db
36 from rhodecode.lib.vcs.exceptions import NodeError, RemovedFileNodeError
36 from rhodecode.lib.vcs.exceptions import NodeError, RemovedFileNodeError
37
37
38 LARGEFILE_PREFIX = '.hglf'
38 LARGEFILE_PREFIX = '.hglf'
39
39
40
40
41 class NodeKind:
41 class NodeKind:
42 SUBMODULE = -1
42 SUBMODULE = -1
43 DIR = 1
43 DIR = 1
44 FILE = 2
44 FILE = 2
45 LARGEFILE = 3
45 LARGEFILE = 3
46
46
47
47
48 class NodeState:
48 class NodeState:
49 ADDED = u'added'
49 ADDED = u'added'
50 CHANGED = u'changed'
50 CHANGED = u'changed'
51 NOT_CHANGED = u'not changed'
51 NOT_CHANGED = u'not changed'
52 REMOVED = u'removed'
52 REMOVED = u'removed'
53
53
54
54
55 class NodeGeneratorBase(object):
55 class NodeGeneratorBase(object):
56 """
56 """
57 Base class for removed added and changed filenodes, it's a lazy generator
57 Base class for removed added and changed filenodes, it's a lazy generator
58 class that will create filenodes only on iteration or call
58 class that will create filenodes only on iteration or call
59
59
60 The len method doesn't need to create filenodes at all
60 The len method doesn't need to create filenodes at all
61 """
61 """
62
62
63 def __init__(self, current_paths, cs):
63 def __init__(self, current_paths, cs):
64 self.cs = cs
64 self.cs = cs
65 self.current_paths = current_paths
65 self.current_paths = current_paths
66
66
67 def __call__(self):
67 def __call__(self):
68 return [n for n in self]
68 return [n for n in self]
69
69
70 def __getslice__(self, i, j):
70 def __getslice__(self, i, j):
71 for p in self.current_paths[i:j]:
71 for p in self.current_paths[i:j]:
72 yield self.cs.get_node(p)
72 yield self.cs.get_node(p)
73
73
74 def __len__(self):
74 def __len__(self):
75 return len(self.current_paths)
75 return len(self.current_paths)
76
76
77 def __iter__(self):
77 def __iter__(self):
78 for p in self.current_paths:
78 for p in self.current_paths:
79 yield self.cs.get_node(p)
79 yield self.cs.get_node(p)
80
80
81
81
82 class AddedFileNodesGenerator(NodeGeneratorBase):
82 class AddedFileNodesGenerator(NodeGeneratorBase):
83 """
83 """
84 Class holding added files for current commit
84 Class holding added files for current commit
85 """
85 """
86
86
87
87
88 class ChangedFileNodesGenerator(NodeGeneratorBase):
88 class ChangedFileNodesGenerator(NodeGeneratorBase):
89 """
89 """
90 Class holding changed files for current commit
90 Class holding changed files for current commit
91 """
91 """
92
92
93
93
94 class RemovedFileNodesGenerator(NodeGeneratorBase):
94 class RemovedFileNodesGenerator(NodeGeneratorBase):
95 """
95 """
96 Class holding removed files for current commit
96 Class holding removed files for current commit
97 """
97 """
98 def __iter__(self):
98 def __iter__(self):
99 for p in self.current_paths:
99 for p in self.current_paths:
100 yield RemovedFileNode(path=p)
100 yield RemovedFileNode(path=p)
101
101
102 def __getslice__(self, i, j):
102 def __getslice__(self, i, j):
103 for p in self.current_paths[i:j]:
103 for p in self.current_paths[i:j]:
104 yield RemovedFileNode(path=p)
104 yield RemovedFileNode(path=p)
105
105
106
106
107 class Node(object):
107 class Node(object):
108 """
108 """
109 Simplest class representing file or directory on repository. SCM backends
109 Simplest class representing file or directory on repository. SCM backends
110 should use ``FileNode`` and ``DirNode`` subclasses rather than ``Node``
110 should use ``FileNode`` and ``DirNode`` subclasses rather than ``Node``
111 directly.
111 directly.
112
112
113 Node's ``path`` cannot start with slash as we operate on *relative* paths
113 Node's ``path`` cannot start with slash as we operate on *relative* paths
114 only. Moreover, every single node is identified by the ``path`` attribute,
114 only. Moreover, every single node is identified by the ``path`` attribute,
115 so it cannot end with slash, too. Otherwise, path could lead to mistakes.
115 so it cannot end with slash, too. Otherwise, path could lead to mistakes.
116 """
116 """
117 RTLO_MARKER = u"\u202E" # RTLO marker allows swapping text, and certain
117 RTLO_MARKER = u"\u202E" # RTLO marker allows swapping text, and certain
118 # security attacks could be used with this
118 # security attacks could be used with this
119 commit = None
119 commit = None
120
120
121 def __init__(self, path, kind):
121 def __init__(self, path, kind):
122 self._validate_path(path) # can throw exception if path is invalid
122 self._validate_path(path) # can throw exception if path is invalid
123 self.path = safe_str(path.rstrip('/')) # we store paths as str
123 self.path = safe_str(path.rstrip('/')) # we store paths as str
124 if path == '' and kind != NodeKind.DIR:
124 if path == '' and kind != NodeKind.DIR:
125 raise NodeError("Only DirNode and its subclasses may be "
125 raise NodeError("Only DirNode and its subclasses may be "
126 "initialized with empty path")
126 "initialized with empty path")
127 self.kind = kind
127 self.kind = kind
128
128
129 if self.is_root() and not self.is_dir():
129 if self.is_root() and not self.is_dir():
130 raise NodeError("Root node cannot be FILE kind")
130 raise NodeError("Root node cannot be FILE kind")
131
131
132 def _validate_path(self, path):
132 def _validate_path(self, path):
133 if path.startswith('/'):
133 if path.startswith('/'):
134 raise NodeError(
134 raise NodeError(
135 "Cannot initialize Node objects with slash at "
135 "Cannot initialize Node objects with slash at "
136 "the beginning as only relative paths are supported. "
136 "the beginning as only relative paths are supported. "
137 "Got %s" % (path,))
137 "Got %s" % (path,))
138
138
139 @LazyProperty
139 @LazyProperty
140 def parent(self):
140 def parent(self):
141 parent_path = self.get_parent_path()
141 parent_path = self.get_parent_path()
142 if parent_path:
142 if parent_path:
143 if self.commit:
143 if self.commit:
144 return self.commit.get_node(parent_path)
144 return self.commit.get_node(parent_path)
145 return DirNode(parent_path)
145 return DirNode(parent_path)
146 return None
146 return None
147
147
148 @LazyProperty
148 @LazyProperty
149 def unicode_path(self):
149 def unicode_path(self):
150 return safe_unicode(self.path)
150 return safe_unicode(self.path)
151
151
152 @LazyProperty
152 @LazyProperty
153 def has_rtlo(self):
153 def has_rtlo(self):
154 """Detects if a path has right-to-left-override marker"""
154 """Detects if a path has right-to-left-override marker"""
155 return self.RTLO_MARKER in self.unicode_path
155 return self.RTLO_MARKER in self.unicode_path
156
156
157 @LazyProperty
157 @LazyProperty
158 def unicode_path_safe(self):
158 def unicode_path_safe(self):
159 """
159 """
160 Special SAFE representation of path without the right-to-left-override.
160 Special SAFE representation of path without the right-to-left-override.
161 This should be only used for "showing" the file, cannot be used for any
161 This should be only used for "showing" the file, cannot be used for any
162 urls etc.
162 urls etc.
163 """
163 """
164 return safe_unicode(self.path).replace(self.RTLO_MARKER, '')
164 return safe_unicode(self.path).replace(self.RTLO_MARKER, '')
165
165
166 @LazyProperty
166 @LazyProperty
167 def dir_path(self):
167 def dir_path(self):
168 """
168 """
169 Returns name of the directory from full path of this vcs node. Empty
169 Returns name of the directory from full path of this vcs node. Empty
170 string is returned if there's no directory in the path
170 string is returned if there's no directory in the path
171 """
171 """
172 _parts = self.path.rstrip('/').rsplit('/', 1)
172 _parts = self.path.rstrip('/').rsplit('/', 1)
173 if len(_parts) == 2:
173 if len(_parts) == 2:
174 return safe_unicode(_parts[0])
174 return safe_unicode(_parts[0])
175 return u''
175 return u''
176
176
177 @LazyProperty
177 @LazyProperty
178 def name(self):
178 def name(self):
179 """
179 """
180 Returns name of the node so if its path
180 Returns name of the node so if its path
181 then only last part is returned.
181 then only last part is returned.
182 """
182 """
183 return safe_unicode(self.path.rstrip('/').split('/')[-1])
183 return safe_unicode(self.path.rstrip('/').split('/')[-1])
184
184
185 @property
185 @property
186 def kind(self):
186 def kind(self):
187 return self._kind
187 return self._kind
188
188
189 @kind.setter
189 @kind.setter
190 def kind(self, kind):
190 def kind(self, kind):
191 if hasattr(self, '_kind'):
191 if hasattr(self, '_kind'):
192 raise NodeError("Cannot change node's kind")
192 raise NodeError("Cannot change node's kind")
193 else:
193 else:
194 self._kind = kind
194 self._kind = kind
195 # Post setter check (path's trailing slash)
195 # Post setter check (path's trailing slash)
196 if self.path.endswith('/'):
196 if self.path.endswith('/'):
197 raise NodeError("Node's path cannot end with slash")
197 raise NodeError("Node's path cannot end with slash")
198
198
199 def __cmp__(self, other):
199 def __cmp__(self, other):
200 """
200 """
201 Comparator using name of the node, needed for quick list sorting.
201 Comparator using name of the node, needed for quick list sorting.
202 """
202 """
203 kind_cmp = cmp(self.kind, other.kind)
203 kind_cmp = cmp(self.kind, other.kind)
204 if kind_cmp:
204 if kind_cmp:
205 return kind_cmp
205 return kind_cmp
206 return cmp(self.name, other.name)
206 return cmp(self.name, other.name)
207
207
208 def __eq__(self, other):
208 def __eq__(self, other):
209 for attr in ['name', 'path', 'kind']:
209 for attr in ['name', 'path', 'kind']:
210 if getattr(self, attr) != getattr(other, attr):
210 if getattr(self, attr) != getattr(other, attr):
211 return False
211 return False
212 if self.is_file():
212 if self.is_file():
213 if self.content != other.content:
213 if self.content != other.content:
214 return False
214 return False
215 else:
215 else:
216 # For DirNode's check without entering each dir
216 # For DirNode's check without entering each dir
217 self_nodes_paths = list(sorted(n.path for n in self.nodes))
217 self_nodes_paths = list(sorted(n.path for n in self.nodes))
218 other_nodes_paths = list(sorted(n.path for n in self.nodes))
218 other_nodes_paths = list(sorted(n.path for n in self.nodes))
219 if self_nodes_paths != other_nodes_paths:
219 if self_nodes_paths != other_nodes_paths:
220 return False
220 return False
221 return True
221 return True
222
222
223 def __ne__(self, other):
223 def __ne__(self, other):
224 return not self.__eq__(other)
224 return not self.__eq__(other)
225
225
226 def __repr__(self):
226 def __repr__(self):
227 return '<%s %r>' % (self.__class__.__name__, self.path)
227 return '<%s %r>' % (self.__class__.__name__, self.path)
228
228
229 def __str__(self):
229 def __str__(self):
230 return self.__repr__()
230 return self.__repr__()
231
231
232 def __unicode__(self):
232 def __unicode__(self):
233 return self.name
233 return self.name
234
234
235 def get_parent_path(self):
235 def get_parent_path(self):
236 """
236 """
237 Returns node's parent path or empty string if node is root.
237 Returns node's parent path or empty string if node is root.
238 """
238 """
239 if self.is_root():
239 if self.is_root():
240 return ''
240 return ''
241 return vcspath.dirname(self.path.rstrip('/')) + '/'
241 return vcspath.dirname(self.path.rstrip('/')) + '/'
242
242
243 def is_file(self):
243 def is_file(self):
244 """
244 """
245 Returns ``True`` if node's kind is ``NodeKind.FILE``, ``False``
245 Returns ``True`` if node's kind is ``NodeKind.FILE``, ``False``
246 otherwise.
246 otherwise.
247 """
247 """
248 return self.kind == NodeKind.FILE
248 return self.kind == NodeKind.FILE
249
249
250 def is_dir(self):
250 def is_dir(self):
251 """
251 """
252 Returns ``True`` if node's kind is ``NodeKind.DIR``, ``False``
252 Returns ``True`` if node's kind is ``NodeKind.DIR``, ``False``
253 otherwise.
253 otherwise.
254 """
254 """
255 return self.kind == NodeKind.DIR
255 return self.kind == NodeKind.DIR
256
256
257 def is_root(self):
257 def is_root(self):
258 """
258 """
259 Returns ``True`` if node is a root node and ``False`` otherwise.
259 Returns ``True`` if node is a root node and ``False`` otherwise.
260 """
260 """
261 return self.kind == NodeKind.DIR and self.path == ''
261 return self.kind == NodeKind.DIR and self.path == ''
262
262
263 def is_submodule(self):
263 def is_submodule(self):
264 """
264 """
265 Returns ``True`` if node's kind is ``NodeKind.SUBMODULE``, ``False``
265 Returns ``True`` if node's kind is ``NodeKind.SUBMODULE``, ``False``
266 otherwise.
266 otherwise.
267 """
267 """
268 return self.kind == NodeKind.SUBMODULE
268 return self.kind == NodeKind.SUBMODULE
269
269
270 def is_largefile(self):
270 def is_largefile(self):
271 """
271 """
272 Returns ``True`` if node's kind is ``NodeKind.LARGEFILE``, ``False``
272 Returns ``True`` if node's kind is ``NodeKind.LARGEFILE``, ``False``
273 otherwise
273 otherwise
274 """
274 """
275 return self.kind == NodeKind.LARGEFILE
275 return self.kind == NodeKind.LARGEFILE
276
276
277 def is_link(self):
277 def is_link(self):
278 if self.commit:
278 if self.commit:
279 return self.commit.is_link(self.path)
279 return self.commit.is_link(self.path)
280 return False
280 return False
281
281
282 @LazyProperty
282 @LazyProperty
283 def added(self):
283 def added(self):
284 return self.state is NodeState.ADDED
284 return self.state is NodeState.ADDED
285
285
286 @LazyProperty
286 @LazyProperty
287 def changed(self):
287 def changed(self):
288 return self.state is NodeState.CHANGED
288 return self.state is NodeState.CHANGED
289
289
290 @LazyProperty
290 @LazyProperty
291 def not_changed(self):
291 def not_changed(self):
292 return self.state is NodeState.NOT_CHANGED
292 return self.state is NodeState.NOT_CHANGED
293
293
294 @LazyProperty
294 @LazyProperty
295 def removed(self):
295 def removed(self):
296 return self.state is NodeState.REMOVED
296 return self.state is NodeState.REMOVED
297
297
298
298
299 class FileNode(Node):
299 class FileNode(Node):
300 """
300 """
301 Class representing file nodes.
301 Class representing file nodes.
302
302
303 :attribute: path: path to the node, relative to repository's root
303 :attribute: path: path to the node, relative to repository's root
304 :attribute: content: if given arbitrary sets content of the file
304 :attribute: content: if given arbitrary sets content of the file
305 :attribute: commit: if given, first time content is accessed, callback
305 :attribute: commit: if given, first time content is accessed, callback
306 :attribute: mode: stat mode for a node. Default is `FILEMODE_DEFAULT`.
306 :attribute: mode: stat mode for a node. Default is `FILEMODE_DEFAULT`.
307 """
307 """
308 _filter_pre_load = []
308 _filter_pre_load = []
309
309
310 def __init__(self, path, content=None, commit=None, mode=None, pre_load=None):
310 def __init__(self, path, content=None, commit=None, mode=None, pre_load=None):
311 """
311 """
312 Only one of ``content`` and ``commit`` may be given. Passing both
312 Only one of ``content`` and ``commit`` may be given. Passing both
313 would raise ``NodeError`` exception.
313 would raise ``NodeError`` exception.
314
314
315 :param path: relative path to the node
315 :param path: relative path to the node
316 :param content: content may be passed to constructor
316 :param content: content may be passed to constructor
317 :param commit: if given, will use it to lazily fetch content
317 :param commit: if given, will use it to lazily fetch content
318 :param mode: ST_MODE (i.e. 0100644)
318 :param mode: ST_MODE (i.e. 0100644)
319 """
319 """
320 if content and commit:
320 if content and commit:
321 raise NodeError("Cannot use both content and commit")
321 raise NodeError("Cannot use both content and commit")
322 super(FileNode, self).__init__(path, kind=NodeKind.FILE)
322 super(FileNode, self).__init__(path, kind=NodeKind.FILE)
323 self.commit = commit
323 self.commit = commit
324 self._content = content
324 self._content = content
325 self._mode = mode or FILEMODE_DEFAULT
325 self._mode = mode or FILEMODE_DEFAULT
326
326
327 self._set_bulk_properties(pre_load)
327 self._set_bulk_properties(pre_load)
328
328
329 def _set_bulk_properties(self, pre_load):
329 def _set_bulk_properties(self, pre_load):
330 if not pre_load:
330 if not pre_load:
331 return
331 return
332 pre_load = [entry for entry in pre_load
332 pre_load = [entry for entry in pre_load
333 if entry not in self._filter_pre_load]
333 if entry not in self._filter_pre_load]
334 if not pre_load:
334 if not pre_load:
335 return
335 return
336
336
337 for attr_name in pre_load:
337 for attr_name in pre_load:
338 result = getattr(self, attr_name)
338 result = getattr(self, attr_name)
339 if callable(result):
339 if callable(result):
340 result = result()
340 result = result()
341 self.__dict__[attr_name] = result
341 self.__dict__[attr_name] = result
342
342
343 @LazyProperty
343 @LazyProperty
344 def mode(self):
344 def mode(self):
345 """
345 """
346 Returns lazily mode of the FileNode. If `commit` is not set, would
346 Returns lazily mode of the FileNode. If `commit` is not set, would
347 use value given at initialization or `FILEMODE_DEFAULT` (default).
347 use value given at initialization or `FILEMODE_DEFAULT` (default).
348 """
348 """
349 if self.commit:
349 if self.commit:
350 mode = self.commit.get_file_mode(self.path)
350 mode = self.commit.get_file_mode(self.path)
351 else:
351 else:
352 mode = self._mode
352 mode = self._mode
353 return mode
353 return mode
354
354
355 @LazyProperty
355 @LazyProperty
356 def raw_bytes(self):
356 def raw_bytes(self):
357 """
357 """
358 Returns lazily the raw bytes of the FileNode.
358 Returns lazily the raw bytes of the FileNode.
359 """
359 """
360 if self.commit:
360 if self.commit:
361 if self._content is None:
361 if self._content is None:
362 self._content = self.commit.get_file_content(self.path)
362 self._content = self.commit.get_file_content(self.path)
363 content = self._content
363 content = self._content
364 else:
364 else:
365 content = self._content
365 content = self._content
366 return content
366 return content
367
367
368 @LazyProperty
368 @LazyProperty
369 def md5(self):
369 def md5(self):
370 """
370 """
371 Returns md5 of the file node.
371 Returns md5 of the file node.
372 """
372 """
373 return md5(self.raw_bytes)
373 return md5(self.raw_bytes)
374
374
375 @LazyProperty
375 @LazyProperty
376 def content(self):
376 def content(self):
377 """
377 """
378 Returns lazily content of the FileNode. If possible, would try to
378 Returns lazily content of the FileNode. If possible, would try to
379 decode content from UTF-8.
379 decode content from UTF-8.
380 """
380 """
381 content = self.raw_bytes
381 content = self.raw_bytes
382
382
383 if self.is_binary:
383 if self.is_binary:
384 return content
384 return content
385 return safe_unicode(content)
385 return safe_unicode(content)
386
386
387 @LazyProperty
387 @LazyProperty
388 def size(self):
388 def size(self):
389 if self.commit:
389 if self.commit:
390 return self.commit.get_file_size(self.path)
390 return self.commit.get_file_size(self.path)
391 raise NodeError(
391 raise NodeError(
392 "Cannot retrieve size of the file without related "
392 "Cannot retrieve size of the file without related "
393 "commit attribute")
393 "commit attribute")
394
394
395 @LazyProperty
395 @LazyProperty
396 def message(self):
396 def message(self):
397 if self.commit:
397 if self.commit:
398 return self.last_commit.message
398 return self.last_commit.message
399 raise NodeError(
399 raise NodeError(
400 "Cannot retrieve message of the file without related "
400 "Cannot retrieve message of the file without related "
401 "commit attribute")
401 "commit attribute")
402
402
403 @LazyProperty
403 @LazyProperty
404 def last_commit(self):
404 def last_commit(self):
405 if self.commit:
405 if self.commit:
406 pre_load = ["author", "date", "message"]
406 pre_load = ["author", "date", "message"]
407 return self.commit.get_file_commit(self.path, pre_load=pre_load)
407 return self.commit.get_path_commit(self.path, pre_load=pre_load)
408 raise NodeError(
408 raise NodeError(
409 "Cannot retrieve last commit of the file without "
409 "Cannot retrieve last commit of the file without "
410 "related commit attribute")
410 "related commit attribute")
411
411
412 def get_mimetype(self):
412 def get_mimetype(self):
413 """
413 """
414 Mimetype is calculated based on the file's content. If ``_mimetype``
414 Mimetype is calculated based on the file's content. If ``_mimetype``
415 attribute is available, it will be returned (backends which store
415 attribute is available, it will be returned (backends which store
416 mimetypes or can easily recognize them, should set this private
416 mimetypes or can easily recognize them, should set this private
417 attribute to indicate that type should *NOT* be calculated).
417 attribute to indicate that type should *NOT* be calculated).
418 """
418 """
419
419
420 if hasattr(self, '_mimetype'):
420 if hasattr(self, '_mimetype'):
421 if (isinstance(self._mimetype, (tuple, list,)) and
421 if (isinstance(self._mimetype, (tuple, list,)) and
422 len(self._mimetype) == 2):
422 len(self._mimetype) == 2):
423 return self._mimetype
423 return self._mimetype
424 else:
424 else:
425 raise NodeError('given _mimetype attribute must be an 2 '
425 raise NodeError('given _mimetype attribute must be an 2 '
426 'element list or tuple')
426 'element list or tuple')
427
427
428 db = get_mimetypes_db()
428 db = get_mimetypes_db()
429 mtype, encoding = db.guess_type(self.name)
429 mtype, encoding = db.guess_type(self.name)
430
430
431 if mtype is None:
431 if mtype is None:
432 if self.is_binary:
432 if self.is_binary:
433 mtype = 'application/octet-stream'
433 mtype = 'application/octet-stream'
434 encoding = None
434 encoding = None
435 else:
435 else:
436 mtype = 'text/plain'
436 mtype = 'text/plain'
437 encoding = None
437 encoding = None
438
438
439 # try with pygments
439 # try with pygments
440 try:
440 try:
441 from pygments.lexers import get_lexer_for_filename
441 from pygments.lexers import get_lexer_for_filename
442 mt = get_lexer_for_filename(self.name).mimetypes
442 mt = get_lexer_for_filename(self.name).mimetypes
443 except Exception:
443 except Exception:
444 mt = None
444 mt = None
445
445
446 if mt:
446 if mt:
447 mtype = mt[0]
447 mtype = mt[0]
448
448
449 return mtype, encoding
449 return mtype, encoding
450
450
451 @LazyProperty
451 @LazyProperty
452 def mimetype(self):
452 def mimetype(self):
453 """
453 """
454 Wrapper around full mimetype info. It returns only type of fetched
454 Wrapper around full mimetype info. It returns only type of fetched
455 mimetype without the encoding part. use get_mimetype function to fetch
455 mimetype without the encoding part. use get_mimetype function to fetch
456 full set of (type,encoding)
456 full set of (type,encoding)
457 """
457 """
458 return self.get_mimetype()[0]
458 return self.get_mimetype()[0]
459
459
460 @LazyProperty
460 @LazyProperty
461 def mimetype_main(self):
461 def mimetype_main(self):
462 return self.mimetype.split('/')[0]
462 return self.mimetype.split('/')[0]
463
463
464 @classmethod
464 @classmethod
465 def get_lexer(cls, filename, content=None):
465 def get_lexer(cls, filename, content=None):
466 from pygments import lexers
466 from pygments import lexers
467
467
468 extension = filename.split('.')[-1]
468 extension = filename.split('.')[-1]
469 lexer = None
469 lexer = None
470
470
471 try:
471 try:
472 lexer = lexers.guess_lexer_for_filename(
472 lexer = lexers.guess_lexer_for_filename(
473 filename, content, stripnl=False)
473 filename, content, stripnl=False)
474 except lexers.ClassNotFound:
474 except lexers.ClassNotFound:
475 lexer = None
475 lexer = None
476
476
477 # try our EXTENSION_MAP
477 # try our EXTENSION_MAP
478 if not lexer:
478 if not lexer:
479 try:
479 try:
480 lexer_class = LANGUAGES_EXTENSIONS_MAP.get(extension)
480 lexer_class = LANGUAGES_EXTENSIONS_MAP.get(extension)
481 if lexer_class:
481 if lexer_class:
482 lexer = lexers.get_lexer_by_name(lexer_class[0])
482 lexer = lexers.get_lexer_by_name(lexer_class[0])
483 except lexers.ClassNotFound:
483 except lexers.ClassNotFound:
484 lexer = None
484 lexer = None
485
485
486 if not lexer:
486 if not lexer:
487 lexer = lexers.TextLexer(stripnl=False)
487 lexer = lexers.TextLexer(stripnl=False)
488
488
489 return lexer
489 return lexer
490
490
491 @LazyProperty
491 @LazyProperty
492 def lexer(self):
492 def lexer(self):
493 """
493 """
494 Returns pygment's lexer class. Would try to guess lexer taking file's
494 Returns pygment's lexer class. Would try to guess lexer taking file's
495 content, name and mimetype.
495 content, name and mimetype.
496 """
496 """
497 return self.get_lexer(self.name, self.content)
497 return self.get_lexer(self.name, self.content)
498
498
499 @LazyProperty
499 @LazyProperty
500 def lexer_alias(self):
500 def lexer_alias(self):
501 """
501 """
502 Returns first alias of the lexer guessed for this file.
502 Returns first alias of the lexer guessed for this file.
503 """
503 """
504 return self.lexer.aliases[0]
504 return self.lexer.aliases[0]
505
505
506 @LazyProperty
506 @LazyProperty
507 def history(self):
507 def history(self):
508 """
508 """
509 Returns a list of commit for this file in which the file was changed
509 Returns a list of commit for this file in which the file was changed
510 """
510 """
511 if self.commit is None:
511 if self.commit is None:
512 raise NodeError('Unable to get commit for this FileNode')
512 raise NodeError('Unable to get commit for this FileNode')
513 return self.commit.get_file_history(self.path)
513 return self.commit.get_path_history(self.path)
514
514
515 @LazyProperty
515 @LazyProperty
516 def annotate(self):
516 def annotate(self):
517 """
517 """
518 Returns a list of three element tuples with lineno, commit and line
518 Returns a list of three element tuples with lineno, commit and line
519 """
519 """
520 if self.commit is None:
520 if self.commit is None:
521 raise NodeError('Unable to get commit for this FileNode')
521 raise NodeError('Unable to get commit for this FileNode')
522 pre_load = ["author", "date", "message"]
522 pre_load = ["author", "date", "message"]
523 return self.commit.get_file_annotate(self.path, pre_load=pre_load)
523 return self.commit.get_file_annotate(self.path, pre_load=pre_load)
524
524
525 @LazyProperty
525 @LazyProperty
526 def state(self):
526 def state(self):
527 if not self.commit:
527 if not self.commit:
528 raise NodeError(
528 raise NodeError(
529 "Cannot check state of the node if it's not "
529 "Cannot check state of the node if it's not "
530 "linked with commit")
530 "linked with commit")
531 elif self.path in (node.path for node in self.commit.added):
531 elif self.path in (node.path for node in self.commit.added):
532 return NodeState.ADDED
532 return NodeState.ADDED
533 elif self.path in (node.path for node in self.commit.changed):
533 elif self.path in (node.path for node in self.commit.changed):
534 return NodeState.CHANGED
534 return NodeState.CHANGED
535 else:
535 else:
536 return NodeState.NOT_CHANGED
536 return NodeState.NOT_CHANGED
537
537
538 @LazyProperty
538 @LazyProperty
539 def is_binary(self):
539 def is_binary(self):
540 """
540 """
541 Returns True if file has binary content.
541 Returns True if file has binary content.
542 """
542 """
543 _bin = self.raw_bytes and '\0' in self.raw_bytes
543 _bin = self.raw_bytes and '\0' in self.raw_bytes
544 return _bin
544 return _bin
545
545
546 @LazyProperty
546 @LazyProperty
547 def extension(self):
547 def extension(self):
548 """Returns filenode extension"""
548 """Returns filenode extension"""
549 return self.name.split('.')[-1]
549 return self.name.split('.')[-1]
550
550
551 @property
551 @property
552 def is_executable(self):
552 def is_executable(self):
553 """
553 """
554 Returns ``True`` if file has executable flag turned on.
554 Returns ``True`` if file has executable flag turned on.
555 """
555 """
556 return bool(self.mode & stat.S_IXUSR)
556 return bool(self.mode & stat.S_IXUSR)
557
557
558 def get_largefile_node(self):
558 def get_largefile_node(self):
559 """
559 """
560 Try to return a Mercurial FileNode from this node. It does internal
560 Try to return a Mercurial FileNode from this node. It does internal
561 checks inside largefile store, if that file exist there it will
561 checks inside largefile store, if that file exist there it will
562 create special instance of LargeFileNode which can get content from
562 create special instance of LargeFileNode which can get content from
563 LF store.
563 LF store.
564 """
564 """
565 if self.commit:
565 if self.commit:
566 return self.commit.get_largefile_node(self.path)
566 return self.commit.get_largefile_node(self.path)
567
567
568 def lines(self, count_empty=False):
568 def lines(self, count_empty=False):
569 all_lines, empty_lines = 0, 0
569 all_lines, empty_lines = 0, 0
570
570
571 if not self.is_binary:
571 if not self.is_binary:
572 content = self.content
572 content = self.content
573 if count_empty:
573 if count_empty:
574 all_lines = 0
574 all_lines = 0
575 empty_lines = 0
575 empty_lines = 0
576 for line in content.splitlines(True):
576 for line in content.splitlines(True):
577 if line == '\n':
577 if line == '\n':
578 empty_lines += 1
578 empty_lines += 1
579 all_lines += 1
579 all_lines += 1
580
580
581 return all_lines, all_lines - empty_lines
581 return all_lines, all_lines - empty_lines
582 else:
582 else:
583 # fast method
583 # fast method
584 empty_lines = all_lines = content.count('\n')
584 empty_lines = all_lines = content.count('\n')
585 if all_lines == 0 and content:
585 if all_lines == 0 and content:
586 # one-line without a newline
586 # one-line without a newline
587 empty_lines = all_lines = 1
587 empty_lines = all_lines = 1
588
588
589 return all_lines, empty_lines
589 return all_lines, empty_lines
590
590
591 def __repr__(self):
591 def __repr__(self):
592 return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
592 return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
593 getattr(self.commit, 'short_id', ''))
593 getattr(self.commit, 'short_id', ''))
594
594
595
595
596 class RemovedFileNode(FileNode):
596 class RemovedFileNode(FileNode):
597 """
597 """
598 Dummy FileNode class - trying to access any public attribute except path,
598 Dummy FileNode class - trying to access any public attribute except path,
599 name, kind or state (or methods/attributes checking those two) would raise
599 name, kind or state (or methods/attributes checking those two) would raise
600 RemovedFileNodeError.
600 RemovedFileNodeError.
601 """
601 """
602 ALLOWED_ATTRIBUTES = [
602 ALLOWED_ATTRIBUTES = [
603 'name', 'path', 'state', 'is_root', 'is_file', 'is_dir', 'kind',
603 'name', 'path', 'state', 'is_root', 'is_file', 'is_dir', 'kind',
604 'added', 'changed', 'not_changed', 'removed'
604 'added', 'changed', 'not_changed', 'removed'
605 ]
605 ]
606
606
607 def __init__(self, path):
607 def __init__(self, path):
608 """
608 """
609 :param path: relative path to the node
609 :param path: relative path to the node
610 """
610 """
611 super(RemovedFileNode, self).__init__(path=path)
611 super(RemovedFileNode, self).__init__(path=path)
612
612
613 def __getattribute__(self, attr):
613 def __getattribute__(self, attr):
614 if attr.startswith('_') or attr in RemovedFileNode.ALLOWED_ATTRIBUTES:
614 if attr.startswith('_') or attr in RemovedFileNode.ALLOWED_ATTRIBUTES:
615 return super(RemovedFileNode, self).__getattribute__(attr)
615 return super(RemovedFileNode, self).__getattribute__(attr)
616 raise RemovedFileNodeError(
616 raise RemovedFileNodeError(
617 "Cannot access attribute %s on RemovedFileNode" % attr)
617 "Cannot access attribute %s on RemovedFileNode" % attr)
618
618
619 @LazyProperty
619 @LazyProperty
620 def state(self):
620 def state(self):
621 return NodeState.REMOVED
621 return NodeState.REMOVED
622
622
623
623
624 class DirNode(Node):
624 class DirNode(Node):
625 """
625 """
626 DirNode stores list of files and directories within this node.
626 DirNode stores list of files and directories within this node.
627 Nodes may be used standalone but within repository context they
627 Nodes may be used standalone but within repository context they
628 lazily fetch data within same repositorty's commit.
628 lazily fetch data within same repositorty's commit.
629 """
629 """
630
630
631 def __init__(self, path, nodes=(), commit=None):
631 def __init__(self, path, nodes=(), commit=None):
632 """
632 """
633 Only one of ``nodes`` and ``commit`` may be given. Passing both
633 Only one of ``nodes`` and ``commit`` may be given. Passing both
634 would raise ``NodeError`` exception.
634 would raise ``NodeError`` exception.
635
635
636 :param path: relative path to the node
636 :param path: relative path to the node
637 :param nodes: content may be passed to constructor
637 :param nodes: content may be passed to constructor
638 :param commit: if given, will use it to lazily fetch content
638 :param commit: if given, will use it to lazily fetch content
639 """
639 """
640 if nodes and commit:
640 if nodes and commit:
641 raise NodeError("Cannot use both nodes and commit")
641 raise NodeError("Cannot use both nodes and commit")
642 super(DirNode, self).__init__(path, NodeKind.DIR)
642 super(DirNode, self).__init__(path, NodeKind.DIR)
643 self.commit = commit
643 self.commit = commit
644 self._nodes = nodes
644 self._nodes = nodes
645
645
646 @LazyProperty
646 @LazyProperty
647 def content(self):
647 def content(self):
648 raise NodeError(
648 raise NodeError(
649 "%s represents a dir and has no `content` attribute" % self)
649 "%s represents a dir and has no `content` attribute" % self)
650
650
651 @LazyProperty
651 @LazyProperty
652 def nodes(self):
652 def nodes(self):
653 if self.commit:
653 if self.commit:
654 nodes = self.commit.get_nodes(self.path)
654 nodes = self.commit.get_nodes(self.path)
655 else:
655 else:
656 nodes = self._nodes
656 nodes = self._nodes
657 self._nodes_dict = dict((node.path, node) for node in nodes)
657 self._nodes_dict = dict((node.path, node) for node in nodes)
658 return sorted(nodes)
658 return sorted(nodes)
659
659
660 @LazyProperty
660 @LazyProperty
661 def files(self):
661 def files(self):
662 return sorted((node for node in self.nodes if node.is_file()))
662 return sorted((node for node in self.nodes if node.is_file()))
663
663
664 @LazyProperty
664 @LazyProperty
665 def dirs(self):
665 def dirs(self):
666 return sorted((node for node in self.nodes if node.is_dir()))
666 return sorted((node for node in self.nodes if node.is_dir()))
667
667
668 def __iter__(self):
668 def __iter__(self):
669 for node in self.nodes:
669 for node in self.nodes:
670 yield node
670 yield node
671
671
672 def get_node(self, path):
672 def get_node(self, path):
673 """
673 """
674 Returns node from within this particular ``DirNode``, so it is now
674 Returns node from within this particular ``DirNode``, so it is now
675 allowed to fetch, i.e. node located at 'docs/api/index.rst' from node
675 allowed to fetch, i.e. node located at 'docs/api/index.rst' from node
676 'docs'. In order to access deeper nodes one must fetch nodes between
676 'docs'. In order to access deeper nodes one must fetch nodes between
677 them first - this would work::
677 them first - this would work::
678
678
679 docs = root.get_node('docs')
679 docs = root.get_node('docs')
680 docs.get_node('api').get_node('index.rst')
680 docs.get_node('api').get_node('index.rst')
681
681
682 :param: path - relative to the current node
682 :param: path - relative to the current node
683
683
684 .. note::
684 .. note::
685 To access lazily (as in example above) node have to be initialized
685 To access lazily (as in example above) node have to be initialized
686 with related commit object - without it node is out of
686 with related commit object - without it node is out of
687 context and may know nothing about anything else than nearest
687 context and may know nothing about anything else than nearest
688 (located at same level) nodes.
688 (located at same level) nodes.
689 """
689 """
690 try:
690 try:
691 path = path.rstrip('/')
691 path = path.rstrip('/')
692 if path == '':
692 if path == '':
693 raise NodeError("Cannot retrieve node without path")
693 raise NodeError("Cannot retrieve node without path")
694 self.nodes # access nodes first in order to set _nodes_dict
694 self.nodes # access nodes first in order to set _nodes_dict
695 paths = path.split('/')
695 paths = path.split('/')
696 if len(paths) == 1:
696 if len(paths) == 1:
697 if not self.is_root():
697 if not self.is_root():
698 path = '/'.join((self.path, paths[0]))
698 path = '/'.join((self.path, paths[0]))
699 else:
699 else:
700 path = paths[0]
700 path = paths[0]
701 return self._nodes_dict[path]
701 return self._nodes_dict[path]
702 elif len(paths) > 1:
702 elif len(paths) > 1:
703 if self.commit is None:
703 if self.commit is None:
704 raise NodeError(
704 raise NodeError(
705 "Cannot access deeper nodes without commit")
705 "Cannot access deeper nodes without commit")
706 else:
706 else:
707 path1, path2 = paths[0], '/'.join(paths[1:])
707 path1, path2 = paths[0], '/'.join(paths[1:])
708 return self.get_node(path1).get_node(path2)
708 return self.get_node(path1).get_node(path2)
709 else:
709 else:
710 raise KeyError
710 raise KeyError
711 except KeyError:
711 except KeyError:
712 raise NodeError("Node does not exist at %s" % path)
712 raise NodeError("Node does not exist at %s" % path)
713
713
714 @LazyProperty
714 @LazyProperty
715 def state(self):
715 def state(self):
716 raise NodeError("Cannot access state of DirNode")
716 raise NodeError("Cannot access state of DirNode")
717
717
718 @LazyProperty
718 @LazyProperty
719 def size(self):
719 def size(self):
720 size = 0
720 size = 0
721 for root, dirs, files in self.commit.walk(self.path):
721 for root, dirs, files in self.commit.walk(self.path):
722 for f in files:
722 for f in files:
723 size += f.size
723 size += f.size
724
724
725 return size
725 return size
726
726
727 @LazyProperty
728 def last_commit(self):
729 if self.commit:
730 pre_load = ["author", "date", "message"]
731 return self.commit.get_path_commit(self.path, pre_load=pre_load)
732 raise NodeError(
733 "Cannot retrieve last commit of the file without "
734 "related commit attribute")
735
727 def __repr__(self):
736 def __repr__(self):
728 return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
737 return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
729 getattr(self.commit, 'short_id', ''))
738 getattr(self.commit, 'short_id', ''))
730
739
731
740
732 class RootNode(DirNode):
741 class RootNode(DirNode):
733 """
742 """
734 DirNode being the root node of the repository.
743 DirNode being the root node of the repository.
735 """
744 """
736
745
737 def __init__(self, nodes=(), commit=None):
746 def __init__(self, nodes=(), commit=None):
738 super(RootNode, self).__init__(path='', nodes=nodes, commit=commit)
747 super(RootNode, self).__init__(path='', nodes=nodes, commit=commit)
739
748
740 def __repr__(self):
749 def __repr__(self):
741 return '<%s>' % self.__class__.__name__
750 return '<%s>' % self.__class__.__name__
742
751
743
752
744 class SubModuleNode(Node):
753 class SubModuleNode(Node):
745 """
754 """
746 represents a SubModule of Git or SubRepo of Mercurial
755 represents a SubModule of Git or SubRepo of Mercurial
747 """
756 """
748 is_binary = False
757 is_binary = False
749 size = 0
758 size = 0
750
759
751 def __init__(self, name, url=None, commit=None, alias=None):
760 def __init__(self, name, url=None, commit=None, alias=None):
752 self.path = name
761 self.path = name
753 self.kind = NodeKind.SUBMODULE
762 self.kind = NodeKind.SUBMODULE
754 self.alias = alias
763 self.alias = alias
755
764
756 # we have to use EmptyCommit here since this can point to svn/git/hg
765 # we have to use EmptyCommit here since this can point to svn/git/hg
757 # submodules we cannot get from repository
766 # submodules we cannot get from repository
758 self.commit = EmptyCommit(str(commit), alias=alias)
767 self.commit = EmptyCommit(str(commit), alias=alias)
759 self.url = url or self._extract_submodule_url()
768 self.url = url or self._extract_submodule_url()
760
769
761 def __repr__(self):
770 def __repr__(self):
762 return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
771 return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
763 getattr(self.commit, 'short_id', ''))
772 getattr(self.commit, 'short_id', ''))
764
773
765 def _extract_submodule_url(self):
774 def _extract_submodule_url(self):
766 # TODO: find a way to parse gits submodule file and extract the
775 # TODO: find a way to parse gits submodule file and extract the
767 # linking URL
776 # linking URL
768 return self.path
777 return self.path
769
778
770 @LazyProperty
779 @LazyProperty
771 def name(self):
780 def name(self):
772 """
781 """
773 Returns name of the node so if its path
782 Returns name of the node so if its path
774 then only last part is returned.
783 then only last part is returned.
775 """
784 """
776 org = safe_unicode(self.path.rstrip('/').split('/')[-1])
785 org = safe_unicode(self.path.rstrip('/').split('/')[-1])
777 return u'%s @ %s' % (org, self.commit.short_id)
786 return u'%s @ %s' % (org, self.commit.short_id)
778
787
779
788
780 class LargeFileNode(FileNode):
789 class LargeFileNode(FileNode):
781
790
782 def __init__(self, path, url=None, commit=None, alias=None, org_path=None):
791 def __init__(self, path, url=None, commit=None, alias=None, org_path=None):
783 self.path = path
792 self.path = path
784 self.org_path = org_path
793 self.org_path = org_path
785 self.kind = NodeKind.LARGEFILE
794 self.kind = NodeKind.LARGEFILE
786 self.alias = alias
795 self.alias = alias
787
796
788 def _validate_path(self, path):
797 def _validate_path(self, path):
789 """
798 """
790 we override check since the LargeFileNode path is system absolute
799 we override check since the LargeFileNode path is system absolute
791 """
800 """
792 pass
801 pass
793
802
794 def __repr__(self):
803 def __repr__(self):
795 return '<%s %r>' % (self.__class__.__name__, self.path)
804 return '<%s %r>' % (self.__class__.__name__, self.path)
796
805
797 @LazyProperty
806 @LazyProperty
798 def size(self):
807 def size(self):
799 return os.stat(self.path).st_size
808 return os.stat(self.path).st_size
800
809
801 @LazyProperty
810 @LazyProperty
802 def raw_bytes(self):
811 def raw_bytes(self):
803 with open(self.path, 'rb') as f:
812 with open(self.path, 'rb') as f:
804 content = f.read()
813 content = f.read()
805 return content
814 return content
806
815
807 @LazyProperty
816 @LazyProperty
808 def name(self):
817 def name(self):
809 """
818 """
810 Overwrites name to be the org lf path
819 Overwrites name to be the org lf path
811 """
820 """
812 return self.org_path
821 return self.org_path
@@ -1,593 +1,593 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2018 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 datetime
21 import datetime
22 import time
22 import time
23
23
24 import pytest
24 import pytest
25
25
26 from rhodecode.lib.vcs.backends.base import (
26 from rhodecode.lib.vcs.backends.base import (
27 CollectionGenerator, FILEMODE_DEFAULT, EmptyCommit)
27 CollectionGenerator, FILEMODE_DEFAULT, EmptyCommit)
28 from rhodecode.lib.vcs.exceptions import (
28 from rhodecode.lib.vcs.exceptions import (
29 BranchDoesNotExistError, CommitDoesNotExistError,
29 BranchDoesNotExistError, CommitDoesNotExistError,
30 RepositoryError, EmptyRepositoryError)
30 RepositoryError, EmptyRepositoryError)
31 from rhodecode.lib.vcs.nodes import (
31 from rhodecode.lib.vcs.nodes import (
32 FileNode, AddedFileNodesGenerator,
32 FileNode, AddedFileNodesGenerator,
33 ChangedFileNodesGenerator, RemovedFileNodesGenerator)
33 ChangedFileNodesGenerator, RemovedFileNodesGenerator)
34 from rhodecode.tests import get_new_dir
34 from rhodecode.tests import get_new_dir
35 from rhodecode.tests.vcs.conftest import BackendTestMixin
35 from rhodecode.tests.vcs.conftest import BackendTestMixin
36
36
37
37
38 class TestBaseChangeset:
38 class TestBaseChangeset:
39
39
40 def test_is_deprecated(self):
40 def test_is_deprecated(self):
41 from rhodecode.lib.vcs.backends.base import BaseChangeset
41 from rhodecode.lib.vcs.backends.base import BaseChangeset
42 pytest.deprecated_call(BaseChangeset)
42 pytest.deprecated_call(BaseChangeset)
43
43
44
44
45 class TestEmptyCommit(object):
45 class TestEmptyCommit(object):
46
46
47 def test_branch_without_alias_returns_none(self):
47 def test_branch_without_alias_returns_none(self):
48 commit = EmptyCommit()
48 commit = EmptyCommit()
49 assert commit.branch is None
49 assert commit.branch is None
50
50
51
51
52 @pytest.mark.usefixtures("vcs_repository_support")
52 @pytest.mark.usefixtures("vcs_repository_support")
53 class TestCommitsInNonEmptyRepo(BackendTestMixin):
53 class TestCommitsInNonEmptyRepo(BackendTestMixin):
54 recreate_repo_per_test = True
54 recreate_repo_per_test = True
55
55
56 @classmethod
56 @classmethod
57 def _get_commits(cls):
57 def _get_commits(cls):
58 start_date = datetime.datetime(2010, 1, 1, 20)
58 start_date = datetime.datetime(2010, 1, 1, 20)
59 for x in xrange(5):
59 for x in xrange(5):
60 yield {
60 yield {
61 'message': 'Commit %d' % x,
61 'message': 'Commit %d' % x,
62 'author': 'Joe Doe <joe.doe@example.com>',
62 'author': 'Joe Doe <joe.doe@example.com>',
63 'date': start_date + datetime.timedelta(hours=12 * x),
63 'date': start_date + datetime.timedelta(hours=12 * x),
64 'added': [
64 'added': [
65 FileNode('file_%d.txt' % x, content='Foobar %d' % x),
65 FileNode('file_%d.txt' % x, content='Foobar %d' % x),
66 ],
66 ],
67 }
67 }
68
68
69 def test_walk_returns_empty_list_in_case_of_file(self):
69 def test_walk_returns_empty_list_in_case_of_file(self):
70 result = list(self.tip.walk('file_0.txt'))
70 result = list(self.tip.walk('file_0.txt'))
71 assert result == []
71 assert result == []
72
72
73 @pytest.mark.backends("git", "hg")
73 @pytest.mark.backends("git", "hg")
74 def test_new_branch(self):
74 def test_new_branch(self):
75 self.imc.add(FileNode('docs/index.txt',
75 self.imc.add(FileNode('docs/index.txt',
76 content='Documentation\n'))
76 content='Documentation\n'))
77 foobar_tip = self.imc.commit(
77 foobar_tip = self.imc.commit(
78 message=u'New branch: foobar',
78 message=u'New branch: foobar',
79 author=u'joe',
79 author=u'joe',
80 branch='foobar',
80 branch='foobar',
81 )
81 )
82 assert 'foobar' in self.repo.branches
82 assert 'foobar' in self.repo.branches
83 assert foobar_tip.branch == 'foobar'
83 assert foobar_tip.branch == 'foobar'
84 # 'foobar' should be the only branch that contains the new commit
84 # 'foobar' should be the only branch that contains the new commit
85 branch = self.repo.branches.values()
85 branch = self.repo.branches.values()
86 assert branch[0] != branch[1]
86 assert branch[0] != branch[1]
87
87
88 @pytest.mark.backends("git", "hg")
88 @pytest.mark.backends("git", "hg")
89 def test_new_head_in_default_branch(self):
89 def test_new_head_in_default_branch(self):
90 tip = self.repo.get_commit()
90 tip = self.repo.get_commit()
91 self.imc.add(FileNode('docs/index.txt',
91 self.imc.add(FileNode('docs/index.txt',
92 content='Documentation\n'))
92 content='Documentation\n'))
93 foobar_tip = self.imc.commit(
93 foobar_tip = self.imc.commit(
94 message=u'New branch: foobar',
94 message=u'New branch: foobar',
95 author=u'joe',
95 author=u'joe',
96 branch='foobar',
96 branch='foobar',
97 parents=[tip],
97 parents=[tip],
98 )
98 )
99 self.imc.change(FileNode('docs/index.txt',
99 self.imc.change(FileNode('docs/index.txt',
100 content='Documentation\nand more...\n'))
100 content='Documentation\nand more...\n'))
101 newtip = self.imc.commit(
101 newtip = self.imc.commit(
102 message=u'At default branch',
102 message=u'At default branch',
103 author=u'joe',
103 author=u'joe',
104 branch=foobar_tip.branch,
104 branch=foobar_tip.branch,
105 parents=[foobar_tip],
105 parents=[foobar_tip],
106 )
106 )
107
107
108 newest_tip = self.imc.commit(
108 newest_tip = self.imc.commit(
109 message=u'Merged with %s' % foobar_tip.raw_id,
109 message=u'Merged with %s' % foobar_tip.raw_id,
110 author=u'joe',
110 author=u'joe',
111 branch=self.backend_class.DEFAULT_BRANCH_NAME,
111 branch=self.backend_class.DEFAULT_BRANCH_NAME,
112 parents=[newtip, foobar_tip],
112 parents=[newtip, foobar_tip],
113 )
113 )
114
114
115 assert newest_tip.branch == self.backend_class.DEFAULT_BRANCH_NAME
115 assert newest_tip.branch == self.backend_class.DEFAULT_BRANCH_NAME
116
116
117 @pytest.mark.backends("git", "hg")
117 @pytest.mark.backends("git", "hg")
118 def test_get_commits_respects_branch_name(self):
118 def test_get_commits_respects_branch_name(self):
119 """
119 """
120 * e1930d0 (HEAD, master) Back in default branch
120 * e1930d0 (HEAD, master) Back in default branch
121 | * e1930d0 (docs) New Branch: docs2
121 | * e1930d0 (docs) New Branch: docs2
122 | * dcc14fa New branch: docs
122 | * dcc14fa New branch: docs
123 |/
123 |/
124 * e63c41a Initial commit
124 * e63c41a Initial commit
125 ...
125 ...
126 * 624d3db Commit 0
126 * 624d3db Commit 0
127
127
128 :return:
128 :return:
129 """
129 """
130 DEFAULT_BRANCH = self.repo.DEFAULT_BRANCH_NAME
130 DEFAULT_BRANCH = self.repo.DEFAULT_BRANCH_NAME
131 TEST_BRANCH = 'docs'
131 TEST_BRANCH = 'docs'
132 org_tip = self.repo.get_commit()
132 org_tip = self.repo.get_commit()
133
133
134 self.imc.add(FileNode('readme.txt', content='Document\n'))
134 self.imc.add(FileNode('readme.txt', content='Document\n'))
135 initial = self.imc.commit(
135 initial = self.imc.commit(
136 message=u'Initial commit',
136 message=u'Initial commit',
137 author=u'joe',
137 author=u'joe',
138 parents=[org_tip],
138 parents=[org_tip],
139 branch=DEFAULT_BRANCH,)
139 branch=DEFAULT_BRANCH,)
140
140
141 self.imc.add(FileNode('newdoc.txt', content='foobar\n'))
141 self.imc.add(FileNode('newdoc.txt', content='foobar\n'))
142 docs_branch_commit1 = self.imc.commit(
142 docs_branch_commit1 = self.imc.commit(
143 message=u'New branch: docs',
143 message=u'New branch: docs',
144 author=u'joe',
144 author=u'joe',
145 parents=[initial],
145 parents=[initial],
146 branch=TEST_BRANCH,)
146 branch=TEST_BRANCH,)
147
147
148 self.imc.add(FileNode('newdoc2.txt', content='foobar2\n'))
148 self.imc.add(FileNode('newdoc2.txt', content='foobar2\n'))
149 docs_branch_commit2 = self.imc.commit(
149 docs_branch_commit2 = self.imc.commit(
150 message=u'New branch: docs2',
150 message=u'New branch: docs2',
151 author=u'joe',
151 author=u'joe',
152 parents=[docs_branch_commit1],
152 parents=[docs_branch_commit1],
153 branch=TEST_BRANCH,)
153 branch=TEST_BRANCH,)
154
154
155 self.imc.add(FileNode('newfile', content='hello world\n'))
155 self.imc.add(FileNode('newfile', content='hello world\n'))
156 self.imc.commit(
156 self.imc.commit(
157 message=u'Back in default branch',
157 message=u'Back in default branch',
158 author=u'joe',
158 author=u'joe',
159 parents=[initial],
159 parents=[initial],
160 branch=DEFAULT_BRANCH,)
160 branch=DEFAULT_BRANCH,)
161
161
162 default_branch_commits = self.repo.get_commits(
162 default_branch_commits = self.repo.get_commits(
163 branch_name=DEFAULT_BRANCH)
163 branch_name=DEFAULT_BRANCH)
164 assert docs_branch_commit1 not in list(default_branch_commits)
164 assert docs_branch_commit1 not in list(default_branch_commits)
165 assert docs_branch_commit2 not in list(default_branch_commits)
165 assert docs_branch_commit2 not in list(default_branch_commits)
166
166
167 docs_branch_commits = self.repo.get_commits(
167 docs_branch_commits = self.repo.get_commits(
168 start_id=self.repo.commit_ids[0], end_id=self.repo.commit_ids[-1],
168 start_id=self.repo.commit_ids[0], end_id=self.repo.commit_ids[-1],
169 branch_name=TEST_BRANCH)
169 branch_name=TEST_BRANCH)
170 assert docs_branch_commit1 in list(docs_branch_commits)
170 assert docs_branch_commit1 in list(docs_branch_commits)
171 assert docs_branch_commit2 in list(docs_branch_commits)
171 assert docs_branch_commit2 in list(docs_branch_commits)
172
172
173 @pytest.mark.backends("svn")
173 @pytest.mark.backends("svn")
174 def test_get_commits_respects_branch_name_svn(self, vcsbackend_svn):
174 def test_get_commits_respects_branch_name_svn(self, vcsbackend_svn):
175 repo = vcsbackend_svn['svn-simple-layout']
175 repo = vcsbackend_svn['svn-simple-layout']
176 commits = repo.get_commits(branch_name='trunk')
176 commits = repo.get_commits(branch_name='trunk')
177 commit_indexes = [c.idx for c in commits]
177 commit_indexes = [c.idx for c in commits]
178 assert commit_indexes == [1, 2, 3, 7, 12, 15]
178 assert commit_indexes == [1, 2, 3, 7, 12, 15]
179
179
180 def test_get_commit_by_branch(self):
180 def test_get_commit_by_branch(self):
181 for branch, commit_id in self.repo.branches.iteritems():
181 for branch, commit_id in self.repo.branches.iteritems():
182 assert commit_id == self.repo.get_commit(branch).raw_id
182 assert commit_id == self.repo.get_commit(branch).raw_id
183
183
184 def test_get_commit_by_tag(self):
184 def test_get_commit_by_tag(self):
185 for tag, commit_id in self.repo.tags.iteritems():
185 for tag, commit_id in self.repo.tags.iteritems():
186 assert commit_id == self.repo.get_commit(tag).raw_id
186 assert commit_id == self.repo.get_commit(tag).raw_id
187
187
188 def test_get_commit_parents(self):
188 def test_get_commit_parents(self):
189 repo = self.repo
189 repo = self.repo
190 for test_idx in [1, 2, 3]:
190 for test_idx in [1, 2, 3]:
191 commit = repo.get_commit(commit_idx=test_idx - 1)
191 commit = repo.get_commit(commit_idx=test_idx - 1)
192 assert [commit] == repo.get_commit(commit_idx=test_idx).parents
192 assert [commit] == repo.get_commit(commit_idx=test_idx).parents
193
193
194 def test_get_commit_children(self):
194 def test_get_commit_children(self):
195 repo = self.repo
195 repo = self.repo
196 for test_idx in [1, 2, 3]:
196 for test_idx in [1, 2, 3]:
197 commit = repo.get_commit(commit_idx=test_idx + 1)
197 commit = repo.get_commit(commit_idx=test_idx + 1)
198 assert [commit] == repo.get_commit(commit_idx=test_idx).children
198 assert [commit] == repo.get_commit(commit_idx=test_idx).children
199
199
200
200
201 @pytest.mark.usefixtures("vcs_repository_support")
201 @pytest.mark.usefixtures("vcs_repository_support")
202 class TestCommits(BackendTestMixin):
202 class TestCommits(BackendTestMixin):
203 recreate_repo_per_test = False
203 recreate_repo_per_test = False
204
204
205 @classmethod
205 @classmethod
206 def _get_commits(cls):
206 def _get_commits(cls):
207 start_date = datetime.datetime(2010, 1, 1, 20)
207 start_date = datetime.datetime(2010, 1, 1, 20)
208 for x in xrange(5):
208 for x in xrange(5):
209 yield {
209 yield {
210 'message': u'Commit %d' % x,
210 'message': u'Commit %d' % x,
211 'author': u'Joe Doe <joe.doe@example.com>',
211 'author': u'Joe Doe <joe.doe@example.com>',
212 'date': start_date + datetime.timedelta(hours=12 * x),
212 'date': start_date + datetime.timedelta(hours=12 * x),
213 'added': [
213 'added': [
214 FileNode('file_%d.txt' % x, content='Foobar %d' % x),
214 FileNode('file_%d.txt' % x, content='Foobar %d' % x),
215 ],
215 ],
216 }
216 }
217
217
218 def test_simple(self):
218 def test_simple(self):
219 tip = self.repo.get_commit()
219 tip = self.repo.get_commit()
220 assert tip.date, datetime.datetime(2010, 1, 3 == 20)
220 assert tip.date, datetime.datetime(2010, 1, 3 == 20)
221
221
222 def test_simple_serialized_commit(self):
222 def test_simple_serialized_commit(self):
223 tip = self.repo.get_commit()
223 tip = self.repo.get_commit()
224 # json.dumps(tip) uses .__json__() method
224 # json.dumps(tip) uses .__json__() method
225 data = tip.__json__()
225 data = tip.__json__()
226 assert 'branch' in data
226 assert 'branch' in data
227 assert data['revision']
227 assert data['revision']
228
228
229 def test_retrieve_tip(self):
229 def test_retrieve_tip(self):
230 tip = self.repo.get_commit('tip')
230 tip = self.repo.get_commit('tip')
231 assert tip == self.repo.get_commit()
231 assert tip == self.repo.get_commit()
232
232
233 def test_invalid(self):
233 def test_invalid(self):
234 with pytest.raises(CommitDoesNotExistError):
234 with pytest.raises(CommitDoesNotExistError):
235 self.repo.get_commit(commit_idx=123456789)
235 self.repo.get_commit(commit_idx=123456789)
236
236
237 def test_idx(self):
237 def test_idx(self):
238 commit = self.repo[0]
238 commit = self.repo[0]
239 assert commit.idx == 0
239 assert commit.idx == 0
240
240
241 def test_negative_idx(self):
241 def test_negative_idx(self):
242 commit = self.repo.get_commit(commit_idx=-1)
242 commit = self.repo.get_commit(commit_idx=-1)
243 assert commit.idx >= 0
243 assert commit.idx >= 0
244
244
245 def test_revision_is_deprecated(self):
245 def test_revision_is_deprecated(self):
246 def get_revision(commit):
246 def get_revision(commit):
247 return commit.revision
247 return commit.revision
248
248
249 commit = self.repo[0]
249 commit = self.repo[0]
250 pytest.deprecated_call(get_revision, commit)
250 pytest.deprecated_call(get_revision, commit)
251
251
252 def test_size(self):
252 def test_size(self):
253 tip = self.repo.get_commit()
253 tip = self.repo.get_commit()
254 size = 5 * len('Foobar N') # Size of 5 files
254 size = 5 * len('Foobar N') # Size of 5 files
255 assert tip.size == size
255 assert tip.size == size
256
256
257 def test_size_at_commit(self):
257 def test_size_at_commit(self):
258 tip = self.repo.get_commit()
258 tip = self.repo.get_commit()
259 size = 5 * len('Foobar N') # Size of 5 files
259 size = 5 * len('Foobar N') # Size of 5 files
260 assert self.repo.size_at_commit(tip.raw_id) == size
260 assert self.repo.size_at_commit(tip.raw_id) == size
261
261
262 def test_size_at_first_commit(self):
262 def test_size_at_first_commit(self):
263 commit = self.repo[0]
263 commit = self.repo[0]
264 size = len('Foobar N') # Size of 1 file
264 size = len('Foobar N') # Size of 1 file
265 assert self.repo.size_at_commit(commit.raw_id) == size
265 assert self.repo.size_at_commit(commit.raw_id) == size
266
266
267 def test_author(self):
267 def test_author(self):
268 tip = self.repo.get_commit()
268 tip = self.repo.get_commit()
269 assert_text_equal(tip.author, u'Joe Doe <joe.doe@example.com>')
269 assert_text_equal(tip.author, u'Joe Doe <joe.doe@example.com>')
270
270
271 def test_author_name(self):
271 def test_author_name(self):
272 tip = self.repo.get_commit()
272 tip = self.repo.get_commit()
273 assert_text_equal(tip.author_name, u'Joe Doe')
273 assert_text_equal(tip.author_name, u'Joe Doe')
274
274
275 def test_author_email(self):
275 def test_author_email(self):
276 tip = self.repo.get_commit()
276 tip = self.repo.get_commit()
277 assert_text_equal(tip.author_email, u'joe.doe@example.com')
277 assert_text_equal(tip.author_email, u'joe.doe@example.com')
278
278
279 def test_message(self):
279 def test_message(self):
280 tip = self.repo.get_commit()
280 tip = self.repo.get_commit()
281 assert_text_equal(tip.message, u'Commit 4')
281 assert_text_equal(tip.message, u'Commit 4')
282
282
283 def test_diff(self):
283 def test_diff(self):
284 tip = self.repo.get_commit()
284 tip = self.repo.get_commit()
285 diff = tip.diff()
285 diff = tip.diff()
286 assert "+Foobar 4" in diff.raw
286 assert "+Foobar 4" in diff.raw
287
287
288 def test_prev(self):
288 def test_prev(self):
289 tip = self.repo.get_commit()
289 tip = self.repo.get_commit()
290 prev_commit = tip.prev()
290 prev_commit = tip.prev()
291 assert prev_commit.message == 'Commit 3'
291 assert prev_commit.message == 'Commit 3'
292
292
293 def test_prev_raises_on_first_commit(self):
293 def test_prev_raises_on_first_commit(self):
294 commit = self.repo.get_commit(commit_idx=0)
294 commit = self.repo.get_commit(commit_idx=0)
295 with pytest.raises(CommitDoesNotExistError):
295 with pytest.raises(CommitDoesNotExistError):
296 commit.prev()
296 commit.prev()
297
297
298 def test_prev_works_on_second_commit_issue_183(self):
298 def test_prev_works_on_second_commit_issue_183(self):
299 commit = self.repo.get_commit(commit_idx=1)
299 commit = self.repo.get_commit(commit_idx=1)
300 prev_commit = commit.prev()
300 prev_commit = commit.prev()
301 assert prev_commit.idx == 0
301 assert prev_commit.idx == 0
302
302
303 def test_next(self):
303 def test_next(self):
304 commit = self.repo.get_commit(commit_idx=2)
304 commit = self.repo.get_commit(commit_idx=2)
305 next_commit = commit.next()
305 next_commit = commit.next()
306 assert next_commit.message == 'Commit 3'
306 assert next_commit.message == 'Commit 3'
307
307
308 def test_next_raises_on_tip(self):
308 def test_next_raises_on_tip(self):
309 commit = self.repo.get_commit()
309 commit = self.repo.get_commit()
310 with pytest.raises(CommitDoesNotExistError):
310 with pytest.raises(CommitDoesNotExistError):
311 commit.next()
311 commit.next()
312
312
313 def test_get_file_commit(self):
313 def test_get_path_commit(self):
314 commit = self.repo.get_commit()
314 commit = self.repo.get_commit()
315 commit.get_file_commit('file_4.txt')
315 commit.get_path_commit('file_4.txt')
316 assert commit.message == 'Commit 4'
316 assert commit.message == 'Commit 4'
317
317
318 def test_get_filenodes_generator(self):
318 def test_get_filenodes_generator(self):
319 tip = self.repo.get_commit()
319 tip = self.repo.get_commit()
320 filepaths = [node.path for node in tip.get_filenodes_generator()]
320 filepaths = [node.path for node in tip.get_filenodes_generator()]
321 assert filepaths == ['file_%d.txt' % x for x in xrange(5)]
321 assert filepaths == ['file_%d.txt' % x for x in xrange(5)]
322
322
323 def test_get_file_annotate(self):
323 def test_get_file_annotate(self):
324 file_added_commit = self.repo.get_commit(commit_idx=3)
324 file_added_commit = self.repo.get_commit(commit_idx=3)
325 annotations = list(file_added_commit.get_file_annotate('file_3.txt'))
325 annotations = list(file_added_commit.get_file_annotate('file_3.txt'))
326
326
327 line_no, commit_id, commit_loader, line = annotations[0]
327 line_no, commit_id, commit_loader, line = annotations[0]
328
328
329 assert line_no == 1
329 assert line_no == 1
330 assert commit_id == file_added_commit.raw_id
330 assert commit_id == file_added_commit.raw_id
331 assert commit_loader() == file_added_commit
331 assert commit_loader() == file_added_commit
332 assert 'Foobar 3' in line
332 assert 'Foobar 3' in line
333
333
334 def test_get_file_annotate_does_not_exist(self):
334 def test_get_file_annotate_does_not_exist(self):
335 file_added_commit = self.repo.get_commit(commit_idx=2)
335 file_added_commit = self.repo.get_commit(commit_idx=2)
336 # TODO: Should use a specific exception class here?
336 # TODO: Should use a specific exception class here?
337 with pytest.raises(Exception):
337 with pytest.raises(Exception):
338 list(file_added_commit.get_file_annotate('file_3.txt'))
338 list(file_added_commit.get_file_annotate('file_3.txt'))
339
339
340 def test_get_file_annotate_tip(self):
340 def test_get_file_annotate_tip(self):
341 tip = self.repo.get_commit()
341 tip = self.repo.get_commit()
342 commit = self.repo.get_commit(commit_idx=3)
342 commit = self.repo.get_commit(commit_idx=3)
343 expected_values = list(commit.get_file_annotate('file_3.txt'))
343 expected_values = list(commit.get_file_annotate('file_3.txt'))
344 annotations = list(tip.get_file_annotate('file_3.txt'))
344 annotations = list(tip.get_file_annotate('file_3.txt'))
345
345
346 # Note: Skip index 2 because the loader function is not the same
346 # Note: Skip index 2 because the loader function is not the same
347 for idx in (0, 1, 3):
347 for idx in (0, 1, 3):
348 assert annotations[0][idx] == expected_values[0][idx]
348 assert annotations[0][idx] == expected_values[0][idx]
349
349
350 def test_get_commits_is_ordered_by_date(self):
350 def test_get_commits_is_ordered_by_date(self):
351 commits = self.repo.get_commits()
351 commits = self.repo.get_commits()
352 assert isinstance(commits, CollectionGenerator)
352 assert isinstance(commits, CollectionGenerator)
353 assert len(commits) == 0 or len(commits) != 0
353 assert len(commits) == 0 or len(commits) != 0
354 commits = list(commits)
354 commits = list(commits)
355 ordered_by_date = sorted(commits, key=lambda commit: commit.date)
355 ordered_by_date = sorted(commits, key=lambda commit: commit.date)
356 assert commits == ordered_by_date
356 assert commits == ordered_by_date
357
357
358 def test_get_commits_respects_start(self):
358 def test_get_commits_respects_start(self):
359 second_id = self.repo.commit_ids[1]
359 second_id = self.repo.commit_ids[1]
360 commits = self.repo.get_commits(start_id=second_id)
360 commits = self.repo.get_commits(start_id=second_id)
361 assert isinstance(commits, CollectionGenerator)
361 assert isinstance(commits, CollectionGenerator)
362 commits = list(commits)
362 commits = list(commits)
363 assert len(commits) == 4
363 assert len(commits) == 4
364
364
365 def test_get_commits_includes_start_commit(self):
365 def test_get_commits_includes_start_commit(self):
366 second_id = self.repo.commit_ids[1]
366 second_id = self.repo.commit_ids[1]
367 commits = self.repo.get_commits(start_id=second_id)
367 commits = self.repo.get_commits(start_id=second_id)
368 assert isinstance(commits, CollectionGenerator)
368 assert isinstance(commits, CollectionGenerator)
369 commits = list(commits)
369 commits = list(commits)
370 assert commits[0].raw_id == second_id
370 assert commits[0].raw_id == second_id
371
371
372 def test_get_commits_respects_end(self):
372 def test_get_commits_respects_end(self):
373 second_id = self.repo.commit_ids[1]
373 second_id = self.repo.commit_ids[1]
374 commits = self.repo.get_commits(end_id=second_id)
374 commits = self.repo.get_commits(end_id=second_id)
375 assert isinstance(commits, CollectionGenerator)
375 assert isinstance(commits, CollectionGenerator)
376 commits = list(commits)
376 commits = list(commits)
377 assert commits[-1].raw_id == second_id
377 assert commits[-1].raw_id == second_id
378 assert len(commits) == 2
378 assert len(commits) == 2
379
379
380 def test_get_commits_respects_both_start_and_end(self):
380 def test_get_commits_respects_both_start_and_end(self):
381 second_id = self.repo.commit_ids[1]
381 second_id = self.repo.commit_ids[1]
382 third_id = self.repo.commit_ids[2]
382 third_id = self.repo.commit_ids[2]
383 commits = self.repo.get_commits(start_id=second_id, end_id=third_id)
383 commits = self.repo.get_commits(start_id=second_id, end_id=third_id)
384 assert isinstance(commits, CollectionGenerator)
384 assert isinstance(commits, CollectionGenerator)
385 commits = list(commits)
385 commits = list(commits)
386 assert len(commits) == 2
386 assert len(commits) == 2
387
387
388 def test_get_commits_on_empty_repo_raises_EmptyRepository_error(self):
388 def test_get_commits_on_empty_repo_raises_EmptyRepository_error(self):
389 repo_path = get_new_dir(str(time.time()))
389 repo_path = get_new_dir(str(time.time()))
390 repo = self.Backend(repo_path, create=True)
390 repo = self.Backend(repo_path, create=True)
391
391
392 with pytest.raises(EmptyRepositoryError):
392 with pytest.raises(EmptyRepositoryError):
393 list(repo.get_commits(start_id='foobar'))
393 list(repo.get_commits(start_id='foobar'))
394
394
395 def test_get_commits_respects_hidden(self):
395 def test_get_commits_respects_hidden(self):
396 commits = self.repo.get_commits(show_hidden=True)
396 commits = self.repo.get_commits(show_hidden=True)
397 assert isinstance(commits, CollectionGenerator)
397 assert isinstance(commits, CollectionGenerator)
398 assert len(commits) == 5
398 assert len(commits) == 5
399
399
400 def test_get_commits_includes_end_commit(self):
400 def test_get_commits_includes_end_commit(self):
401 second_id = self.repo.commit_ids[1]
401 second_id = self.repo.commit_ids[1]
402 commits = self.repo.get_commits(end_id=second_id)
402 commits = self.repo.get_commits(end_id=second_id)
403 assert isinstance(commits, CollectionGenerator)
403 assert isinstance(commits, CollectionGenerator)
404 assert len(commits) == 2
404 assert len(commits) == 2
405 commits = list(commits)
405 commits = list(commits)
406 assert commits[-1].raw_id == second_id
406 assert commits[-1].raw_id == second_id
407
407
408 def test_get_commits_respects_start_date(self):
408 def test_get_commits_respects_start_date(self):
409 start_date = datetime.datetime(2010, 1, 2)
409 start_date = datetime.datetime(2010, 1, 2)
410 commits = self.repo.get_commits(start_date=start_date)
410 commits = self.repo.get_commits(start_date=start_date)
411 assert isinstance(commits, CollectionGenerator)
411 assert isinstance(commits, CollectionGenerator)
412 # Should be 4 commits after 2010-01-02 00:00:00
412 # Should be 4 commits after 2010-01-02 00:00:00
413 assert len(commits) == 4
413 assert len(commits) == 4
414 for c in commits:
414 for c in commits:
415 assert c.date >= start_date
415 assert c.date >= start_date
416
416
417 def test_get_commits_respects_start_date_with_branch(self):
417 def test_get_commits_respects_start_date_with_branch(self):
418 start_date = datetime.datetime(2010, 1, 2)
418 start_date = datetime.datetime(2010, 1, 2)
419 commits = self.repo.get_commits(
419 commits = self.repo.get_commits(
420 start_date=start_date, branch_name=self.repo.DEFAULT_BRANCH_NAME)
420 start_date=start_date, branch_name=self.repo.DEFAULT_BRANCH_NAME)
421 assert isinstance(commits, CollectionGenerator)
421 assert isinstance(commits, CollectionGenerator)
422 # Should be 4 commits after 2010-01-02 00:00:00
422 # Should be 4 commits after 2010-01-02 00:00:00
423 assert len(commits) == 4
423 assert len(commits) == 4
424 for c in commits:
424 for c in commits:
425 assert c.date >= start_date
425 assert c.date >= start_date
426
426
427 def test_get_commits_respects_start_date_and_end_date(self):
427 def test_get_commits_respects_start_date_and_end_date(self):
428 start_date = datetime.datetime(2010, 1, 2)
428 start_date = datetime.datetime(2010, 1, 2)
429 end_date = datetime.datetime(2010, 1, 3)
429 end_date = datetime.datetime(2010, 1, 3)
430 commits = self.repo.get_commits(start_date=start_date,
430 commits = self.repo.get_commits(start_date=start_date,
431 end_date=end_date)
431 end_date=end_date)
432 assert isinstance(commits, CollectionGenerator)
432 assert isinstance(commits, CollectionGenerator)
433 assert len(commits) == 2
433 assert len(commits) == 2
434 for c in commits:
434 for c in commits:
435 assert c.date >= start_date
435 assert c.date >= start_date
436 assert c.date <= end_date
436 assert c.date <= end_date
437
437
438 def test_get_commits_respects_end_date(self):
438 def test_get_commits_respects_end_date(self):
439 end_date = datetime.datetime(2010, 1, 2)
439 end_date = datetime.datetime(2010, 1, 2)
440 commits = self.repo.get_commits(end_date=end_date)
440 commits = self.repo.get_commits(end_date=end_date)
441 assert isinstance(commits, CollectionGenerator)
441 assert isinstance(commits, CollectionGenerator)
442 assert len(commits) == 1
442 assert len(commits) == 1
443 for c in commits:
443 for c in commits:
444 assert c.date <= end_date
444 assert c.date <= end_date
445
445
446 def test_get_commits_respects_reverse(self):
446 def test_get_commits_respects_reverse(self):
447 commits = self.repo.get_commits() # no longer reverse support
447 commits = self.repo.get_commits() # no longer reverse support
448 assert isinstance(commits, CollectionGenerator)
448 assert isinstance(commits, CollectionGenerator)
449 assert len(commits) == 5
449 assert len(commits) == 5
450 commit_ids = reversed([c.raw_id for c in commits])
450 commit_ids = reversed([c.raw_id for c in commits])
451 assert list(commit_ids) == list(reversed(self.repo.commit_ids))
451 assert list(commit_ids) == list(reversed(self.repo.commit_ids))
452
452
453 def test_get_commits_slice_generator(self):
453 def test_get_commits_slice_generator(self):
454 commits = self.repo.get_commits(
454 commits = self.repo.get_commits(
455 branch_name=self.repo.DEFAULT_BRANCH_NAME)
455 branch_name=self.repo.DEFAULT_BRANCH_NAME)
456 assert isinstance(commits, CollectionGenerator)
456 assert isinstance(commits, CollectionGenerator)
457 commit_slice = list(commits[1:3])
457 commit_slice = list(commits[1:3])
458 assert len(commit_slice) == 2
458 assert len(commit_slice) == 2
459
459
460 def test_get_commits_raise_commitdoesnotexist_for_wrong_start(self):
460 def test_get_commits_raise_commitdoesnotexist_for_wrong_start(self):
461 with pytest.raises(CommitDoesNotExistError):
461 with pytest.raises(CommitDoesNotExistError):
462 list(self.repo.get_commits(start_id='foobar'))
462 list(self.repo.get_commits(start_id='foobar'))
463
463
464 def test_get_commits_raise_commitdoesnotexist_for_wrong_end(self):
464 def test_get_commits_raise_commitdoesnotexist_for_wrong_end(self):
465 with pytest.raises(CommitDoesNotExistError):
465 with pytest.raises(CommitDoesNotExistError):
466 list(self.repo.get_commits(end_id='foobar'))
466 list(self.repo.get_commits(end_id='foobar'))
467
467
468 def test_get_commits_raise_branchdoesnotexist_for_wrong_branch_name(self):
468 def test_get_commits_raise_branchdoesnotexist_for_wrong_branch_name(self):
469 with pytest.raises(BranchDoesNotExistError):
469 with pytest.raises(BranchDoesNotExistError):
470 list(self.repo.get_commits(branch_name='foobar'))
470 list(self.repo.get_commits(branch_name='foobar'))
471
471
472 def test_get_commits_raise_repositoryerror_for_wrong_start_end(self):
472 def test_get_commits_raise_repositoryerror_for_wrong_start_end(self):
473 start_id = self.repo.commit_ids[-1]
473 start_id = self.repo.commit_ids[-1]
474 end_id = self.repo.commit_ids[0]
474 end_id = self.repo.commit_ids[0]
475 with pytest.raises(RepositoryError):
475 with pytest.raises(RepositoryError):
476 list(self.repo.get_commits(start_id=start_id, end_id=end_id))
476 list(self.repo.get_commits(start_id=start_id, end_id=end_id))
477
477
478 def test_get_commits_raises_for_numerical_ids(self):
478 def test_get_commits_raises_for_numerical_ids(self):
479 with pytest.raises(TypeError):
479 with pytest.raises(TypeError):
480 self.repo.get_commits(start_id=1, end_id=2)
480 self.repo.get_commits(start_id=1, end_id=2)
481
481
482 def test_commit_equality(self):
482 def test_commit_equality(self):
483 commit1 = self.repo.get_commit(self.repo.commit_ids[0])
483 commit1 = self.repo.get_commit(self.repo.commit_ids[0])
484 commit2 = self.repo.get_commit(self.repo.commit_ids[1])
484 commit2 = self.repo.get_commit(self.repo.commit_ids[1])
485
485
486 assert commit1 == commit1
486 assert commit1 == commit1
487 assert commit2 == commit2
487 assert commit2 == commit2
488 assert commit1 != commit2
488 assert commit1 != commit2
489 assert commit2 != commit1
489 assert commit2 != commit1
490 assert commit1 != None
490 assert commit1 != None
491 assert None != commit1
491 assert None != commit1
492 assert 1 != commit1
492 assert 1 != commit1
493 assert 'string' != commit1
493 assert 'string' != commit1
494
494
495
495
496 @pytest.mark.parametrize("filename, expected", [
496 @pytest.mark.parametrize("filename, expected", [
497 ("README.rst", False),
497 ("README.rst", False),
498 ("README", True),
498 ("README", True),
499 ])
499 ])
500 def test_commit_is_link(vcsbackend, filename, expected):
500 def test_commit_is_link(vcsbackend, filename, expected):
501 commit = vcsbackend.repo.get_commit()
501 commit = vcsbackend.repo.get_commit()
502 link_status = commit.is_link(filename)
502 link_status = commit.is_link(filename)
503 assert link_status is expected
503 assert link_status is expected
504
504
505
505
506 @pytest.mark.usefixtures("vcs_repository_support")
506 @pytest.mark.usefixtures("vcs_repository_support")
507 class TestCommitsChanges(BackendTestMixin):
507 class TestCommitsChanges(BackendTestMixin):
508 recreate_repo_per_test = False
508 recreate_repo_per_test = False
509
509
510 @classmethod
510 @classmethod
511 def _get_commits(cls):
511 def _get_commits(cls):
512 return [
512 return [
513 {
513 {
514 'message': u'Initial',
514 'message': u'Initial',
515 'author': u'Joe Doe <joe.doe@example.com>',
515 'author': u'Joe Doe <joe.doe@example.com>',
516 'date': datetime.datetime(2010, 1, 1, 20),
516 'date': datetime.datetime(2010, 1, 1, 20),
517 'added': [
517 'added': [
518 FileNode('foo/bar', content='foo'),
518 FileNode('foo/bar', content='foo'),
519 FileNode('foo/bał', content='foo'),
519 FileNode('foo/bał', content='foo'),
520 FileNode('foobar', content='foo'),
520 FileNode('foobar', content='foo'),
521 FileNode('qwe', content='foo'),
521 FileNode('qwe', content='foo'),
522 ],
522 ],
523 },
523 },
524 {
524 {
525 'message': u'Massive changes',
525 'message': u'Massive changes',
526 'author': u'Joe Doe <joe.doe@example.com>',
526 'author': u'Joe Doe <joe.doe@example.com>',
527 'date': datetime.datetime(2010, 1, 1, 22),
527 'date': datetime.datetime(2010, 1, 1, 22),
528 'added': [FileNode('fallout', content='War never changes')],
528 'added': [FileNode('fallout', content='War never changes')],
529 'changed': [
529 'changed': [
530 FileNode('foo/bar', content='baz'),
530 FileNode('foo/bar', content='baz'),
531 FileNode('foobar', content='baz'),
531 FileNode('foobar', content='baz'),
532 ],
532 ],
533 'removed': [FileNode('qwe')],
533 'removed': [FileNode('qwe')],
534 },
534 },
535 ]
535 ]
536
536
537 def test_initial_commit(self, local_dt_to_utc):
537 def test_initial_commit(self, local_dt_to_utc):
538 commit = self.repo.get_commit(commit_idx=0)
538 commit = self.repo.get_commit(commit_idx=0)
539 assert set(commit.added) == set([
539 assert set(commit.added) == set([
540 commit.get_node('foo/bar'),
540 commit.get_node('foo/bar'),
541 commit.get_node('foo/bał'),
541 commit.get_node('foo/bał'),
542 commit.get_node('foobar'),
542 commit.get_node('foobar'),
543 commit.get_node('qwe'),
543 commit.get_node('qwe'),
544 ])
544 ])
545 assert set(commit.changed) == set()
545 assert set(commit.changed) == set()
546 assert set(commit.removed) == set()
546 assert set(commit.removed) == set()
547 assert set(commit.affected_files) == set(
547 assert set(commit.affected_files) == set(
548 ['foo/bar', 'foo/bał', 'foobar', 'qwe'])
548 ['foo/bar', 'foo/bał', 'foobar', 'qwe'])
549 assert commit.date == local_dt_to_utc(
549 assert commit.date == local_dt_to_utc(
550 datetime.datetime(2010, 1, 1, 20, 0))
550 datetime.datetime(2010, 1, 1, 20, 0))
551
551
552 def test_head_added(self):
552 def test_head_added(self):
553 commit = self.repo.get_commit()
553 commit = self.repo.get_commit()
554 assert isinstance(commit.added, AddedFileNodesGenerator)
554 assert isinstance(commit.added, AddedFileNodesGenerator)
555 assert set(commit.added) == set([commit.get_node('fallout')])
555 assert set(commit.added) == set([commit.get_node('fallout')])
556 assert isinstance(commit.changed, ChangedFileNodesGenerator)
556 assert isinstance(commit.changed, ChangedFileNodesGenerator)
557 assert set(commit.changed) == set([
557 assert set(commit.changed) == set([
558 commit.get_node('foo/bar'),
558 commit.get_node('foo/bar'),
559 commit.get_node('foobar'),
559 commit.get_node('foobar'),
560 ])
560 ])
561 assert isinstance(commit.removed, RemovedFileNodesGenerator)
561 assert isinstance(commit.removed, RemovedFileNodesGenerator)
562 assert len(commit.removed) == 1
562 assert len(commit.removed) == 1
563 assert list(commit.removed)[0].path == 'qwe'
563 assert list(commit.removed)[0].path == 'qwe'
564
564
565 def test_get_filemode(self):
565 def test_get_filemode(self):
566 commit = self.repo.get_commit()
566 commit = self.repo.get_commit()
567 assert FILEMODE_DEFAULT == commit.get_file_mode('foo/bar')
567 assert FILEMODE_DEFAULT == commit.get_file_mode('foo/bar')
568
568
569 def test_get_filemode_non_ascii(self):
569 def test_get_filemode_non_ascii(self):
570 commit = self.repo.get_commit()
570 commit = self.repo.get_commit()
571 assert FILEMODE_DEFAULT == commit.get_file_mode('foo/bał')
571 assert FILEMODE_DEFAULT == commit.get_file_mode('foo/bał')
572 assert FILEMODE_DEFAULT == commit.get_file_mode(u'foo/bał')
572 assert FILEMODE_DEFAULT == commit.get_file_mode(u'foo/bał')
573
573
574 def test_get_file_history(self):
574 def test_get_path_history(self):
575 commit = self.repo.get_commit()
575 commit = self.repo.get_commit()
576 history = commit.get_file_history('foo/bar')
576 history = commit.get_path_history('foo/bar')
577 assert len(history) == 2
577 assert len(history) == 2
578
578
579 def test_get_file_history_with_limit(self):
579 def test_get_path_history_with_limit(self):
580 commit = self.repo.get_commit()
580 commit = self.repo.get_commit()
581 history = commit.get_file_history('foo/bar', limit=1)
581 history = commit.get_path_history('foo/bar', limit=1)
582 assert len(history) == 1
582 assert len(history) == 1
583
583
584 def test_get_file_history_first_commit(self):
584 def test_get_path_history_first_commit(self):
585 commit = self.repo[0]
585 commit = self.repo[0]
586 history = commit.get_file_history('foo/bar')
586 history = commit.get_path_history('foo/bar')
587 assert len(history) == 1
587 assert len(history) == 1
588
588
589
589
590 def assert_text_equal(expected, given):
590 def assert_text_equal(expected, given):
591 assert expected == given
591 assert expected == given
592 assert isinstance(expected, unicode)
592 assert isinstance(expected, unicode)
593 assert isinstance(given, unicode)
593 assert isinstance(given, unicode)
General Comments 0
You need to be logged in to leave comments. Login now