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