##// END OF EJS Templates
vcsserver: made binary content check be calculated on vcsserver...
dan -
r3896:4b32a14b default
parent child Browse files
Show More
@@ -1,1890 +1,1896 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2014-2019 RhodeCode GmbH
3 # Copyright (C) 2014-2019 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Base module for all VCS systems
22 Base module for all VCS systems
23 """
23 """
24 import os
24 import os
25 import re
25 import re
26 import time
26 import time
27 import shutil
27 import shutil
28 import datetime
28 import datetime
29 import fnmatch
29 import fnmatch
30 import itertools
30 import itertools
31 import logging
31 import logging
32 import collections
32 import collections
33 import warnings
33 import warnings
34
34
35 from zope.cachedescriptors.property import Lazy as LazyProperty
35 from zope.cachedescriptors.property import Lazy as LazyProperty
36
36
37 from pyramid import compat
37 from pyramid import compat
38
38
39 import rhodecode
39 import rhodecode
40 from rhodecode.translation import lazy_ugettext
40 from rhodecode.translation import lazy_ugettext
41 from rhodecode.lib.utils2 import safe_str, safe_unicode, CachedProperty
41 from rhodecode.lib.utils2 import safe_str, safe_unicode, CachedProperty
42 from rhodecode.lib.vcs import connection
42 from rhodecode.lib.vcs import connection
43 from rhodecode.lib.vcs.utils import author_name, author_email
43 from rhodecode.lib.vcs.utils import author_name, author_email
44 from rhodecode.lib.vcs.conf import settings
44 from rhodecode.lib.vcs.conf import settings
45 from rhodecode.lib.vcs.exceptions import (
45 from rhodecode.lib.vcs.exceptions import (
46 CommitError, EmptyRepositoryError, NodeAlreadyAddedError,
46 CommitError, EmptyRepositoryError, NodeAlreadyAddedError,
47 NodeAlreadyChangedError, NodeAlreadyExistsError, NodeAlreadyRemovedError,
47 NodeAlreadyChangedError, NodeAlreadyExistsError, NodeAlreadyRemovedError,
48 NodeDoesNotExistError, NodeNotChangedError, VCSError,
48 NodeDoesNotExistError, NodeNotChangedError, VCSError,
49 ImproperArchiveTypeError, BranchDoesNotExistError, CommitDoesNotExistError,
49 ImproperArchiveTypeError, BranchDoesNotExistError, CommitDoesNotExistError,
50 RepositoryError)
50 RepositoryError)
51
51
52
52
53 log = logging.getLogger(__name__)
53 log = logging.getLogger(__name__)
54
54
55
55
56 FILEMODE_DEFAULT = 0o100644
56 FILEMODE_DEFAULT = 0o100644
57 FILEMODE_EXECUTABLE = 0o100755
57 FILEMODE_EXECUTABLE = 0o100755
58 EMPTY_COMMIT_ID = '0' * 40
58 EMPTY_COMMIT_ID = '0' * 40
59
59
60 Reference = collections.namedtuple('Reference', ('type', 'name', 'commit_id'))
60 Reference = collections.namedtuple('Reference', ('type', 'name', 'commit_id'))
61
61
62
62
63 class MergeFailureReason(object):
63 class MergeFailureReason(object):
64 """
64 """
65 Enumeration with all the reasons why the server side merge could fail.
65 Enumeration with all the reasons why the server side merge could fail.
66
66
67 DO NOT change the number of the reasons, as they may be stored in the
67 DO NOT change the number of the reasons, as they may be stored in the
68 database.
68 database.
69
69
70 Changing the name of a reason is acceptable and encouraged to deprecate old
70 Changing the name of a reason is acceptable and encouraged to deprecate old
71 reasons.
71 reasons.
72 """
72 """
73
73
74 # Everything went well.
74 # Everything went well.
75 NONE = 0
75 NONE = 0
76
76
77 # An unexpected exception was raised. Check the logs for more details.
77 # An unexpected exception was raised. Check the logs for more details.
78 UNKNOWN = 1
78 UNKNOWN = 1
79
79
80 # The merge was not successful, there are conflicts.
80 # The merge was not successful, there are conflicts.
81 MERGE_FAILED = 2
81 MERGE_FAILED = 2
82
82
83 # The merge succeeded but we could not push it to the target repository.
83 # The merge succeeded but we could not push it to the target repository.
84 PUSH_FAILED = 3
84 PUSH_FAILED = 3
85
85
86 # The specified target is not a head in the target repository.
86 # The specified target is not a head in the target repository.
87 TARGET_IS_NOT_HEAD = 4
87 TARGET_IS_NOT_HEAD = 4
88
88
89 # The source repository contains more branches than the target. Pushing
89 # The source repository contains more branches than the target. Pushing
90 # the merge will create additional branches in the target.
90 # the merge will create additional branches in the target.
91 HG_SOURCE_HAS_MORE_BRANCHES = 5
91 HG_SOURCE_HAS_MORE_BRANCHES = 5
92
92
93 # The target reference has multiple heads. That does not allow to correctly
93 # The target reference has multiple heads. That does not allow to correctly
94 # identify the target location. This could only happen for mercurial
94 # identify the target location. This could only happen for mercurial
95 # branches.
95 # branches.
96 HG_TARGET_HAS_MULTIPLE_HEADS = 6
96 HG_TARGET_HAS_MULTIPLE_HEADS = 6
97
97
98 # The target repository is locked
98 # The target repository is locked
99 TARGET_IS_LOCKED = 7
99 TARGET_IS_LOCKED = 7
100
100
101 # Deprecated, use MISSING_TARGET_REF or MISSING_SOURCE_REF instead.
101 # Deprecated, use MISSING_TARGET_REF or MISSING_SOURCE_REF instead.
102 # A involved commit could not be found.
102 # A involved commit could not be found.
103 _DEPRECATED_MISSING_COMMIT = 8
103 _DEPRECATED_MISSING_COMMIT = 8
104
104
105 # The target repo reference is missing.
105 # The target repo reference is missing.
106 MISSING_TARGET_REF = 9
106 MISSING_TARGET_REF = 9
107
107
108 # The source repo reference is missing.
108 # The source repo reference is missing.
109 MISSING_SOURCE_REF = 10
109 MISSING_SOURCE_REF = 10
110
110
111 # The merge was not successful, there are conflicts related to sub
111 # The merge was not successful, there are conflicts related to sub
112 # repositories.
112 # repositories.
113 SUBREPO_MERGE_FAILED = 11
113 SUBREPO_MERGE_FAILED = 11
114
114
115
115
116 class UpdateFailureReason(object):
116 class UpdateFailureReason(object):
117 """
117 """
118 Enumeration with all the reasons why the pull request update could fail.
118 Enumeration with all the reasons why the pull request update could fail.
119
119
120 DO NOT change the number of the reasons, as they may be stored in the
120 DO NOT change the number of the reasons, as they may be stored in the
121 database.
121 database.
122
122
123 Changing the name of a reason is acceptable and encouraged to deprecate old
123 Changing the name of a reason is acceptable and encouraged to deprecate old
124 reasons.
124 reasons.
125 """
125 """
126
126
127 # Everything went well.
127 # Everything went well.
128 NONE = 0
128 NONE = 0
129
129
130 # An unexpected exception was raised. Check the logs for more details.
130 # An unexpected exception was raised. Check the logs for more details.
131 UNKNOWN = 1
131 UNKNOWN = 1
132
132
133 # The pull request is up to date.
133 # The pull request is up to date.
134 NO_CHANGE = 2
134 NO_CHANGE = 2
135
135
136 # The pull request has a reference type that is not supported for update.
136 # The pull request has a reference type that is not supported for update.
137 WRONG_REF_TYPE = 3
137 WRONG_REF_TYPE = 3
138
138
139 # Update failed because the target reference is missing.
139 # Update failed because the target reference is missing.
140 MISSING_TARGET_REF = 4
140 MISSING_TARGET_REF = 4
141
141
142 # Update failed because the source reference is missing.
142 # Update failed because the source reference is missing.
143 MISSING_SOURCE_REF = 5
143 MISSING_SOURCE_REF = 5
144
144
145
145
146 class MergeResponse(object):
146 class MergeResponse(object):
147
147
148 # uses .format(**metadata) for variables
148 # uses .format(**metadata) for variables
149 MERGE_STATUS_MESSAGES = {
149 MERGE_STATUS_MESSAGES = {
150 MergeFailureReason.NONE: lazy_ugettext(
150 MergeFailureReason.NONE: lazy_ugettext(
151 u'This pull request can be automatically merged.'),
151 u'This pull request can be automatically merged.'),
152 MergeFailureReason.UNKNOWN: lazy_ugettext(
152 MergeFailureReason.UNKNOWN: lazy_ugettext(
153 u'This pull request cannot be merged because of an unhandled exception. '
153 u'This pull request cannot be merged because of an unhandled exception. '
154 u'{exception}'),
154 u'{exception}'),
155 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
155 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
156 u'This pull request cannot be merged because of merge conflicts.'),
156 u'This pull request cannot be merged because of merge conflicts.'),
157 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
157 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
158 u'This pull request could not be merged because push to '
158 u'This pull request could not be merged because push to '
159 u'target:`{target}@{merge_commit}` failed.'),
159 u'target:`{target}@{merge_commit}` failed.'),
160 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
160 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
161 u'This pull request cannot be merged because the target '
161 u'This pull request cannot be merged because the target '
162 u'`{target_ref.name}` is not a head.'),
162 u'`{target_ref.name}` is not a head.'),
163 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
163 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
164 u'This pull request cannot be merged because the source contains '
164 u'This pull request cannot be merged because the source contains '
165 u'more branches than the target.'),
165 u'more branches than the target.'),
166 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
166 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
167 u'This pull request cannot be merged because the target `{target_ref.name}` '
167 u'This pull request cannot be merged because the target `{target_ref.name}` '
168 u'has multiple heads: `{heads}`.'),
168 u'has multiple heads: `{heads}`.'),
169 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
169 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
170 u'This pull request cannot be merged because the target repository is '
170 u'This pull request cannot be merged because the target repository is '
171 u'locked by {locked_by}.'),
171 u'locked by {locked_by}.'),
172
172
173 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
173 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
174 u'This pull request cannot be merged because the target '
174 u'This pull request cannot be merged because the target '
175 u'reference `{target_ref.name}` is missing.'),
175 u'reference `{target_ref.name}` is missing.'),
176 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
176 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
177 u'This pull request cannot be merged because the source '
177 u'This pull request cannot be merged because the source '
178 u'reference `{source_ref.name}` is missing.'),
178 u'reference `{source_ref.name}` is missing.'),
179 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
179 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
180 u'This pull request cannot be merged because of conflicts related '
180 u'This pull request cannot be merged because of conflicts related '
181 u'to sub repositories.'),
181 u'to sub repositories.'),
182
182
183 # Deprecations
183 # Deprecations
184 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
184 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
185 u'This pull request cannot be merged because the target or the '
185 u'This pull request cannot be merged because the target or the '
186 u'source reference is missing.'),
186 u'source reference is missing.'),
187
187
188 }
188 }
189
189
190 def __init__(self, possible, executed, merge_ref, failure_reason, metadata=None):
190 def __init__(self, possible, executed, merge_ref, failure_reason, metadata=None):
191 self.possible = possible
191 self.possible = possible
192 self.executed = executed
192 self.executed = executed
193 self.merge_ref = merge_ref
193 self.merge_ref = merge_ref
194 self.failure_reason = failure_reason
194 self.failure_reason = failure_reason
195 self.metadata = metadata or {}
195 self.metadata = metadata or {}
196
196
197 def __repr__(self):
197 def __repr__(self):
198 return '<MergeResponse:{} {}>'.format(self.label, self.failure_reason)
198 return '<MergeResponse:{} {}>'.format(self.label, self.failure_reason)
199
199
200 def __eq__(self, other):
200 def __eq__(self, other):
201 same_instance = isinstance(other, self.__class__)
201 same_instance = isinstance(other, self.__class__)
202 return same_instance \
202 return same_instance \
203 and self.possible == other.possible \
203 and self.possible == other.possible \
204 and self.executed == other.executed \
204 and self.executed == other.executed \
205 and self.failure_reason == other.failure_reason
205 and self.failure_reason == other.failure_reason
206
206
207 @property
207 @property
208 def label(self):
208 def label(self):
209 label_dict = dict((v, k) for k, v in MergeFailureReason.__dict__.items() if
209 label_dict = dict((v, k) for k, v in MergeFailureReason.__dict__.items() if
210 not k.startswith('_'))
210 not k.startswith('_'))
211 return label_dict.get(self.failure_reason)
211 return label_dict.get(self.failure_reason)
212
212
213 @property
213 @property
214 def merge_status_message(self):
214 def merge_status_message(self):
215 """
215 """
216 Return a human friendly error message for the given merge status code.
216 Return a human friendly error message for the given merge status code.
217 """
217 """
218 msg = safe_unicode(self.MERGE_STATUS_MESSAGES[self.failure_reason])
218 msg = safe_unicode(self.MERGE_STATUS_MESSAGES[self.failure_reason])
219 try:
219 try:
220 return msg.format(**self.metadata)
220 return msg.format(**self.metadata)
221 except Exception:
221 except Exception:
222 log.exception('Failed to format %s message', self)
222 log.exception('Failed to format %s message', self)
223 return msg
223 return msg
224
224
225 def asdict(self):
225 def asdict(self):
226 data = {}
226 data = {}
227 for k in ['possible', 'executed', 'merge_ref', 'failure_reason',
227 for k in ['possible', 'executed', 'merge_ref', 'failure_reason',
228 'merge_status_message']:
228 'merge_status_message']:
229 data[k] = getattr(self, k)
229 data[k] = getattr(self, k)
230 return data
230 return data
231
231
232
232
233 class BaseRepository(object):
233 class BaseRepository(object):
234 """
234 """
235 Base Repository for final backends
235 Base Repository for final backends
236
236
237 .. attribute:: DEFAULT_BRANCH_NAME
237 .. attribute:: DEFAULT_BRANCH_NAME
238
238
239 name of default branch (i.e. "trunk" for svn, "master" for git etc.
239 name of default branch (i.e. "trunk" for svn, "master" for git etc.
240
240
241 .. attribute:: commit_ids
241 .. attribute:: commit_ids
242
242
243 list of all available commit ids, in ascending order
243 list of all available commit ids, in ascending order
244
244
245 .. attribute:: path
245 .. attribute:: path
246
246
247 absolute path to the repository
247 absolute path to the repository
248
248
249 .. attribute:: bookmarks
249 .. attribute:: bookmarks
250
250
251 Mapping from name to :term:`Commit ID` of the bookmark. Empty in case
251 Mapping from name to :term:`Commit ID` of the bookmark. Empty in case
252 there are no bookmarks or the backend implementation does not support
252 there are no bookmarks or the backend implementation does not support
253 bookmarks.
253 bookmarks.
254
254
255 .. attribute:: tags
255 .. attribute:: tags
256
256
257 Mapping from name to :term:`Commit ID` of the tag.
257 Mapping from name to :term:`Commit ID` of the tag.
258
258
259 """
259 """
260
260
261 DEFAULT_BRANCH_NAME = None
261 DEFAULT_BRANCH_NAME = None
262 DEFAULT_CONTACT = u"Unknown"
262 DEFAULT_CONTACT = u"Unknown"
263 DEFAULT_DESCRIPTION = u"unknown"
263 DEFAULT_DESCRIPTION = u"unknown"
264 EMPTY_COMMIT_ID = '0' * 40
264 EMPTY_COMMIT_ID = '0' * 40
265
265
266 path = None
266 path = None
267
267
268 _is_empty = None
268 _is_empty = None
269 _commit_ids = {}
269 _commit_ids = {}
270
270
271 def __init__(self, repo_path, config=None, create=False, **kwargs):
271 def __init__(self, repo_path, config=None, create=False, **kwargs):
272 """
272 """
273 Initializes repository. Raises RepositoryError if repository could
273 Initializes repository. Raises RepositoryError if repository could
274 not be find at the given ``repo_path`` or directory at ``repo_path``
274 not be find at the given ``repo_path`` or directory at ``repo_path``
275 exists and ``create`` is set to True.
275 exists and ``create`` is set to True.
276
276
277 :param repo_path: local path of the repository
277 :param repo_path: local path of the repository
278 :param config: repository configuration
278 :param config: repository configuration
279 :param create=False: if set to True, would try to create repository.
279 :param create=False: if set to True, would try to create repository.
280 :param src_url=None: if set, should be proper url from which repository
280 :param src_url=None: if set, should be proper url from which repository
281 would be cloned; requires ``create`` parameter to be set to True -
281 would be cloned; requires ``create`` parameter to be set to True -
282 raises RepositoryError if src_url is set and create evaluates to
282 raises RepositoryError if src_url is set and create evaluates to
283 False
283 False
284 """
284 """
285 raise NotImplementedError
285 raise NotImplementedError
286
286
287 def __repr__(self):
287 def __repr__(self):
288 return '<%s at %s>' % (self.__class__.__name__, self.path)
288 return '<%s at %s>' % (self.__class__.__name__, self.path)
289
289
290 def __len__(self):
290 def __len__(self):
291 return self.count()
291 return self.count()
292
292
293 def __eq__(self, other):
293 def __eq__(self, other):
294 same_instance = isinstance(other, self.__class__)
294 same_instance = isinstance(other, self.__class__)
295 return same_instance and other.path == self.path
295 return same_instance and other.path == self.path
296
296
297 def __ne__(self, other):
297 def __ne__(self, other):
298 return not self.__eq__(other)
298 return not self.__eq__(other)
299
299
300 def get_create_shadow_cache_pr_path(self, db_repo):
300 def get_create_shadow_cache_pr_path(self, db_repo):
301 path = db_repo.cached_diffs_dir
301 path = db_repo.cached_diffs_dir
302 if not os.path.exists(path):
302 if not os.path.exists(path):
303 os.makedirs(path, 0o755)
303 os.makedirs(path, 0o755)
304 return path
304 return path
305
305
306 @classmethod
306 @classmethod
307 def get_default_config(cls, default=None):
307 def get_default_config(cls, default=None):
308 config = Config()
308 config = Config()
309 if default and isinstance(default, list):
309 if default and isinstance(default, list):
310 for section, key, val in default:
310 for section, key, val in default:
311 config.set(section, key, val)
311 config.set(section, key, val)
312 return config
312 return config
313
313
314 @LazyProperty
314 @LazyProperty
315 def _remote(self):
315 def _remote(self):
316 raise NotImplementedError
316 raise NotImplementedError
317
317
318 def _heads(self, branch=None):
318 def _heads(self, branch=None):
319 return []
319 return []
320
320
321 @LazyProperty
321 @LazyProperty
322 def EMPTY_COMMIT(self):
322 def EMPTY_COMMIT(self):
323 return EmptyCommit(self.EMPTY_COMMIT_ID)
323 return EmptyCommit(self.EMPTY_COMMIT_ID)
324
324
325 @LazyProperty
325 @LazyProperty
326 def alias(self):
326 def alias(self):
327 for k, v in settings.BACKENDS.items():
327 for k, v in settings.BACKENDS.items():
328 if v.split('.')[-1] == str(self.__class__.__name__):
328 if v.split('.')[-1] == str(self.__class__.__name__):
329 return k
329 return k
330
330
331 @LazyProperty
331 @LazyProperty
332 def name(self):
332 def name(self):
333 return safe_unicode(os.path.basename(self.path))
333 return safe_unicode(os.path.basename(self.path))
334
334
335 @LazyProperty
335 @LazyProperty
336 def description(self):
336 def description(self):
337 raise NotImplementedError
337 raise NotImplementedError
338
338
339 def refs(self):
339 def refs(self):
340 """
340 """
341 returns a `dict` with branches, bookmarks, tags, and closed_branches
341 returns a `dict` with branches, bookmarks, tags, and closed_branches
342 for this repository
342 for this repository
343 """
343 """
344 return dict(
344 return dict(
345 branches=self.branches,
345 branches=self.branches,
346 branches_closed=self.branches_closed,
346 branches_closed=self.branches_closed,
347 tags=self.tags,
347 tags=self.tags,
348 bookmarks=self.bookmarks
348 bookmarks=self.bookmarks
349 )
349 )
350
350
351 @LazyProperty
351 @LazyProperty
352 def branches(self):
352 def branches(self):
353 """
353 """
354 A `dict` which maps branch names to commit ids.
354 A `dict` which maps branch names to commit ids.
355 """
355 """
356 raise NotImplementedError
356 raise NotImplementedError
357
357
358 @LazyProperty
358 @LazyProperty
359 def branches_closed(self):
359 def branches_closed(self):
360 """
360 """
361 A `dict` which maps tags names to commit ids.
361 A `dict` which maps tags names to commit ids.
362 """
362 """
363 raise NotImplementedError
363 raise NotImplementedError
364
364
365 @LazyProperty
365 @LazyProperty
366 def bookmarks(self):
366 def bookmarks(self):
367 """
367 """
368 A `dict` which maps tags names to commit ids.
368 A `dict` which maps tags names to commit ids.
369 """
369 """
370 raise NotImplementedError
370 raise NotImplementedError
371
371
372 @LazyProperty
372 @LazyProperty
373 def tags(self):
373 def tags(self):
374 """
374 """
375 A `dict` which maps tags names to commit ids.
375 A `dict` which maps tags names to commit ids.
376 """
376 """
377 raise NotImplementedError
377 raise NotImplementedError
378
378
379 @LazyProperty
379 @LazyProperty
380 def size(self):
380 def size(self):
381 """
381 """
382 Returns combined size in bytes for all repository files
382 Returns combined size in bytes for all repository files
383 """
383 """
384 tip = self.get_commit()
384 tip = self.get_commit()
385 return tip.size
385 return tip.size
386
386
387 def size_at_commit(self, commit_id):
387 def size_at_commit(self, commit_id):
388 commit = self.get_commit(commit_id)
388 commit = self.get_commit(commit_id)
389 return commit.size
389 return commit.size
390
390
391 def _check_for_empty(self):
391 def _check_for_empty(self):
392 no_commits = len(self._commit_ids) == 0
392 no_commits = len(self._commit_ids) == 0
393 if no_commits:
393 if no_commits:
394 # check on remote to be sure
394 # check on remote to be sure
395 return self._remote.is_empty()
395 return self._remote.is_empty()
396 else:
396 else:
397 return False
397 return False
398
398
399 def is_empty(self):
399 def is_empty(self):
400 if rhodecode.is_test:
400 if rhodecode.is_test:
401 return self._check_for_empty()
401 return self._check_for_empty()
402
402
403 if self._is_empty is None:
403 if self._is_empty is None:
404 # cache empty for production, but not tests
404 # cache empty for production, but not tests
405 self._is_empty = self._check_for_empty()
405 self._is_empty = self._check_for_empty()
406
406
407 return self._is_empty
407 return self._is_empty
408
408
409 @staticmethod
409 @staticmethod
410 def check_url(url, config):
410 def check_url(url, config):
411 """
411 """
412 Function will check given url and try to verify if it's a valid
412 Function will check given url and try to verify if it's a valid
413 link.
413 link.
414 """
414 """
415 raise NotImplementedError
415 raise NotImplementedError
416
416
417 @staticmethod
417 @staticmethod
418 def is_valid_repository(path):
418 def is_valid_repository(path):
419 """
419 """
420 Check if given `path` contains a valid repository of this backend
420 Check if given `path` contains a valid repository of this backend
421 """
421 """
422 raise NotImplementedError
422 raise NotImplementedError
423
423
424 # ==========================================================================
424 # ==========================================================================
425 # COMMITS
425 # COMMITS
426 # ==========================================================================
426 # ==========================================================================
427
427
428 @CachedProperty
428 @CachedProperty
429 def commit_ids(self):
429 def commit_ids(self):
430 raise NotImplementedError
430 raise NotImplementedError
431
431
432 def append_commit_id(self, commit_id):
432 def append_commit_id(self, commit_id):
433 if commit_id not in self.commit_ids:
433 if commit_id not in self.commit_ids:
434 self._rebuild_cache(self.commit_ids + [commit_id])
434 self._rebuild_cache(self.commit_ids + [commit_id])
435
435
436 # clear cache
436 # clear cache
437 self._invalidate_prop_cache('commit_ids')
437 self._invalidate_prop_cache('commit_ids')
438 self._is_empty = False
438 self._is_empty = False
439
439
440 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None, translate_tag=None):
440 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None, translate_tag=None):
441 """
441 """
442 Returns instance of `BaseCommit` class. If `commit_id` and `commit_idx`
442 Returns instance of `BaseCommit` class. If `commit_id` and `commit_idx`
443 are both None, most recent commit is returned.
443 are both None, most recent commit is returned.
444
444
445 :param pre_load: Optional. List of commit attributes to load.
445 :param pre_load: Optional. List of commit attributes to load.
446
446
447 :raises ``EmptyRepositoryError``: if there are no commits
447 :raises ``EmptyRepositoryError``: if there are no commits
448 """
448 """
449 raise NotImplementedError
449 raise NotImplementedError
450
450
451 def __iter__(self):
451 def __iter__(self):
452 for commit_id in self.commit_ids:
452 for commit_id in self.commit_ids:
453 yield self.get_commit(commit_id=commit_id)
453 yield self.get_commit(commit_id=commit_id)
454
454
455 def get_commits(
455 def get_commits(
456 self, start_id=None, end_id=None, start_date=None, end_date=None,
456 self, start_id=None, end_id=None, start_date=None, end_date=None,
457 branch_name=None, show_hidden=False, pre_load=None, translate_tags=None):
457 branch_name=None, show_hidden=False, pre_load=None, translate_tags=None):
458 """
458 """
459 Returns iterator of `BaseCommit` objects from start to end
459 Returns iterator of `BaseCommit` objects from start to end
460 not inclusive. This should behave just like a list, ie. end is not
460 not inclusive. This should behave just like a list, ie. end is not
461 inclusive.
461 inclusive.
462
462
463 :param start_id: None or str, must be a valid commit id
463 :param start_id: None or str, must be a valid commit id
464 :param end_id: None or str, must be a valid commit id
464 :param end_id: None or str, must be a valid commit id
465 :param start_date:
465 :param start_date:
466 :param end_date:
466 :param end_date:
467 :param branch_name:
467 :param branch_name:
468 :param show_hidden:
468 :param show_hidden:
469 :param pre_load:
469 :param pre_load:
470 :param translate_tags:
470 :param translate_tags:
471 """
471 """
472 raise NotImplementedError
472 raise NotImplementedError
473
473
474 def __getitem__(self, key):
474 def __getitem__(self, key):
475 """
475 """
476 Allows index based access to the commit objects of this repository.
476 Allows index based access to the commit objects of this repository.
477 """
477 """
478 pre_load = ["author", "branch", "date", "message", "parents"]
478 pre_load = ["author", "branch", "date", "message", "parents"]
479 if isinstance(key, slice):
479 if isinstance(key, slice):
480 return self._get_range(key, pre_load)
480 return self._get_range(key, pre_load)
481 return self.get_commit(commit_idx=key, pre_load=pre_load)
481 return self.get_commit(commit_idx=key, pre_load=pre_load)
482
482
483 def _get_range(self, slice_obj, pre_load):
483 def _get_range(self, slice_obj, pre_load):
484 for commit_id in self.commit_ids.__getitem__(slice_obj):
484 for commit_id in self.commit_ids.__getitem__(slice_obj):
485 yield self.get_commit(commit_id=commit_id, pre_load=pre_load)
485 yield self.get_commit(commit_id=commit_id, pre_load=pre_load)
486
486
487 def count(self):
487 def count(self):
488 return len(self.commit_ids)
488 return len(self.commit_ids)
489
489
490 def tag(self, name, user, commit_id=None, message=None, date=None, **opts):
490 def tag(self, name, user, commit_id=None, message=None, date=None, **opts):
491 """
491 """
492 Creates and returns a tag for the given ``commit_id``.
492 Creates and returns a tag for the given ``commit_id``.
493
493
494 :param name: name for new tag
494 :param name: name for new tag
495 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
495 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
496 :param commit_id: commit id for which new tag would be created
496 :param commit_id: commit id for which new tag would be created
497 :param message: message of the tag's commit
497 :param message: message of the tag's commit
498 :param date: date of tag's commit
498 :param date: date of tag's commit
499
499
500 :raises TagAlreadyExistError: if tag with same name already exists
500 :raises TagAlreadyExistError: if tag with same name already exists
501 """
501 """
502 raise NotImplementedError
502 raise NotImplementedError
503
503
504 def remove_tag(self, name, user, message=None, date=None):
504 def remove_tag(self, name, user, message=None, date=None):
505 """
505 """
506 Removes tag with the given ``name``.
506 Removes tag with the given ``name``.
507
507
508 :param name: name of the tag to be removed
508 :param name: name of the tag to be removed
509 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
509 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
510 :param message: message of the tag's removal commit
510 :param message: message of the tag's removal commit
511 :param date: date of tag's removal commit
511 :param date: date of tag's removal commit
512
512
513 :raises TagDoesNotExistError: if tag with given name does not exists
513 :raises TagDoesNotExistError: if tag with given name does not exists
514 """
514 """
515 raise NotImplementedError
515 raise NotImplementedError
516
516
517 def get_diff(
517 def get_diff(
518 self, commit1, commit2, path=None, ignore_whitespace=False,
518 self, commit1, commit2, path=None, ignore_whitespace=False,
519 context=3, path1=None):
519 context=3, path1=None):
520 """
520 """
521 Returns (git like) *diff*, as plain text. Shows changes introduced by
521 Returns (git like) *diff*, as plain text. Shows changes introduced by
522 `commit2` since `commit1`.
522 `commit2` since `commit1`.
523
523
524 :param commit1: Entry point from which diff is shown. Can be
524 :param commit1: Entry point from which diff is shown. Can be
525 ``self.EMPTY_COMMIT`` - in this case, patch showing all
525 ``self.EMPTY_COMMIT`` - in this case, patch showing all
526 the changes since empty state of the repository until `commit2`
526 the changes since empty state of the repository until `commit2`
527 :param commit2: Until which commit changes should be shown.
527 :param commit2: Until which commit changes should be shown.
528 :param path: Can be set to a path of a file to create a diff of that
528 :param path: Can be set to a path of a file to create a diff of that
529 file. If `path1` is also set, this value is only associated to
529 file. If `path1` is also set, this value is only associated to
530 `commit2`.
530 `commit2`.
531 :param ignore_whitespace: If set to ``True``, would not show whitespace
531 :param ignore_whitespace: If set to ``True``, would not show whitespace
532 changes. Defaults to ``False``.
532 changes. Defaults to ``False``.
533 :param context: How many lines before/after changed lines should be
533 :param context: How many lines before/after changed lines should be
534 shown. Defaults to ``3``.
534 shown. Defaults to ``3``.
535 :param path1: Can be set to a path to associate with `commit1`. This
535 :param path1: Can be set to a path to associate with `commit1`. This
536 parameter works only for backends which support diff generation for
536 parameter works only for backends which support diff generation for
537 different paths. Other backends will raise a `ValueError` if `path1`
537 different paths. Other backends will raise a `ValueError` if `path1`
538 is set and has a different value than `path`.
538 is set and has a different value than `path`.
539 :param file_path: filter this diff by given path pattern
539 :param file_path: filter this diff by given path pattern
540 """
540 """
541 raise NotImplementedError
541 raise NotImplementedError
542
542
543 def strip(self, commit_id, branch=None):
543 def strip(self, commit_id, branch=None):
544 """
544 """
545 Strip given commit_id from the repository
545 Strip given commit_id from the repository
546 """
546 """
547 raise NotImplementedError
547 raise NotImplementedError
548
548
549 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
549 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
550 """
550 """
551 Return a latest common ancestor commit if one exists for this repo
551 Return a latest common ancestor commit if one exists for this repo
552 `commit_id1` vs `commit_id2` from `repo2`.
552 `commit_id1` vs `commit_id2` from `repo2`.
553
553
554 :param commit_id1: Commit it from this repository to use as a
554 :param commit_id1: Commit it from this repository to use as a
555 target for the comparison.
555 target for the comparison.
556 :param commit_id2: Source commit id to use for comparison.
556 :param commit_id2: Source commit id to use for comparison.
557 :param repo2: Source repository to use for comparison.
557 :param repo2: Source repository to use for comparison.
558 """
558 """
559 raise NotImplementedError
559 raise NotImplementedError
560
560
561 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
561 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
562 """
562 """
563 Compare this repository's revision `commit_id1` with `commit_id2`.
563 Compare this repository's revision `commit_id1` with `commit_id2`.
564
564
565 Returns a tuple(commits, ancestor) that would be merged from
565 Returns a tuple(commits, ancestor) that would be merged from
566 `commit_id2`. Doing a normal compare (``merge=False``), ``None``
566 `commit_id2`. Doing a normal compare (``merge=False``), ``None``
567 will be returned as ancestor.
567 will be returned as ancestor.
568
568
569 :param commit_id1: Commit it from this repository to use as a
569 :param commit_id1: Commit it from this repository to use as a
570 target for the comparison.
570 target for the comparison.
571 :param commit_id2: Source commit id to use for comparison.
571 :param commit_id2: Source commit id to use for comparison.
572 :param repo2: Source repository to use for comparison.
572 :param repo2: Source repository to use for comparison.
573 :param merge: If set to ``True`` will do a merge compare which also
573 :param merge: If set to ``True`` will do a merge compare which also
574 returns the common ancestor.
574 returns the common ancestor.
575 :param pre_load: Optional. List of commit attributes to load.
575 :param pre_load: Optional. List of commit attributes to load.
576 """
576 """
577 raise NotImplementedError
577 raise NotImplementedError
578
578
579 def merge(self, repo_id, workspace_id, target_ref, source_repo, source_ref,
579 def merge(self, repo_id, workspace_id, target_ref, source_repo, source_ref,
580 user_name='', user_email='', message='', dry_run=False,
580 user_name='', user_email='', message='', dry_run=False,
581 use_rebase=False, close_branch=False):
581 use_rebase=False, close_branch=False):
582 """
582 """
583 Merge the revisions specified in `source_ref` from `source_repo`
583 Merge the revisions specified in `source_ref` from `source_repo`
584 onto the `target_ref` of this repository.
584 onto the `target_ref` of this repository.
585
585
586 `source_ref` and `target_ref` are named tupls with the following
586 `source_ref` and `target_ref` are named tupls with the following
587 fields `type`, `name` and `commit_id`.
587 fields `type`, `name` and `commit_id`.
588
588
589 Returns a MergeResponse named tuple with the following fields
589 Returns a MergeResponse named tuple with the following fields
590 'possible', 'executed', 'source_commit', 'target_commit',
590 'possible', 'executed', 'source_commit', 'target_commit',
591 'merge_commit'.
591 'merge_commit'.
592
592
593 :param repo_id: `repo_id` target repo id.
593 :param repo_id: `repo_id` target repo id.
594 :param workspace_id: `workspace_id` unique identifier.
594 :param workspace_id: `workspace_id` unique identifier.
595 :param target_ref: `target_ref` points to the commit on top of which
595 :param target_ref: `target_ref` points to the commit on top of which
596 the `source_ref` should be merged.
596 the `source_ref` should be merged.
597 :param source_repo: The repository that contains the commits to be
597 :param source_repo: The repository that contains the commits to be
598 merged.
598 merged.
599 :param source_ref: `source_ref` points to the topmost commit from
599 :param source_ref: `source_ref` points to the topmost commit from
600 the `source_repo` which should be merged.
600 the `source_repo` which should be merged.
601 :param user_name: Merge commit `user_name`.
601 :param user_name: Merge commit `user_name`.
602 :param user_email: Merge commit `user_email`.
602 :param user_email: Merge commit `user_email`.
603 :param message: Merge commit `message`.
603 :param message: Merge commit `message`.
604 :param dry_run: If `True` the merge will not take place.
604 :param dry_run: If `True` the merge will not take place.
605 :param use_rebase: If `True` commits from the source will be rebased
605 :param use_rebase: If `True` commits from the source will be rebased
606 on top of the target instead of being merged.
606 on top of the target instead of being merged.
607 :param close_branch: If `True` branch will be close before merging it
607 :param close_branch: If `True` branch will be close before merging it
608 """
608 """
609 if dry_run:
609 if dry_run:
610 message = message or settings.MERGE_DRY_RUN_MESSAGE
610 message = message or settings.MERGE_DRY_RUN_MESSAGE
611 user_email = user_email or settings.MERGE_DRY_RUN_EMAIL
611 user_email = user_email or settings.MERGE_DRY_RUN_EMAIL
612 user_name = user_name or settings.MERGE_DRY_RUN_USER
612 user_name = user_name or settings.MERGE_DRY_RUN_USER
613 else:
613 else:
614 if not user_name:
614 if not user_name:
615 raise ValueError('user_name cannot be empty')
615 raise ValueError('user_name cannot be empty')
616 if not user_email:
616 if not user_email:
617 raise ValueError('user_email cannot be empty')
617 raise ValueError('user_email cannot be empty')
618 if not message:
618 if not message:
619 raise ValueError('message cannot be empty')
619 raise ValueError('message cannot be empty')
620
620
621 try:
621 try:
622 return self._merge_repo(
622 return self._merge_repo(
623 repo_id, workspace_id, target_ref, source_repo,
623 repo_id, workspace_id, target_ref, source_repo,
624 source_ref, message, user_name, user_email, dry_run=dry_run,
624 source_ref, message, user_name, user_email, dry_run=dry_run,
625 use_rebase=use_rebase, close_branch=close_branch)
625 use_rebase=use_rebase, close_branch=close_branch)
626 except RepositoryError as exc:
626 except RepositoryError as exc:
627 log.exception('Unexpected failure when running merge, dry-run=%s', dry_run)
627 log.exception('Unexpected failure when running merge, dry-run=%s', dry_run)
628 return MergeResponse(
628 return MergeResponse(
629 False, False, None, MergeFailureReason.UNKNOWN,
629 False, False, None, MergeFailureReason.UNKNOWN,
630 metadata={'exception': str(exc)})
630 metadata={'exception': str(exc)})
631
631
632 def _merge_repo(self, repo_id, workspace_id, target_ref,
632 def _merge_repo(self, repo_id, workspace_id, target_ref,
633 source_repo, source_ref, merge_message,
633 source_repo, source_ref, merge_message,
634 merger_name, merger_email, dry_run=False,
634 merger_name, merger_email, dry_run=False,
635 use_rebase=False, close_branch=False):
635 use_rebase=False, close_branch=False):
636 """Internal implementation of merge."""
636 """Internal implementation of merge."""
637 raise NotImplementedError
637 raise NotImplementedError
638
638
639 def _maybe_prepare_merge_workspace(
639 def _maybe_prepare_merge_workspace(
640 self, repo_id, workspace_id, target_ref, source_ref):
640 self, repo_id, workspace_id, target_ref, source_ref):
641 """
641 """
642 Create the merge workspace.
642 Create the merge workspace.
643
643
644 :param workspace_id: `workspace_id` unique identifier.
644 :param workspace_id: `workspace_id` unique identifier.
645 """
645 """
646 raise NotImplementedError
646 raise NotImplementedError
647
647
648 def _get_legacy_shadow_repository_path(self, workspace_id):
648 def _get_legacy_shadow_repository_path(self, workspace_id):
649 """
649 """
650 Legacy version that was used before. We still need it for
650 Legacy version that was used before. We still need it for
651 backward compat
651 backward compat
652 """
652 """
653 return os.path.join(
653 return os.path.join(
654 os.path.dirname(self.path),
654 os.path.dirname(self.path),
655 '.__shadow_%s_%s' % (os.path.basename(self.path), workspace_id))
655 '.__shadow_%s_%s' % (os.path.basename(self.path), workspace_id))
656
656
657 def _get_shadow_repository_path(self, repo_id, workspace_id):
657 def _get_shadow_repository_path(self, repo_id, workspace_id):
658 # The name of the shadow repository must start with '.', so it is
658 # The name of the shadow repository must start with '.', so it is
659 # skipped by 'rhodecode.lib.utils.get_filesystem_repos'.
659 # skipped by 'rhodecode.lib.utils.get_filesystem_repos'.
660 legacy_repository_path = self._get_legacy_shadow_repository_path(workspace_id)
660 legacy_repository_path = self._get_legacy_shadow_repository_path(workspace_id)
661 if os.path.exists(legacy_repository_path):
661 if os.path.exists(legacy_repository_path):
662 return legacy_repository_path
662 return legacy_repository_path
663 else:
663 else:
664 return os.path.join(
664 return os.path.join(
665 os.path.dirname(self.path),
665 os.path.dirname(self.path),
666 '.__shadow_repo_%s_%s' % (repo_id, workspace_id))
666 '.__shadow_repo_%s_%s' % (repo_id, workspace_id))
667
667
668 def cleanup_merge_workspace(self, repo_id, workspace_id):
668 def cleanup_merge_workspace(self, repo_id, workspace_id):
669 """
669 """
670 Remove merge workspace.
670 Remove merge workspace.
671
671
672 This function MUST not fail in case there is no workspace associated to
672 This function MUST not fail in case there is no workspace associated to
673 the given `workspace_id`.
673 the given `workspace_id`.
674
674
675 :param workspace_id: `workspace_id` unique identifier.
675 :param workspace_id: `workspace_id` unique identifier.
676 """
676 """
677 shadow_repository_path = self._get_shadow_repository_path(repo_id, workspace_id)
677 shadow_repository_path = self._get_shadow_repository_path(repo_id, workspace_id)
678 shadow_repository_path_del = '{}.{}.delete'.format(
678 shadow_repository_path_del = '{}.{}.delete'.format(
679 shadow_repository_path, time.time())
679 shadow_repository_path, time.time())
680
680
681 # move the shadow repo, so it never conflicts with the one used.
681 # move the shadow repo, so it never conflicts with the one used.
682 # we use this method because shutil.rmtree had some edge case problems
682 # we use this method because shutil.rmtree had some edge case problems
683 # removing symlinked repositories
683 # removing symlinked repositories
684 if not os.path.isdir(shadow_repository_path):
684 if not os.path.isdir(shadow_repository_path):
685 return
685 return
686
686
687 shutil.move(shadow_repository_path, shadow_repository_path_del)
687 shutil.move(shadow_repository_path, shadow_repository_path_del)
688 try:
688 try:
689 shutil.rmtree(shadow_repository_path_del, ignore_errors=False)
689 shutil.rmtree(shadow_repository_path_del, ignore_errors=False)
690 except Exception:
690 except Exception:
691 log.exception('Failed to gracefully remove shadow repo under %s',
691 log.exception('Failed to gracefully remove shadow repo under %s',
692 shadow_repository_path_del)
692 shadow_repository_path_del)
693 shutil.rmtree(shadow_repository_path_del, ignore_errors=True)
693 shutil.rmtree(shadow_repository_path_del, ignore_errors=True)
694
694
695 # ========== #
695 # ========== #
696 # COMMIT API #
696 # COMMIT API #
697 # ========== #
697 # ========== #
698
698
699 @LazyProperty
699 @LazyProperty
700 def in_memory_commit(self):
700 def in_memory_commit(self):
701 """
701 """
702 Returns :class:`InMemoryCommit` object for this repository.
702 Returns :class:`InMemoryCommit` object for this repository.
703 """
703 """
704 raise NotImplementedError
704 raise NotImplementedError
705
705
706 # ======================== #
706 # ======================== #
707 # UTILITIES FOR SUBCLASSES #
707 # UTILITIES FOR SUBCLASSES #
708 # ======================== #
708 # ======================== #
709
709
710 def _validate_diff_commits(self, commit1, commit2):
710 def _validate_diff_commits(self, commit1, commit2):
711 """
711 """
712 Validates that the given commits are related to this repository.
712 Validates that the given commits are related to this repository.
713
713
714 Intended as a utility for sub classes to have a consistent validation
714 Intended as a utility for sub classes to have a consistent validation
715 of input parameters in methods like :meth:`get_diff`.
715 of input parameters in methods like :meth:`get_diff`.
716 """
716 """
717 self._validate_commit(commit1)
717 self._validate_commit(commit1)
718 self._validate_commit(commit2)
718 self._validate_commit(commit2)
719 if (isinstance(commit1, EmptyCommit) and
719 if (isinstance(commit1, EmptyCommit) and
720 isinstance(commit2, EmptyCommit)):
720 isinstance(commit2, EmptyCommit)):
721 raise ValueError("Cannot compare two empty commits")
721 raise ValueError("Cannot compare two empty commits")
722
722
723 def _validate_commit(self, commit):
723 def _validate_commit(self, commit):
724 if not isinstance(commit, BaseCommit):
724 if not isinstance(commit, BaseCommit):
725 raise TypeError(
725 raise TypeError(
726 "%s is not of type BaseCommit" % repr(commit))
726 "%s is not of type BaseCommit" % repr(commit))
727 if commit.repository != self and not isinstance(commit, EmptyCommit):
727 if commit.repository != self and not isinstance(commit, EmptyCommit):
728 raise ValueError(
728 raise ValueError(
729 "Commit %s must be a valid commit from this repository %s, "
729 "Commit %s must be a valid commit from this repository %s, "
730 "related to this repository instead %s." %
730 "related to this repository instead %s." %
731 (commit, self, commit.repository))
731 (commit, self, commit.repository))
732
732
733 def _validate_commit_id(self, commit_id):
733 def _validate_commit_id(self, commit_id):
734 if not isinstance(commit_id, compat.string_types):
734 if not isinstance(commit_id, compat.string_types):
735 raise TypeError("commit_id must be a string value")
735 raise TypeError("commit_id must be a string value")
736
736
737 def _validate_commit_idx(self, commit_idx):
737 def _validate_commit_idx(self, commit_idx):
738 if not isinstance(commit_idx, (int, long)):
738 if not isinstance(commit_idx, (int, long)):
739 raise TypeError("commit_idx must be a numeric value")
739 raise TypeError("commit_idx must be a numeric value")
740
740
741 def _validate_branch_name(self, branch_name):
741 def _validate_branch_name(self, branch_name):
742 if branch_name and branch_name not in self.branches_all:
742 if branch_name and branch_name not in self.branches_all:
743 msg = ("Branch %s not found in %s" % (branch_name, self))
743 msg = ("Branch %s not found in %s" % (branch_name, self))
744 raise BranchDoesNotExistError(msg)
744 raise BranchDoesNotExistError(msg)
745
745
746 #
746 #
747 # Supporting deprecated API parts
747 # Supporting deprecated API parts
748 # TODO: johbo: consider to move this into a mixin
748 # TODO: johbo: consider to move this into a mixin
749 #
749 #
750
750
751 @property
751 @property
752 def EMPTY_CHANGESET(self):
752 def EMPTY_CHANGESET(self):
753 warnings.warn(
753 warnings.warn(
754 "Use EMPTY_COMMIT or EMPTY_COMMIT_ID instead", DeprecationWarning)
754 "Use EMPTY_COMMIT or EMPTY_COMMIT_ID instead", DeprecationWarning)
755 return self.EMPTY_COMMIT_ID
755 return self.EMPTY_COMMIT_ID
756
756
757 @property
757 @property
758 def revisions(self):
758 def revisions(self):
759 warnings.warn("Use commits attribute instead", DeprecationWarning)
759 warnings.warn("Use commits attribute instead", DeprecationWarning)
760 return self.commit_ids
760 return self.commit_ids
761
761
762 @revisions.setter
762 @revisions.setter
763 def revisions(self, value):
763 def revisions(self, value):
764 warnings.warn("Use commits attribute instead", DeprecationWarning)
764 warnings.warn("Use commits attribute instead", DeprecationWarning)
765 self.commit_ids = value
765 self.commit_ids = value
766
766
767 def get_changeset(self, revision=None, pre_load=None):
767 def get_changeset(self, revision=None, pre_load=None):
768 warnings.warn("Use get_commit instead", DeprecationWarning)
768 warnings.warn("Use get_commit instead", DeprecationWarning)
769 commit_id = None
769 commit_id = None
770 commit_idx = None
770 commit_idx = None
771 if isinstance(revision, compat.string_types):
771 if isinstance(revision, compat.string_types):
772 commit_id = revision
772 commit_id = revision
773 else:
773 else:
774 commit_idx = revision
774 commit_idx = revision
775 return self.get_commit(
775 return self.get_commit(
776 commit_id=commit_id, commit_idx=commit_idx, pre_load=pre_load)
776 commit_id=commit_id, commit_idx=commit_idx, pre_load=pre_load)
777
777
778 def get_changesets(
778 def get_changesets(
779 self, start=None, end=None, start_date=None, end_date=None,
779 self, start=None, end=None, start_date=None, end_date=None,
780 branch_name=None, pre_load=None):
780 branch_name=None, pre_load=None):
781 warnings.warn("Use get_commits instead", DeprecationWarning)
781 warnings.warn("Use get_commits instead", DeprecationWarning)
782 start_id = self._revision_to_commit(start)
782 start_id = self._revision_to_commit(start)
783 end_id = self._revision_to_commit(end)
783 end_id = self._revision_to_commit(end)
784 return self.get_commits(
784 return self.get_commits(
785 start_id=start_id, end_id=end_id, start_date=start_date,
785 start_id=start_id, end_id=end_id, start_date=start_date,
786 end_date=end_date, branch_name=branch_name, pre_load=pre_load)
786 end_date=end_date, branch_name=branch_name, pre_load=pre_load)
787
787
788 def _revision_to_commit(self, revision):
788 def _revision_to_commit(self, revision):
789 """
789 """
790 Translates a revision to a commit_id
790 Translates a revision to a commit_id
791
791
792 Helps to support the old changeset based API which allows to use
792 Helps to support the old changeset based API which allows to use
793 commit ids and commit indices interchangeable.
793 commit ids and commit indices interchangeable.
794 """
794 """
795 if revision is None:
795 if revision is None:
796 return revision
796 return revision
797
797
798 if isinstance(revision, compat.string_types):
798 if isinstance(revision, compat.string_types):
799 commit_id = revision
799 commit_id = revision
800 else:
800 else:
801 commit_id = self.commit_ids[revision]
801 commit_id = self.commit_ids[revision]
802 return commit_id
802 return commit_id
803
803
804 @property
804 @property
805 def in_memory_changeset(self):
805 def in_memory_changeset(self):
806 warnings.warn("Use in_memory_commit instead", DeprecationWarning)
806 warnings.warn("Use in_memory_commit instead", DeprecationWarning)
807 return self.in_memory_commit
807 return self.in_memory_commit
808
808
809 def get_path_permissions(self, username):
809 def get_path_permissions(self, username):
810 """
810 """
811 Returns a path permission checker or None if not supported
811 Returns a path permission checker or None if not supported
812
812
813 :param username: session user name
813 :param username: session user name
814 :return: an instance of BasePathPermissionChecker or None
814 :return: an instance of BasePathPermissionChecker or None
815 """
815 """
816 return None
816 return None
817
817
818 def install_hooks(self, force=False):
818 def install_hooks(self, force=False):
819 return self._remote.install_hooks(force)
819 return self._remote.install_hooks(force)
820
820
821 def get_hooks_info(self):
821 def get_hooks_info(self):
822 return self._remote.get_hooks_info()
822 return self._remote.get_hooks_info()
823
823
824
824
825 class BaseCommit(object):
825 class BaseCommit(object):
826 """
826 """
827 Each backend should implement it's commit representation.
827 Each backend should implement it's commit representation.
828
828
829 **Attributes**
829 **Attributes**
830
830
831 ``repository``
831 ``repository``
832 repository object within which commit exists
832 repository object within which commit exists
833
833
834 ``id``
834 ``id``
835 The commit id, may be ``raw_id`` or i.e. for mercurial's tip
835 The commit id, may be ``raw_id`` or i.e. for mercurial's tip
836 just ``tip``.
836 just ``tip``.
837
837
838 ``raw_id``
838 ``raw_id``
839 raw commit representation (i.e. full 40 length sha for git
839 raw commit representation (i.e. full 40 length sha for git
840 backend)
840 backend)
841
841
842 ``short_id``
842 ``short_id``
843 shortened (if apply) version of ``raw_id``; it would be simple
843 shortened (if apply) version of ``raw_id``; it would be simple
844 shortcut for ``raw_id[:12]`` for git/mercurial backends or same
844 shortcut for ``raw_id[:12]`` for git/mercurial backends or same
845 as ``raw_id`` for subversion
845 as ``raw_id`` for subversion
846
846
847 ``idx``
847 ``idx``
848 commit index
848 commit index
849
849
850 ``files``
850 ``files``
851 list of ``FileNode`` (``Node`` with NodeKind.FILE) objects
851 list of ``FileNode`` (``Node`` with NodeKind.FILE) objects
852
852
853 ``dirs``
853 ``dirs``
854 list of ``DirNode`` (``Node`` with NodeKind.DIR) objects
854 list of ``DirNode`` (``Node`` with NodeKind.DIR) objects
855
855
856 ``nodes``
856 ``nodes``
857 combined list of ``Node`` objects
857 combined list of ``Node`` objects
858
858
859 ``author``
859 ``author``
860 author of the commit, as unicode
860 author of the commit, as unicode
861
861
862 ``message``
862 ``message``
863 message of the commit, as unicode
863 message of the commit, as unicode
864
864
865 ``parents``
865 ``parents``
866 list of parent commits
866 list of parent commits
867
867
868 """
868 """
869
869
870 branch = None
870 branch = None
871 """
871 """
872 Depending on the backend this should be set to the branch name of the
872 Depending on the backend this should be set to the branch name of the
873 commit. Backends not supporting branches on commits should leave this
873 commit. Backends not supporting branches on commits should leave this
874 value as ``None``.
874 value as ``None``.
875 """
875 """
876
876
877 _ARCHIVE_PREFIX_TEMPLATE = b'{repo_name}-{short_id}'
877 _ARCHIVE_PREFIX_TEMPLATE = b'{repo_name}-{short_id}'
878 """
878 """
879 This template is used to generate a default prefix for repository archives
879 This template is used to generate a default prefix for repository archives
880 if no prefix has been specified.
880 if no prefix has been specified.
881 """
881 """
882
882
883 def __str__(self):
883 def __str__(self):
884 return '<%s at %s:%s>' % (
884 return '<%s at %s:%s>' % (
885 self.__class__.__name__, self.idx, self.short_id)
885 self.__class__.__name__, self.idx, self.short_id)
886
886
887 def __repr__(self):
887 def __repr__(self):
888 return self.__str__()
888 return self.__str__()
889
889
890 def __unicode__(self):
890 def __unicode__(self):
891 return u'%s:%s' % (self.idx, self.short_id)
891 return u'%s:%s' % (self.idx, self.short_id)
892
892
893 def __eq__(self, other):
893 def __eq__(self, other):
894 same_instance = isinstance(other, self.__class__)
894 same_instance = isinstance(other, self.__class__)
895 return same_instance and self.raw_id == other.raw_id
895 return same_instance and self.raw_id == other.raw_id
896
896
897 def __json__(self):
897 def __json__(self):
898 parents = []
898 parents = []
899 try:
899 try:
900 for parent in self.parents:
900 for parent in self.parents:
901 parents.append({'raw_id': parent.raw_id})
901 parents.append({'raw_id': parent.raw_id})
902 except NotImplementedError:
902 except NotImplementedError:
903 # empty commit doesn't have parents implemented
903 # empty commit doesn't have parents implemented
904 pass
904 pass
905
905
906 return {
906 return {
907 'short_id': self.short_id,
907 'short_id': self.short_id,
908 'raw_id': self.raw_id,
908 'raw_id': self.raw_id,
909 'revision': self.idx,
909 'revision': self.idx,
910 'message': self.message,
910 'message': self.message,
911 'date': self.date,
911 'date': self.date,
912 'author': self.author,
912 'author': self.author,
913 'parents': parents,
913 'parents': parents,
914 'branch': self.branch
914 'branch': self.branch
915 }
915 }
916
916
917 def __getstate__(self):
917 def __getstate__(self):
918 d = self.__dict__.copy()
918 d = self.__dict__.copy()
919 d.pop('_remote', None)
919 d.pop('_remote', None)
920 d.pop('repository', None)
920 d.pop('repository', None)
921 return d
921 return d
922
922
923 def _get_refs(self):
923 def _get_refs(self):
924 return {
924 return {
925 'branches': [self.branch] if self.branch else [],
925 'branches': [self.branch] if self.branch else [],
926 'bookmarks': getattr(self, 'bookmarks', []),
926 'bookmarks': getattr(self, 'bookmarks', []),
927 'tags': self.tags
927 'tags': self.tags
928 }
928 }
929
929
930 @LazyProperty
930 @LazyProperty
931 def last(self):
931 def last(self):
932 """
932 """
933 ``True`` if this is last commit in repository, ``False``
933 ``True`` if this is last commit in repository, ``False``
934 otherwise; trying to access this attribute while there is no
934 otherwise; trying to access this attribute while there is no
935 commits would raise `EmptyRepositoryError`
935 commits would raise `EmptyRepositoryError`
936 """
936 """
937 if self.repository is None:
937 if self.repository is None:
938 raise CommitError("Cannot check if it's most recent commit")
938 raise CommitError("Cannot check if it's most recent commit")
939 return self.raw_id == self.repository.commit_ids[-1]
939 return self.raw_id == self.repository.commit_ids[-1]
940
940
941 @LazyProperty
941 @LazyProperty
942 def parents(self):
942 def parents(self):
943 """
943 """
944 Returns list of parent commits.
944 Returns list of parent commits.
945 """
945 """
946 raise NotImplementedError
946 raise NotImplementedError
947
947
948 @LazyProperty
948 @LazyProperty
949 def first_parent(self):
949 def first_parent(self):
950 """
950 """
951 Returns list of parent commits.
951 Returns list of parent commits.
952 """
952 """
953 return self.parents[0] if self.parents else EmptyCommit()
953 return self.parents[0] if self.parents else EmptyCommit()
954
954
955 @property
955 @property
956 def merge(self):
956 def merge(self):
957 """
957 """
958 Returns boolean if commit is a merge.
958 Returns boolean if commit is a merge.
959 """
959 """
960 return len(self.parents) > 1
960 return len(self.parents) > 1
961
961
962 @LazyProperty
962 @LazyProperty
963 def children(self):
963 def children(self):
964 """
964 """
965 Returns list of child commits.
965 Returns list of child commits.
966 """
966 """
967 raise NotImplementedError
967 raise NotImplementedError
968
968
969 @LazyProperty
969 @LazyProperty
970 def id(self):
970 def id(self):
971 """
971 """
972 Returns string identifying this commit.
972 Returns string identifying this commit.
973 """
973 """
974 raise NotImplementedError
974 raise NotImplementedError
975
975
976 @LazyProperty
976 @LazyProperty
977 def raw_id(self):
977 def raw_id(self):
978 """
978 """
979 Returns raw string identifying this commit.
979 Returns raw string identifying this commit.
980 """
980 """
981 raise NotImplementedError
981 raise NotImplementedError
982
982
983 @LazyProperty
983 @LazyProperty
984 def short_id(self):
984 def short_id(self):
985 """
985 """
986 Returns shortened version of ``raw_id`` attribute, as string,
986 Returns shortened version of ``raw_id`` attribute, as string,
987 identifying this commit, useful for presentation to users.
987 identifying this commit, useful for presentation to users.
988 """
988 """
989 raise NotImplementedError
989 raise NotImplementedError
990
990
991 @LazyProperty
991 @LazyProperty
992 def idx(self):
992 def idx(self):
993 """
993 """
994 Returns integer identifying this commit.
994 Returns integer identifying this commit.
995 """
995 """
996 raise NotImplementedError
996 raise NotImplementedError
997
997
998 @LazyProperty
998 @LazyProperty
999 def committer(self):
999 def committer(self):
1000 """
1000 """
1001 Returns committer for this commit
1001 Returns committer for this commit
1002 """
1002 """
1003 raise NotImplementedError
1003 raise NotImplementedError
1004
1004
1005 @LazyProperty
1005 @LazyProperty
1006 def committer_name(self):
1006 def committer_name(self):
1007 """
1007 """
1008 Returns committer name for this commit
1008 Returns committer name for this commit
1009 """
1009 """
1010
1010
1011 return author_name(self.committer)
1011 return author_name(self.committer)
1012
1012
1013 @LazyProperty
1013 @LazyProperty
1014 def committer_email(self):
1014 def committer_email(self):
1015 """
1015 """
1016 Returns committer email address for this commit
1016 Returns committer email address for this commit
1017 """
1017 """
1018
1018
1019 return author_email(self.committer)
1019 return author_email(self.committer)
1020
1020
1021 @LazyProperty
1021 @LazyProperty
1022 def author(self):
1022 def author(self):
1023 """
1023 """
1024 Returns author for this commit
1024 Returns author for this commit
1025 """
1025 """
1026
1026
1027 raise NotImplementedError
1027 raise NotImplementedError
1028
1028
1029 @LazyProperty
1029 @LazyProperty
1030 def author_name(self):
1030 def author_name(self):
1031 """
1031 """
1032 Returns author name for this commit
1032 Returns author name for this commit
1033 """
1033 """
1034
1034
1035 return author_name(self.author)
1035 return author_name(self.author)
1036
1036
1037 @LazyProperty
1037 @LazyProperty
1038 def author_email(self):
1038 def author_email(self):
1039 """
1039 """
1040 Returns author email address for this commit
1040 Returns author email address for this commit
1041 """
1041 """
1042
1042
1043 return author_email(self.author)
1043 return author_email(self.author)
1044
1044
1045 def get_file_mode(self, path):
1045 def get_file_mode(self, path):
1046 """
1046 """
1047 Returns stat mode of the file at `path`.
1047 Returns stat mode of the file at `path`.
1048 """
1048 """
1049 raise NotImplementedError
1049 raise NotImplementedError
1050
1050
1051 def is_link(self, path):
1051 def is_link(self, path):
1052 """
1052 """
1053 Returns ``True`` if given `path` is a symlink
1053 Returns ``True`` if given `path` is a symlink
1054 """
1054 """
1055 raise NotImplementedError
1055 raise NotImplementedError
1056
1056
1057 def is_node_binary(self, path):
1058 """
1059 Returns ``True`` is given path is a binary file
1060 """
1061 raise NotImplementedError
1062
1057 def get_file_content(self, path):
1063 def get_file_content(self, path):
1058 """
1064 """
1059 Returns content of the file at the given `path`.
1065 Returns content of the file at the given `path`.
1060 """
1066 """
1061 raise NotImplementedError
1067 raise NotImplementedError
1062
1068
1063 def get_file_content_streamed(self, path):
1069 def get_file_content_streamed(self, path):
1064 """
1070 """
1065 returns a streaming response from vcsserver with file content
1071 returns a streaming response from vcsserver with file content
1066 """
1072 """
1067 raise NotImplementedError
1073 raise NotImplementedError
1068
1074
1069 def get_file_size(self, path):
1075 def get_file_size(self, path):
1070 """
1076 """
1071 Returns size of the file at the given `path`.
1077 Returns size of the file at the given `path`.
1072 """
1078 """
1073 raise NotImplementedError
1079 raise NotImplementedError
1074
1080
1075 def get_path_commit(self, path, pre_load=None):
1081 def get_path_commit(self, path, pre_load=None):
1076 """
1082 """
1077 Returns last commit of the file at the given `path`.
1083 Returns last commit of the file at the given `path`.
1078
1084
1079 :param pre_load: Optional. List of commit attributes to load.
1085 :param pre_load: Optional. List of commit attributes to load.
1080 """
1086 """
1081 commits = self.get_path_history(path, limit=1, pre_load=pre_load)
1087 commits = self.get_path_history(path, limit=1, pre_load=pre_load)
1082 if not commits:
1088 if not commits:
1083 raise RepositoryError(
1089 raise RepositoryError(
1084 'Failed to fetch history for path {}. '
1090 'Failed to fetch history for path {}. '
1085 'Please check if such path exists in your repository'.format(
1091 'Please check if such path exists in your repository'.format(
1086 path))
1092 path))
1087 return commits[0]
1093 return commits[0]
1088
1094
1089 def get_path_history(self, path, limit=None, pre_load=None):
1095 def get_path_history(self, path, limit=None, pre_load=None):
1090 """
1096 """
1091 Returns history of file as reversed list of :class:`BaseCommit`
1097 Returns history of file as reversed list of :class:`BaseCommit`
1092 objects for which file at given `path` has been modified.
1098 objects for which file at given `path` has been modified.
1093
1099
1094 :param limit: Optional. Allows to limit the size of the returned
1100 :param limit: Optional. Allows to limit the size of the returned
1095 history. This is intended as a hint to the underlying backend, so
1101 history. This is intended as a hint to the underlying backend, so
1096 that it can apply optimizations depending on the limit.
1102 that it can apply optimizations depending on the limit.
1097 :param pre_load: Optional. List of commit attributes to load.
1103 :param pre_load: Optional. List of commit attributes to load.
1098 """
1104 """
1099 raise NotImplementedError
1105 raise NotImplementedError
1100
1106
1101 def get_file_annotate(self, path, pre_load=None):
1107 def get_file_annotate(self, path, pre_load=None):
1102 """
1108 """
1103 Returns a generator of four element tuples with
1109 Returns a generator of four element tuples with
1104 lineno, sha, commit lazy loader and line
1110 lineno, sha, commit lazy loader and line
1105
1111
1106 :param pre_load: Optional. List of commit attributes to load.
1112 :param pre_load: Optional. List of commit attributes to load.
1107 """
1113 """
1108 raise NotImplementedError
1114 raise NotImplementedError
1109
1115
1110 def get_nodes(self, path):
1116 def get_nodes(self, path):
1111 """
1117 """
1112 Returns combined ``DirNode`` and ``FileNode`` objects list representing
1118 Returns combined ``DirNode`` and ``FileNode`` objects list representing
1113 state of commit at the given ``path``.
1119 state of commit at the given ``path``.
1114
1120
1115 :raises ``CommitError``: if node at the given ``path`` is not
1121 :raises ``CommitError``: if node at the given ``path`` is not
1116 instance of ``DirNode``
1122 instance of ``DirNode``
1117 """
1123 """
1118 raise NotImplementedError
1124 raise NotImplementedError
1119
1125
1120 def get_node(self, path):
1126 def get_node(self, path):
1121 """
1127 """
1122 Returns ``Node`` object from the given ``path``.
1128 Returns ``Node`` object from the given ``path``.
1123
1129
1124 :raises ``NodeDoesNotExistError``: if there is no node at the given
1130 :raises ``NodeDoesNotExistError``: if there is no node at the given
1125 ``path``
1131 ``path``
1126 """
1132 """
1127 raise NotImplementedError
1133 raise NotImplementedError
1128
1134
1129 def get_largefile_node(self, path):
1135 def get_largefile_node(self, path):
1130 """
1136 """
1131 Returns the path to largefile from Mercurial/Git-lfs storage.
1137 Returns the path to largefile from Mercurial/Git-lfs storage.
1132 or None if it's not a largefile node
1138 or None if it's not a largefile node
1133 """
1139 """
1134 return None
1140 return None
1135
1141
1136 def archive_repo(self, archive_dest_path, kind='tgz', subrepos=None,
1142 def archive_repo(self, archive_dest_path, kind='tgz', subrepos=None,
1137 prefix=None, write_metadata=False, mtime=None, archive_at_path='/'):
1143 prefix=None, write_metadata=False, mtime=None, archive_at_path='/'):
1138 """
1144 """
1139 Creates an archive containing the contents of the repository.
1145 Creates an archive containing the contents of the repository.
1140
1146
1141 :param archive_dest_path: path to the file which to create the archive.
1147 :param archive_dest_path: path to the file which to create the archive.
1142 :param kind: one of following: ``"tbz2"``, ``"tgz"``, ``"zip"``.
1148 :param kind: one of following: ``"tbz2"``, ``"tgz"``, ``"zip"``.
1143 :param prefix: name of root directory in archive.
1149 :param prefix: name of root directory in archive.
1144 Default is repository name and commit's short_id joined with dash:
1150 Default is repository name and commit's short_id joined with dash:
1145 ``"{repo_name}-{short_id}"``.
1151 ``"{repo_name}-{short_id}"``.
1146 :param write_metadata: write a metadata file into archive.
1152 :param write_metadata: write a metadata file into archive.
1147 :param mtime: custom modification time for archive creation, defaults
1153 :param mtime: custom modification time for archive creation, defaults
1148 to time.time() if not given.
1154 to time.time() if not given.
1149 :param archive_at_path: pack files at this path (default '/')
1155 :param archive_at_path: pack files at this path (default '/')
1150
1156
1151 :raise VCSError: If prefix has a problem.
1157 :raise VCSError: If prefix has a problem.
1152 """
1158 """
1153 allowed_kinds = [x[0] for x in settings.ARCHIVE_SPECS]
1159 allowed_kinds = [x[0] for x in settings.ARCHIVE_SPECS]
1154 if kind not in allowed_kinds:
1160 if kind not in allowed_kinds:
1155 raise ImproperArchiveTypeError(
1161 raise ImproperArchiveTypeError(
1156 'Archive kind (%s) not supported use one of %s' %
1162 'Archive kind (%s) not supported use one of %s' %
1157 (kind, allowed_kinds))
1163 (kind, allowed_kinds))
1158
1164
1159 prefix = self._validate_archive_prefix(prefix)
1165 prefix = self._validate_archive_prefix(prefix)
1160
1166
1161 mtime = mtime is not None or time.mktime(self.date.timetuple())
1167 mtime = mtime is not None or time.mktime(self.date.timetuple())
1162
1168
1163 file_info = []
1169 file_info = []
1164 cur_rev = self.repository.get_commit(commit_id=self.raw_id)
1170 cur_rev = self.repository.get_commit(commit_id=self.raw_id)
1165 for _r, _d, files in cur_rev.walk(archive_at_path):
1171 for _r, _d, files in cur_rev.walk(archive_at_path):
1166 for f in files:
1172 for f in files:
1167 f_path = os.path.join(prefix, f.path)
1173 f_path = os.path.join(prefix, f.path)
1168 file_info.append(
1174 file_info.append(
1169 (f_path, f.mode, f.is_link(), f.raw_bytes))
1175 (f_path, f.mode, f.is_link(), f.raw_bytes))
1170
1176
1171 if write_metadata:
1177 if write_metadata:
1172 metadata = [
1178 metadata = [
1173 ('repo_name', self.repository.name),
1179 ('repo_name', self.repository.name),
1174 ('commit_id', self.raw_id),
1180 ('commit_id', self.raw_id),
1175 ('mtime', mtime),
1181 ('mtime', mtime),
1176 ('branch', self.branch),
1182 ('branch', self.branch),
1177 ('tags', ','.join(self.tags)),
1183 ('tags', ','.join(self.tags)),
1178 ]
1184 ]
1179 meta = ["%s:%s" % (f_name, value) for f_name, value in metadata]
1185 meta = ["%s:%s" % (f_name, value) for f_name, value in metadata]
1180 file_info.append(('.archival.txt', 0o644, False, '\n'.join(meta)))
1186 file_info.append(('.archival.txt', 0o644, False, '\n'.join(meta)))
1181
1187
1182 connection.Hg.archive_repo(archive_dest_path, mtime, file_info, kind)
1188 connection.Hg.archive_repo(archive_dest_path, mtime, file_info, kind)
1183
1189
1184 def _validate_archive_prefix(self, prefix):
1190 def _validate_archive_prefix(self, prefix):
1185 if prefix is None:
1191 if prefix is None:
1186 prefix = self._ARCHIVE_PREFIX_TEMPLATE.format(
1192 prefix = self._ARCHIVE_PREFIX_TEMPLATE.format(
1187 repo_name=safe_str(self.repository.name),
1193 repo_name=safe_str(self.repository.name),
1188 short_id=self.short_id)
1194 short_id=self.short_id)
1189 elif not isinstance(prefix, str):
1195 elif not isinstance(prefix, str):
1190 raise ValueError("prefix not a bytes object: %s" % repr(prefix))
1196 raise ValueError("prefix not a bytes object: %s" % repr(prefix))
1191 elif prefix.startswith('/'):
1197 elif prefix.startswith('/'):
1192 raise VCSError("Prefix cannot start with leading slash")
1198 raise VCSError("Prefix cannot start with leading slash")
1193 elif prefix.strip() == '':
1199 elif prefix.strip() == '':
1194 raise VCSError("Prefix cannot be empty")
1200 raise VCSError("Prefix cannot be empty")
1195 return prefix
1201 return prefix
1196
1202
1197 @LazyProperty
1203 @LazyProperty
1198 def root(self):
1204 def root(self):
1199 """
1205 """
1200 Returns ``RootNode`` object for this commit.
1206 Returns ``RootNode`` object for this commit.
1201 """
1207 """
1202 return self.get_node('')
1208 return self.get_node('')
1203
1209
1204 def next(self, branch=None):
1210 def next(self, branch=None):
1205 """
1211 """
1206 Returns next commit from current, if branch is gives it will return
1212 Returns next commit from current, if branch is gives it will return
1207 next commit belonging to this branch
1213 next commit belonging to this branch
1208
1214
1209 :param branch: show commits within the given named branch
1215 :param branch: show commits within the given named branch
1210 """
1216 """
1211 indexes = xrange(self.idx + 1, self.repository.count())
1217 indexes = xrange(self.idx + 1, self.repository.count())
1212 return self._find_next(indexes, branch)
1218 return self._find_next(indexes, branch)
1213
1219
1214 def prev(self, branch=None):
1220 def prev(self, branch=None):
1215 """
1221 """
1216 Returns previous commit from current, if branch is gives it will
1222 Returns previous commit from current, if branch is gives it will
1217 return previous commit belonging to this branch
1223 return previous commit belonging to this branch
1218
1224
1219 :param branch: show commit within the given named branch
1225 :param branch: show commit within the given named branch
1220 """
1226 """
1221 indexes = xrange(self.idx - 1, -1, -1)
1227 indexes = xrange(self.idx - 1, -1, -1)
1222 return self._find_next(indexes, branch)
1228 return self._find_next(indexes, branch)
1223
1229
1224 def _find_next(self, indexes, branch=None):
1230 def _find_next(self, indexes, branch=None):
1225 if branch and self.branch != branch:
1231 if branch and self.branch != branch:
1226 raise VCSError('Branch option used on commit not belonging '
1232 raise VCSError('Branch option used on commit not belonging '
1227 'to that branch')
1233 'to that branch')
1228
1234
1229 for next_idx in indexes:
1235 for next_idx in indexes:
1230 commit = self.repository.get_commit(commit_idx=next_idx)
1236 commit = self.repository.get_commit(commit_idx=next_idx)
1231 if branch and branch != commit.branch:
1237 if branch and branch != commit.branch:
1232 continue
1238 continue
1233 return commit
1239 return commit
1234 raise CommitDoesNotExistError
1240 raise CommitDoesNotExistError
1235
1241
1236 def diff(self, ignore_whitespace=True, context=3):
1242 def diff(self, ignore_whitespace=True, context=3):
1237 """
1243 """
1238 Returns a `Diff` object representing the change made by this commit.
1244 Returns a `Diff` object representing the change made by this commit.
1239 """
1245 """
1240 parent = self.first_parent
1246 parent = self.first_parent
1241 diff = self.repository.get_diff(
1247 diff = self.repository.get_diff(
1242 parent, self,
1248 parent, self,
1243 ignore_whitespace=ignore_whitespace,
1249 ignore_whitespace=ignore_whitespace,
1244 context=context)
1250 context=context)
1245 return diff
1251 return diff
1246
1252
1247 @LazyProperty
1253 @LazyProperty
1248 def added(self):
1254 def added(self):
1249 """
1255 """
1250 Returns list of added ``FileNode`` objects.
1256 Returns list of added ``FileNode`` objects.
1251 """
1257 """
1252 raise NotImplementedError
1258 raise NotImplementedError
1253
1259
1254 @LazyProperty
1260 @LazyProperty
1255 def changed(self):
1261 def changed(self):
1256 """
1262 """
1257 Returns list of modified ``FileNode`` objects.
1263 Returns list of modified ``FileNode`` objects.
1258 """
1264 """
1259 raise NotImplementedError
1265 raise NotImplementedError
1260
1266
1261 @LazyProperty
1267 @LazyProperty
1262 def removed(self):
1268 def removed(self):
1263 """
1269 """
1264 Returns list of removed ``FileNode`` objects.
1270 Returns list of removed ``FileNode`` objects.
1265 """
1271 """
1266 raise NotImplementedError
1272 raise NotImplementedError
1267
1273
1268 @LazyProperty
1274 @LazyProperty
1269 def size(self):
1275 def size(self):
1270 """
1276 """
1271 Returns total number of bytes from contents of all filenodes.
1277 Returns total number of bytes from contents of all filenodes.
1272 """
1278 """
1273 return sum((node.size for node in self.get_filenodes_generator()))
1279 return sum((node.size for node in self.get_filenodes_generator()))
1274
1280
1275 def walk(self, topurl=''):
1281 def walk(self, topurl=''):
1276 """
1282 """
1277 Similar to os.walk method. Insted of filesystem it walks through
1283 Similar to os.walk method. Insted of filesystem it walks through
1278 commit starting at given ``topurl``. Returns generator of tuples
1284 commit starting at given ``topurl``. Returns generator of tuples
1279 (topnode, dirnodes, filenodes).
1285 (topnode, dirnodes, filenodes).
1280 """
1286 """
1281 topnode = self.get_node(topurl)
1287 topnode = self.get_node(topurl)
1282 if not topnode.is_dir():
1288 if not topnode.is_dir():
1283 return
1289 return
1284 yield (topnode, topnode.dirs, topnode.files)
1290 yield (topnode, topnode.dirs, topnode.files)
1285 for dirnode in topnode.dirs:
1291 for dirnode in topnode.dirs:
1286 for tup in self.walk(dirnode.path):
1292 for tup in self.walk(dirnode.path):
1287 yield tup
1293 yield tup
1288
1294
1289 def get_filenodes_generator(self):
1295 def get_filenodes_generator(self):
1290 """
1296 """
1291 Returns generator that yields *all* file nodes.
1297 Returns generator that yields *all* file nodes.
1292 """
1298 """
1293 for topnode, dirs, files in self.walk():
1299 for topnode, dirs, files in self.walk():
1294 for node in files:
1300 for node in files:
1295 yield node
1301 yield node
1296
1302
1297 #
1303 #
1298 # Utilities for sub classes to support consistent behavior
1304 # Utilities for sub classes to support consistent behavior
1299 #
1305 #
1300
1306
1301 def no_node_at_path(self, path):
1307 def no_node_at_path(self, path):
1302 return NodeDoesNotExistError(
1308 return NodeDoesNotExistError(
1303 u"There is no file nor directory at the given path: "
1309 u"There is no file nor directory at the given path: "
1304 u"`%s` at commit %s" % (safe_unicode(path), self.short_id))
1310 u"`%s` at commit %s" % (safe_unicode(path), self.short_id))
1305
1311
1306 def _fix_path(self, path):
1312 def _fix_path(self, path):
1307 """
1313 """
1308 Paths are stored without trailing slash so we need to get rid off it if
1314 Paths are stored without trailing slash so we need to get rid off it if
1309 needed.
1315 needed.
1310 """
1316 """
1311 return path.rstrip('/')
1317 return path.rstrip('/')
1312
1318
1313 #
1319 #
1314 # Deprecated API based on changesets
1320 # Deprecated API based on changesets
1315 #
1321 #
1316
1322
1317 @property
1323 @property
1318 def revision(self):
1324 def revision(self):
1319 warnings.warn("Use idx instead", DeprecationWarning)
1325 warnings.warn("Use idx instead", DeprecationWarning)
1320 return self.idx
1326 return self.idx
1321
1327
1322 @revision.setter
1328 @revision.setter
1323 def revision(self, value):
1329 def revision(self, value):
1324 warnings.warn("Use idx instead", DeprecationWarning)
1330 warnings.warn("Use idx instead", DeprecationWarning)
1325 self.idx = value
1331 self.idx = value
1326
1332
1327 def get_file_changeset(self, path):
1333 def get_file_changeset(self, path):
1328 warnings.warn("Use get_path_commit instead", DeprecationWarning)
1334 warnings.warn("Use get_path_commit instead", DeprecationWarning)
1329 return self.get_path_commit(path)
1335 return self.get_path_commit(path)
1330
1336
1331
1337
1332 class BaseChangesetClass(type):
1338 class BaseChangesetClass(type):
1333
1339
1334 def __instancecheck__(self, instance):
1340 def __instancecheck__(self, instance):
1335 return isinstance(instance, BaseCommit)
1341 return isinstance(instance, BaseCommit)
1336
1342
1337
1343
1338 class BaseChangeset(BaseCommit):
1344 class BaseChangeset(BaseCommit):
1339
1345
1340 __metaclass__ = BaseChangesetClass
1346 __metaclass__ = BaseChangesetClass
1341
1347
1342 def __new__(cls, *args, **kwargs):
1348 def __new__(cls, *args, **kwargs):
1343 warnings.warn(
1349 warnings.warn(
1344 "Use BaseCommit instead of BaseChangeset", DeprecationWarning)
1350 "Use BaseCommit instead of BaseChangeset", DeprecationWarning)
1345 return super(BaseChangeset, cls).__new__(cls, *args, **kwargs)
1351 return super(BaseChangeset, cls).__new__(cls, *args, **kwargs)
1346
1352
1347
1353
1348 class BaseInMemoryCommit(object):
1354 class BaseInMemoryCommit(object):
1349 """
1355 """
1350 Represents differences between repository's state (most recent head) and
1356 Represents differences between repository's state (most recent head) and
1351 changes made *in place*.
1357 changes made *in place*.
1352
1358
1353 **Attributes**
1359 **Attributes**
1354
1360
1355 ``repository``
1361 ``repository``
1356 repository object for this in-memory-commit
1362 repository object for this in-memory-commit
1357
1363
1358 ``added``
1364 ``added``
1359 list of ``FileNode`` objects marked as *added*
1365 list of ``FileNode`` objects marked as *added*
1360
1366
1361 ``changed``
1367 ``changed``
1362 list of ``FileNode`` objects marked as *changed*
1368 list of ``FileNode`` objects marked as *changed*
1363
1369
1364 ``removed``
1370 ``removed``
1365 list of ``FileNode`` or ``RemovedFileNode`` objects marked to be
1371 list of ``FileNode`` or ``RemovedFileNode`` objects marked to be
1366 *removed*
1372 *removed*
1367
1373
1368 ``parents``
1374 ``parents``
1369 list of :class:`BaseCommit` instances representing parents of
1375 list of :class:`BaseCommit` instances representing parents of
1370 in-memory commit. Should always be 2-element sequence.
1376 in-memory commit. Should always be 2-element sequence.
1371
1377
1372 """
1378 """
1373
1379
1374 def __init__(self, repository):
1380 def __init__(self, repository):
1375 self.repository = repository
1381 self.repository = repository
1376 self.added = []
1382 self.added = []
1377 self.changed = []
1383 self.changed = []
1378 self.removed = []
1384 self.removed = []
1379 self.parents = []
1385 self.parents = []
1380
1386
1381 def add(self, *filenodes):
1387 def add(self, *filenodes):
1382 """
1388 """
1383 Marks given ``FileNode`` objects as *to be committed*.
1389 Marks given ``FileNode`` objects as *to be committed*.
1384
1390
1385 :raises ``NodeAlreadyExistsError``: if node with same path exists at
1391 :raises ``NodeAlreadyExistsError``: if node with same path exists at
1386 latest commit
1392 latest commit
1387 :raises ``NodeAlreadyAddedError``: if node with same path is already
1393 :raises ``NodeAlreadyAddedError``: if node with same path is already
1388 marked as *added*
1394 marked as *added*
1389 """
1395 """
1390 # Check if not already marked as *added* first
1396 # Check if not already marked as *added* first
1391 for node in filenodes:
1397 for node in filenodes:
1392 if node.path in (n.path for n in self.added):
1398 if node.path in (n.path for n in self.added):
1393 raise NodeAlreadyAddedError(
1399 raise NodeAlreadyAddedError(
1394 "Such FileNode %s is already marked for addition"
1400 "Such FileNode %s is already marked for addition"
1395 % node.path)
1401 % node.path)
1396 for node in filenodes:
1402 for node in filenodes:
1397 self.added.append(node)
1403 self.added.append(node)
1398
1404
1399 def change(self, *filenodes):
1405 def change(self, *filenodes):
1400 """
1406 """
1401 Marks given ``FileNode`` objects to be *changed* in next commit.
1407 Marks given ``FileNode`` objects to be *changed* in next commit.
1402
1408
1403 :raises ``EmptyRepositoryError``: if there are no commits yet
1409 :raises ``EmptyRepositoryError``: if there are no commits yet
1404 :raises ``NodeAlreadyExistsError``: if node with same path is already
1410 :raises ``NodeAlreadyExistsError``: if node with same path is already
1405 marked to be *changed*
1411 marked to be *changed*
1406 :raises ``NodeAlreadyRemovedError``: if node with same path is already
1412 :raises ``NodeAlreadyRemovedError``: if node with same path is already
1407 marked to be *removed*
1413 marked to be *removed*
1408 :raises ``NodeDoesNotExistError``: if node doesn't exist in latest
1414 :raises ``NodeDoesNotExistError``: if node doesn't exist in latest
1409 commit
1415 commit
1410 :raises ``NodeNotChangedError``: if node hasn't really be changed
1416 :raises ``NodeNotChangedError``: if node hasn't really be changed
1411 """
1417 """
1412 for node in filenodes:
1418 for node in filenodes:
1413 if node.path in (n.path for n in self.removed):
1419 if node.path in (n.path for n in self.removed):
1414 raise NodeAlreadyRemovedError(
1420 raise NodeAlreadyRemovedError(
1415 "Node at %s is already marked as removed" % node.path)
1421 "Node at %s is already marked as removed" % node.path)
1416 try:
1422 try:
1417 self.repository.get_commit()
1423 self.repository.get_commit()
1418 except EmptyRepositoryError:
1424 except EmptyRepositoryError:
1419 raise EmptyRepositoryError(
1425 raise EmptyRepositoryError(
1420 "Nothing to change - try to *add* new nodes rather than "
1426 "Nothing to change - try to *add* new nodes rather than "
1421 "changing them")
1427 "changing them")
1422 for node in filenodes:
1428 for node in filenodes:
1423 if node.path in (n.path for n in self.changed):
1429 if node.path in (n.path for n in self.changed):
1424 raise NodeAlreadyChangedError(
1430 raise NodeAlreadyChangedError(
1425 "Node at '%s' is already marked as changed" % node.path)
1431 "Node at '%s' is already marked as changed" % node.path)
1426 self.changed.append(node)
1432 self.changed.append(node)
1427
1433
1428 def remove(self, *filenodes):
1434 def remove(self, *filenodes):
1429 """
1435 """
1430 Marks given ``FileNode`` (or ``RemovedFileNode``) objects to be
1436 Marks given ``FileNode`` (or ``RemovedFileNode``) objects to be
1431 *removed* in next commit.
1437 *removed* in next commit.
1432
1438
1433 :raises ``NodeAlreadyRemovedError``: if node has been already marked to
1439 :raises ``NodeAlreadyRemovedError``: if node has been already marked to
1434 be *removed*
1440 be *removed*
1435 :raises ``NodeAlreadyChangedError``: if node has been already marked to
1441 :raises ``NodeAlreadyChangedError``: if node has been already marked to
1436 be *changed*
1442 be *changed*
1437 """
1443 """
1438 for node in filenodes:
1444 for node in filenodes:
1439 if node.path in (n.path for n in self.removed):
1445 if node.path in (n.path for n in self.removed):
1440 raise NodeAlreadyRemovedError(
1446 raise NodeAlreadyRemovedError(
1441 "Node is already marked to for removal at %s" % node.path)
1447 "Node is already marked to for removal at %s" % node.path)
1442 if node.path in (n.path for n in self.changed):
1448 if node.path in (n.path for n in self.changed):
1443 raise NodeAlreadyChangedError(
1449 raise NodeAlreadyChangedError(
1444 "Node is already marked to be changed at %s" % node.path)
1450 "Node is already marked to be changed at %s" % node.path)
1445 # We only mark node as *removed* - real removal is done by
1451 # We only mark node as *removed* - real removal is done by
1446 # commit method
1452 # commit method
1447 self.removed.append(node)
1453 self.removed.append(node)
1448
1454
1449 def reset(self):
1455 def reset(self):
1450 """
1456 """
1451 Resets this instance to initial state (cleans ``added``, ``changed``
1457 Resets this instance to initial state (cleans ``added``, ``changed``
1452 and ``removed`` lists).
1458 and ``removed`` lists).
1453 """
1459 """
1454 self.added = []
1460 self.added = []
1455 self.changed = []
1461 self.changed = []
1456 self.removed = []
1462 self.removed = []
1457 self.parents = []
1463 self.parents = []
1458
1464
1459 def get_ipaths(self):
1465 def get_ipaths(self):
1460 """
1466 """
1461 Returns generator of paths from nodes marked as added, changed or
1467 Returns generator of paths from nodes marked as added, changed or
1462 removed.
1468 removed.
1463 """
1469 """
1464 for node in itertools.chain(self.added, self.changed, self.removed):
1470 for node in itertools.chain(self.added, self.changed, self.removed):
1465 yield node.path
1471 yield node.path
1466
1472
1467 def get_paths(self):
1473 def get_paths(self):
1468 """
1474 """
1469 Returns list of paths from nodes marked as added, changed or removed.
1475 Returns list of paths from nodes marked as added, changed or removed.
1470 """
1476 """
1471 return list(self.get_ipaths())
1477 return list(self.get_ipaths())
1472
1478
1473 def check_integrity(self, parents=None):
1479 def check_integrity(self, parents=None):
1474 """
1480 """
1475 Checks in-memory commit's integrity. Also, sets parents if not
1481 Checks in-memory commit's integrity. Also, sets parents if not
1476 already set.
1482 already set.
1477
1483
1478 :raises CommitError: if any error occurs (i.e.
1484 :raises CommitError: if any error occurs (i.e.
1479 ``NodeDoesNotExistError``).
1485 ``NodeDoesNotExistError``).
1480 """
1486 """
1481 if not self.parents:
1487 if not self.parents:
1482 parents = parents or []
1488 parents = parents or []
1483 if len(parents) == 0:
1489 if len(parents) == 0:
1484 try:
1490 try:
1485 parents = [self.repository.get_commit(), None]
1491 parents = [self.repository.get_commit(), None]
1486 except EmptyRepositoryError:
1492 except EmptyRepositoryError:
1487 parents = [None, None]
1493 parents = [None, None]
1488 elif len(parents) == 1:
1494 elif len(parents) == 1:
1489 parents += [None]
1495 parents += [None]
1490 self.parents = parents
1496 self.parents = parents
1491
1497
1492 # Local parents, only if not None
1498 # Local parents, only if not None
1493 parents = [p for p in self.parents if p]
1499 parents = [p for p in self.parents if p]
1494
1500
1495 # Check nodes marked as added
1501 # Check nodes marked as added
1496 for p in parents:
1502 for p in parents:
1497 for node in self.added:
1503 for node in self.added:
1498 try:
1504 try:
1499 p.get_node(node.path)
1505 p.get_node(node.path)
1500 except NodeDoesNotExistError:
1506 except NodeDoesNotExistError:
1501 pass
1507 pass
1502 else:
1508 else:
1503 raise NodeAlreadyExistsError(
1509 raise NodeAlreadyExistsError(
1504 "Node `%s` already exists at %s" % (node.path, p))
1510 "Node `%s` already exists at %s" % (node.path, p))
1505
1511
1506 # Check nodes marked as changed
1512 # Check nodes marked as changed
1507 missing = set(self.changed)
1513 missing = set(self.changed)
1508 not_changed = set(self.changed)
1514 not_changed = set(self.changed)
1509 if self.changed and not parents:
1515 if self.changed and not parents:
1510 raise NodeDoesNotExistError(str(self.changed[0].path))
1516 raise NodeDoesNotExistError(str(self.changed[0].path))
1511 for p in parents:
1517 for p in parents:
1512 for node in self.changed:
1518 for node in self.changed:
1513 try:
1519 try:
1514 old = p.get_node(node.path)
1520 old = p.get_node(node.path)
1515 missing.remove(node)
1521 missing.remove(node)
1516 # if content actually changed, remove node from not_changed
1522 # if content actually changed, remove node from not_changed
1517 if old.content != node.content:
1523 if old.content != node.content:
1518 not_changed.remove(node)
1524 not_changed.remove(node)
1519 except NodeDoesNotExistError:
1525 except NodeDoesNotExistError:
1520 pass
1526 pass
1521 if self.changed and missing:
1527 if self.changed and missing:
1522 raise NodeDoesNotExistError(
1528 raise NodeDoesNotExistError(
1523 "Node `%s` marked as modified but missing in parents: %s"
1529 "Node `%s` marked as modified but missing in parents: %s"
1524 % (node.path, parents))
1530 % (node.path, parents))
1525
1531
1526 if self.changed and not_changed:
1532 if self.changed and not_changed:
1527 raise NodeNotChangedError(
1533 raise NodeNotChangedError(
1528 "Node `%s` wasn't actually changed (parents: %s)"
1534 "Node `%s` wasn't actually changed (parents: %s)"
1529 % (not_changed.pop().path, parents))
1535 % (not_changed.pop().path, parents))
1530
1536
1531 # Check nodes marked as removed
1537 # Check nodes marked as removed
1532 if self.removed and not parents:
1538 if self.removed and not parents:
1533 raise NodeDoesNotExistError(
1539 raise NodeDoesNotExistError(
1534 "Cannot remove node at %s as there "
1540 "Cannot remove node at %s as there "
1535 "were no parents specified" % self.removed[0].path)
1541 "were no parents specified" % self.removed[0].path)
1536 really_removed = set()
1542 really_removed = set()
1537 for p in parents:
1543 for p in parents:
1538 for node in self.removed:
1544 for node in self.removed:
1539 try:
1545 try:
1540 p.get_node(node.path)
1546 p.get_node(node.path)
1541 really_removed.add(node)
1547 really_removed.add(node)
1542 except CommitError:
1548 except CommitError:
1543 pass
1549 pass
1544 not_removed = set(self.removed) - really_removed
1550 not_removed = set(self.removed) - really_removed
1545 if not_removed:
1551 if not_removed:
1546 # TODO: johbo: This code branch does not seem to be covered
1552 # TODO: johbo: This code branch does not seem to be covered
1547 raise NodeDoesNotExistError(
1553 raise NodeDoesNotExistError(
1548 "Cannot remove node at %s from "
1554 "Cannot remove node at %s from "
1549 "following parents: %s" % (not_removed, parents))
1555 "following parents: %s" % (not_removed, parents))
1550
1556
1551 def commit(self, message, author, parents=None, branch=None, date=None, **kwargs):
1557 def commit(self, message, author, parents=None, branch=None, date=None, **kwargs):
1552 """
1558 """
1553 Performs in-memory commit (doesn't check workdir in any way) and
1559 Performs in-memory commit (doesn't check workdir in any way) and
1554 returns newly created :class:`BaseCommit`. Updates repository's
1560 returns newly created :class:`BaseCommit`. Updates repository's
1555 attribute `commits`.
1561 attribute `commits`.
1556
1562
1557 .. note::
1563 .. note::
1558
1564
1559 While overriding this method each backend's should call
1565 While overriding this method each backend's should call
1560 ``self.check_integrity(parents)`` in the first place.
1566 ``self.check_integrity(parents)`` in the first place.
1561
1567
1562 :param message: message of the commit
1568 :param message: message of the commit
1563 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
1569 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
1564 :param parents: single parent or sequence of parents from which commit
1570 :param parents: single parent or sequence of parents from which commit
1565 would be derived
1571 would be derived
1566 :param date: ``datetime.datetime`` instance. Defaults to
1572 :param date: ``datetime.datetime`` instance. Defaults to
1567 ``datetime.datetime.now()``.
1573 ``datetime.datetime.now()``.
1568 :param branch: branch name, as string. If none given, default backend's
1574 :param branch: branch name, as string. If none given, default backend's
1569 branch would be used.
1575 branch would be used.
1570
1576
1571 :raises ``CommitError``: if any error occurs while committing
1577 :raises ``CommitError``: if any error occurs while committing
1572 """
1578 """
1573 raise NotImplementedError
1579 raise NotImplementedError
1574
1580
1575
1581
1576 class BaseInMemoryChangesetClass(type):
1582 class BaseInMemoryChangesetClass(type):
1577
1583
1578 def __instancecheck__(self, instance):
1584 def __instancecheck__(self, instance):
1579 return isinstance(instance, BaseInMemoryCommit)
1585 return isinstance(instance, BaseInMemoryCommit)
1580
1586
1581
1587
1582 class BaseInMemoryChangeset(BaseInMemoryCommit):
1588 class BaseInMemoryChangeset(BaseInMemoryCommit):
1583
1589
1584 __metaclass__ = BaseInMemoryChangesetClass
1590 __metaclass__ = BaseInMemoryChangesetClass
1585
1591
1586 def __new__(cls, *args, **kwargs):
1592 def __new__(cls, *args, **kwargs):
1587 warnings.warn(
1593 warnings.warn(
1588 "Use BaseCommit instead of BaseInMemoryCommit", DeprecationWarning)
1594 "Use BaseCommit instead of BaseInMemoryCommit", DeprecationWarning)
1589 return super(BaseInMemoryChangeset, cls).__new__(cls, *args, **kwargs)
1595 return super(BaseInMemoryChangeset, cls).__new__(cls, *args, **kwargs)
1590
1596
1591
1597
1592 class EmptyCommit(BaseCommit):
1598 class EmptyCommit(BaseCommit):
1593 """
1599 """
1594 An dummy empty commit. It's possible to pass hash when creating
1600 An dummy empty commit. It's possible to pass hash when creating
1595 an EmptyCommit
1601 an EmptyCommit
1596 """
1602 """
1597
1603
1598 def __init__(
1604 def __init__(
1599 self, commit_id=EMPTY_COMMIT_ID, repo=None, alias=None, idx=-1,
1605 self, commit_id=EMPTY_COMMIT_ID, repo=None, alias=None, idx=-1,
1600 message='', author='', date=None):
1606 message='', author='', date=None):
1601 self._empty_commit_id = commit_id
1607 self._empty_commit_id = commit_id
1602 # TODO: johbo: Solve idx parameter, default value does not make
1608 # TODO: johbo: Solve idx parameter, default value does not make
1603 # too much sense
1609 # too much sense
1604 self.idx = idx
1610 self.idx = idx
1605 self.message = message
1611 self.message = message
1606 self.author = author
1612 self.author = author
1607 self.date = date or datetime.datetime.fromtimestamp(0)
1613 self.date = date or datetime.datetime.fromtimestamp(0)
1608 self.repository = repo
1614 self.repository = repo
1609 self.alias = alias
1615 self.alias = alias
1610
1616
1611 @LazyProperty
1617 @LazyProperty
1612 def raw_id(self):
1618 def raw_id(self):
1613 """
1619 """
1614 Returns raw string identifying this commit, useful for web
1620 Returns raw string identifying this commit, useful for web
1615 representation.
1621 representation.
1616 """
1622 """
1617
1623
1618 return self._empty_commit_id
1624 return self._empty_commit_id
1619
1625
1620 @LazyProperty
1626 @LazyProperty
1621 def branch(self):
1627 def branch(self):
1622 if self.alias:
1628 if self.alias:
1623 from rhodecode.lib.vcs.backends import get_backend
1629 from rhodecode.lib.vcs.backends import get_backend
1624 return get_backend(self.alias).DEFAULT_BRANCH_NAME
1630 return get_backend(self.alias).DEFAULT_BRANCH_NAME
1625
1631
1626 @LazyProperty
1632 @LazyProperty
1627 def short_id(self):
1633 def short_id(self):
1628 return self.raw_id[:12]
1634 return self.raw_id[:12]
1629
1635
1630 @LazyProperty
1636 @LazyProperty
1631 def id(self):
1637 def id(self):
1632 return self.raw_id
1638 return self.raw_id
1633
1639
1634 def get_path_commit(self, path):
1640 def get_path_commit(self, path):
1635 return self
1641 return self
1636
1642
1637 def get_file_content(self, path):
1643 def get_file_content(self, path):
1638 return u''
1644 return u''
1639
1645
1640 def get_file_content_streamed(self, path):
1646 def get_file_content_streamed(self, path):
1641 yield self.get_file_content()
1647 yield self.get_file_content()
1642
1648
1643 def get_file_size(self, path):
1649 def get_file_size(self, path):
1644 return 0
1650 return 0
1645
1651
1646
1652
1647 class EmptyChangesetClass(type):
1653 class EmptyChangesetClass(type):
1648
1654
1649 def __instancecheck__(self, instance):
1655 def __instancecheck__(self, instance):
1650 return isinstance(instance, EmptyCommit)
1656 return isinstance(instance, EmptyCommit)
1651
1657
1652
1658
1653 class EmptyChangeset(EmptyCommit):
1659 class EmptyChangeset(EmptyCommit):
1654
1660
1655 __metaclass__ = EmptyChangesetClass
1661 __metaclass__ = EmptyChangesetClass
1656
1662
1657 def __new__(cls, *args, **kwargs):
1663 def __new__(cls, *args, **kwargs):
1658 warnings.warn(
1664 warnings.warn(
1659 "Use EmptyCommit instead of EmptyChangeset", DeprecationWarning)
1665 "Use EmptyCommit instead of EmptyChangeset", DeprecationWarning)
1660 return super(EmptyCommit, cls).__new__(cls, *args, **kwargs)
1666 return super(EmptyCommit, cls).__new__(cls, *args, **kwargs)
1661
1667
1662 def __init__(self, cs=EMPTY_COMMIT_ID, repo=None, requested_revision=None,
1668 def __init__(self, cs=EMPTY_COMMIT_ID, repo=None, requested_revision=None,
1663 alias=None, revision=-1, message='', author='', date=None):
1669 alias=None, revision=-1, message='', author='', date=None):
1664 if requested_revision is not None:
1670 if requested_revision is not None:
1665 warnings.warn(
1671 warnings.warn(
1666 "Parameter requested_revision not supported anymore",
1672 "Parameter requested_revision not supported anymore",
1667 DeprecationWarning)
1673 DeprecationWarning)
1668 super(EmptyChangeset, self).__init__(
1674 super(EmptyChangeset, self).__init__(
1669 commit_id=cs, repo=repo, alias=alias, idx=revision,
1675 commit_id=cs, repo=repo, alias=alias, idx=revision,
1670 message=message, author=author, date=date)
1676 message=message, author=author, date=date)
1671
1677
1672 @property
1678 @property
1673 def revision(self):
1679 def revision(self):
1674 warnings.warn("Use idx instead", DeprecationWarning)
1680 warnings.warn("Use idx instead", DeprecationWarning)
1675 return self.idx
1681 return self.idx
1676
1682
1677 @revision.setter
1683 @revision.setter
1678 def revision(self, value):
1684 def revision(self, value):
1679 warnings.warn("Use idx instead", DeprecationWarning)
1685 warnings.warn("Use idx instead", DeprecationWarning)
1680 self.idx = value
1686 self.idx = value
1681
1687
1682
1688
1683 class EmptyRepository(BaseRepository):
1689 class EmptyRepository(BaseRepository):
1684 def __init__(self, repo_path=None, config=None, create=False, **kwargs):
1690 def __init__(self, repo_path=None, config=None, create=False, **kwargs):
1685 pass
1691 pass
1686
1692
1687 def get_diff(self, *args, **kwargs):
1693 def get_diff(self, *args, **kwargs):
1688 from rhodecode.lib.vcs.backends.git.diff import GitDiff
1694 from rhodecode.lib.vcs.backends.git.diff import GitDiff
1689 return GitDiff('')
1695 return GitDiff('')
1690
1696
1691
1697
1692 class CollectionGenerator(object):
1698 class CollectionGenerator(object):
1693
1699
1694 def __init__(self, repo, commit_ids, collection_size=None, pre_load=None, translate_tag=None):
1700 def __init__(self, repo, commit_ids, collection_size=None, pre_load=None, translate_tag=None):
1695 self.repo = repo
1701 self.repo = repo
1696 self.commit_ids = commit_ids
1702 self.commit_ids = commit_ids
1697 # TODO: (oliver) this isn't currently hooked up
1703 # TODO: (oliver) this isn't currently hooked up
1698 self.collection_size = None
1704 self.collection_size = None
1699 self.pre_load = pre_load
1705 self.pre_load = pre_load
1700 self.translate_tag = translate_tag
1706 self.translate_tag = translate_tag
1701
1707
1702 def __len__(self):
1708 def __len__(self):
1703 if self.collection_size is not None:
1709 if self.collection_size is not None:
1704 return self.collection_size
1710 return self.collection_size
1705 return self.commit_ids.__len__()
1711 return self.commit_ids.__len__()
1706
1712
1707 def __iter__(self):
1713 def __iter__(self):
1708 for commit_id in self.commit_ids:
1714 for commit_id in self.commit_ids:
1709 # TODO: johbo: Mercurial passes in commit indices or commit ids
1715 # TODO: johbo: Mercurial passes in commit indices or commit ids
1710 yield self._commit_factory(commit_id)
1716 yield self._commit_factory(commit_id)
1711
1717
1712 def _commit_factory(self, commit_id):
1718 def _commit_factory(self, commit_id):
1713 """
1719 """
1714 Allows backends to override the way commits are generated.
1720 Allows backends to override the way commits are generated.
1715 """
1721 """
1716 return self.repo.get_commit(
1722 return self.repo.get_commit(
1717 commit_id=commit_id, pre_load=self.pre_load,
1723 commit_id=commit_id, pre_load=self.pre_load,
1718 translate_tag=self.translate_tag)
1724 translate_tag=self.translate_tag)
1719
1725
1720 def __getslice__(self, i, j):
1726 def __getslice__(self, i, j):
1721 """
1727 """
1722 Returns an iterator of sliced repository
1728 Returns an iterator of sliced repository
1723 """
1729 """
1724 commit_ids = self.commit_ids[i:j]
1730 commit_ids = self.commit_ids[i:j]
1725 return self.__class__(
1731 return self.__class__(
1726 self.repo, commit_ids, pre_load=self.pre_load,
1732 self.repo, commit_ids, pre_load=self.pre_load,
1727 translate_tag=self.translate_tag)
1733 translate_tag=self.translate_tag)
1728
1734
1729 def __repr__(self):
1735 def __repr__(self):
1730 return '<CollectionGenerator[len:%s]>' % (self.__len__())
1736 return '<CollectionGenerator[len:%s]>' % (self.__len__())
1731
1737
1732
1738
1733 class Config(object):
1739 class Config(object):
1734 """
1740 """
1735 Represents the configuration for a repository.
1741 Represents the configuration for a repository.
1736
1742
1737 The API is inspired by :class:`ConfigParser.ConfigParser` from the
1743 The API is inspired by :class:`ConfigParser.ConfigParser` from the
1738 standard library. It implements only the needed subset.
1744 standard library. It implements only the needed subset.
1739 """
1745 """
1740
1746
1741 def __init__(self):
1747 def __init__(self):
1742 self._values = {}
1748 self._values = {}
1743
1749
1744 def copy(self):
1750 def copy(self):
1745 clone = Config()
1751 clone = Config()
1746 for section, values in self._values.items():
1752 for section, values in self._values.items():
1747 clone._values[section] = values.copy()
1753 clone._values[section] = values.copy()
1748 return clone
1754 return clone
1749
1755
1750 def __repr__(self):
1756 def __repr__(self):
1751 return '<Config(%s sections) at %s>' % (
1757 return '<Config(%s sections) at %s>' % (
1752 len(self._values), hex(id(self)))
1758 len(self._values), hex(id(self)))
1753
1759
1754 def items(self, section):
1760 def items(self, section):
1755 return self._values.get(section, {}).iteritems()
1761 return self._values.get(section, {}).iteritems()
1756
1762
1757 def get(self, section, option):
1763 def get(self, section, option):
1758 return self._values.get(section, {}).get(option)
1764 return self._values.get(section, {}).get(option)
1759
1765
1760 def set(self, section, option, value):
1766 def set(self, section, option, value):
1761 section_values = self._values.setdefault(section, {})
1767 section_values = self._values.setdefault(section, {})
1762 section_values[option] = value
1768 section_values[option] = value
1763
1769
1764 def clear_section(self, section):
1770 def clear_section(self, section):
1765 self._values[section] = {}
1771 self._values[section] = {}
1766
1772
1767 def serialize(self):
1773 def serialize(self):
1768 """
1774 """
1769 Creates a list of three tuples (section, key, value) representing
1775 Creates a list of three tuples (section, key, value) representing
1770 this config object.
1776 this config object.
1771 """
1777 """
1772 items = []
1778 items = []
1773 for section in self._values:
1779 for section in self._values:
1774 for option, value in self._values[section].items():
1780 for option, value in self._values[section].items():
1775 items.append(
1781 items.append(
1776 (safe_str(section), safe_str(option), safe_str(value)))
1782 (safe_str(section), safe_str(option), safe_str(value)))
1777 return items
1783 return items
1778
1784
1779
1785
1780 class Diff(object):
1786 class Diff(object):
1781 """
1787 """
1782 Represents a diff result from a repository backend.
1788 Represents a diff result from a repository backend.
1783
1789
1784 Subclasses have to provide a backend specific value for
1790 Subclasses have to provide a backend specific value for
1785 :attr:`_header_re` and :attr:`_meta_re`.
1791 :attr:`_header_re` and :attr:`_meta_re`.
1786 """
1792 """
1787 _meta_re = None
1793 _meta_re = None
1788 _header_re = None
1794 _header_re = None
1789
1795
1790 def __init__(self, raw_diff):
1796 def __init__(self, raw_diff):
1791 self.raw = raw_diff
1797 self.raw = raw_diff
1792
1798
1793 def chunks(self):
1799 def chunks(self):
1794 """
1800 """
1795 split the diff in chunks of separate --git a/file b/file chunks
1801 split the diff in chunks of separate --git a/file b/file chunks
1796 to make diffs consistent we must prepend with \n, and make sure
1802 to make diffs consistent we must prepend with \n, and make sure
1797 we can detect last chunk as this was also has special rule
1803 we can detect last chunk as this was also has special rule
1798 """
1804 """
1799
1805
1800 diff_parts = ('\n' + self.raw).split('\ndiff --git')
1806 diff_parts = ('\n' + self.raw).split('\ndiff --git')
1801 header = diff_parts[0]
1807 header = diff_parts[0]
1802
1808
1803 if self._meta_re:
1809 if self._meta_re:
1804 match = self._meta_re.match(header)
1810 match = self._meta_re.match(header)
1805
1811
1806 chunks = diff_parts[1:]
1812 chunks = diff_parts[1:]
1807 total_chunks = len(chunks)
1813 total_chunks = len(chunks)
1808
1814
1809 return (
1815 return (
1810 DiffChunk(chunk, self, cur_chunk == total_chunks)
1816 DiffChunk(chunk, self, cur_chunk == total_chunks)
1811 for cur_chunk, chunk in enumerate(chunks, start=1))
1817 for cur_chunk, chunk in enumerate(chunks, start=1))
1812
1818
1813
1819
1814 class DiffChunk(object):
1820 class DiffChunk(object):
1815
1821
1816 def __init__(self, chunk, diff, last_chunk):
1822 def __init__(self, chunk, diff, last_chunk):
1817 self._diff = diff
1823 self._diff = diff
1818
1824
1819 # since we split by \ndiff --git that part is lost from original diff
1825 # since we split by \ndiff --git that part is lost from original diff
1820 # we need to re-apply it at the end, EXCEPT ! if it's last chunk
1826 # we need to re-apply it at the end, EXCEPT ! if it's last chunk
1821 if not last_chunk:
1827 if not last_chunk:
1822 chunk += '\n'
1828 chunk += '\n'
1823
1829
1824 match = self._diff._header_re.match(chunk)
1830 match = self._diff._header_re.match(chunk)
1825 self.header = match.groupdict()
1831 self.header = match.groupdict()
1826 self.diff = chunk[match.end():]
1832 self.diff = chunk[match.end():]
1827 self.raw = chunk
1833 self.raw = chunk
1828
1834
1829
1835
1830 class BasePathPermissionChecker(object):
1836 class BasePathPermissionChecker(object):
1831
1837
1832 @staticmethod
1838 @staticmethod
1833 def create_from_patterns(includes, excludes):
1839 def create_from_patterns(includes, excludes):
1834 if includes and '*' in includes and not excludes:
1840 if includes and '*' in includes and not excludes:
1835 return AllPathPermissionChecker()
1841 return AllPathPermissionChecker()
1836 elif excludes and '*' in excludes:
1842 elif excludes and '*' in excludes:
1837 return NonePathPermissionChecker()
1843 return NonePathPermissionChecker()
1838 else:
1844 else:
1839 return PatternPathPermissionChecker(includes, excludes)
1845 return PatternPathPermissionChecker(includes, excludes)
1840
1846
1841 @property
1847 @property
1842 def has_full_access(self):
1848 def has_full_access(self):
1843 raise NotImplemented()
1849 raise NotImplemented()
1844
1850
1845 def has_access(self, path):
1851 def has_access(self, path):
1846 raise NotImplemented()
1852 raise NotImplemented()
1847
1853
1848
1854
1849 class AllPathPermissionChecker(BasePathPermissionChecker):
1855 class AllPathPermissionChecker(BasePathPermissionChecker):
1850
1856
1851 @property
1857 @property
1852 def has_full_access(self):
1858 def has_full_access(self):
1853 return True
1859 return True
1854
1860
1855 def has_access(self, path):
1861 def has_access(self, path):
1856 return True
1862 return True
1857
1863
1858
1864
1859 class NonePathPermissionChecker(BasePathPermissionChecker):
1865 class NonePathPermissionChecker(BasePathPermissionChecker):
1860
1866
1861 @property
1867 @property
1862 def has_full_access(self):
1868 def has_full_access(self):
1863 return False
1869 return False
1864
1870
1865 def has_access(self, path):
1871 def has_access(self, path):
1866 return False
1872 return False
1867
1873
1868
1874
1869 class PatternPathPermissionChecker(BasePathPermissionChecker):
1875 class PatternPathPermissionChecker(BasePathPermissionChecker):
1870
1876
1871 def __init__(self, includes, excludes):
1877 def __init__(self, includes, excludes):
1872 self.includes = includes
1878 self.includes = includes
1873 self.excludes = excludes
1879 self.excludes = excludes
1874 self.includes_re = [] if not includes else [
1880 self.includes_re = [] if not includes else [
1875 re.compile(fnmatch.translate(pattern)) for pattern in includes]
1881 re.compile(fnmatch.translate(pattern)) for pattern in includes]
1876 self.excludes_re = [] if not excludes else [
1882 self.excludes_re = [] if not excludes else [
1877 re.compile(fnmatch.translate(pattern)) for pattern in excludes]
1883 re.compile(fnmatch.translate(pattern)) for pattern in excludes]
1878
1884
1879 @property
1885 @property
1880 def has_full_access(self):
1886 def has_full_access(self):
1881 return '*' in self.includes and not self.excludes
1887 return '*' in self.includes and not self.excludes
1882
1888
1883 def has_access(self, path):
1889 def has_access(self, path):
1884 for regex in self.excludes_re:
1890 for regex in self.excludes_re:
1885 if regex.match(path):
1891 if regex.match(path):
1886 return False
1892 return False
1887 for regex in self.includes_re:
1893 for regex in self.includes_re:
1888 if regex.match(path):
1894 if regex.match(path):
1889 return True
1895 return True
1890 return False
1896 return False
@@ -1,479 +1,483 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2014-2019 RhodeCode GmbH
3 # Copyright (C) 2014-2019 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 GIT commit module
22 GIT commit module
23 """
23 """
24
24
25 import re
25 import re
26 import stat
26 import stat
27 from itertools import chain
27 from itertools import chain
28 from StringIO import StringIO
28 from StringIO import StringIO
29
29
30 from zope.cachedescriptors.property import Lazy as LazyProperty
30 from zope.cachedescriptors.property import Lazy as LazyProperty
31
31
32 from rhodecode.lib.datelib import utcdate_fromtimestamp
32 from rhodecode.lib.datelib import utcdate_fromtimestamp
33 from rhodecode.lib.utils import safe_unicode, safe_str
33 from rhodecode.lib.utils import safe_unicode, safe_str
34 from rhodecode.lib.utils2 import safe_int
34 from rhodecode.lib.utils2 import safe_int
35 from rhodecode.lib.vcs.conf import settings
35 from rhodecode.lib.vcs.conf import settings
36 from rhodecode.lib.vcs.backends import base
36 from rhodecode.lib.vcs.backends import base
37 from rhodecode.lib.vcs.exceptions import CommitError, NodeDoesNotExistError
37 from rhodecode.lib.vcs.exceptions import CommitError, NodeDoesNotExistError
38 from rhodecode.lib.vcs.nodes import (
38 from rhodecode.lib.vcs.nodes import (
39 FileNode, DirNode, NodeKind, RootNode, SubModuleNode,
39 FileNode, DirNode, NodeKind, RootNode, SubModuleNode,
40 ChangedFileNodesGenerator, AddedFileNodesGenerator,
40 ChangedFileNodesGenerator, AddedFileNodesGenerator,
41 RemovedFileNodesGenerator, LargeFileNode)
41 RemovedFileNodesGenerator, LargeFileNode)
42 from rhodecode.lib.vcs.compat import configparser
42 from rhodecode.lib.vcs.compat import configparser
43
43
44
44
45 class GitCommit(base.BaseCommit):
45 class GitCommit(base.BaseCommit):
46 """
46 """
47 Represents state of the repository at single commit id.
47 Represents state of the repository at single commit id.
48 """
48 """
49
49
50 _filter_pre_load = [
50 _filter_pre_load = [
51 # done through a more complex tree walk on parents
51 # done through a more complex tree walk on parents
52 "affected_files",
52 "affected_files",
53 # done through subprocess not remote call
53 # done through subprocess not remote call
54 "children",
54 "children",
55 # done through a more complex tree walk on parents
55 # done through a more complex tree walk on parents
56 "status",
56 "status",
57 # mercurial specific property not supported here
57 # mercurial specific property not supported here
58 "_file_paths",
58 "_file_paths",
59 # mercurial specific property not supported here
59 # mercurial specific property not supported here
60 'obsolete',
60 'obsolete',
61 # mercurial specific property not supported here
61 # mercurial specific property not supported here
62 'phase',
62 'phase',
63 # mercurial specific property not supported here
63 # mercurial specific property not supported here
64 'hidden'
64 'hidden'
65 ]
65 ]
66
66
67 def __init__(self, repository, raw_id, idx, pre_load=None):
67 def __init__(self, repository, raw_id, idx, pre_load=None):
68 self.repository = repository
68 self.repository = repository
69 self._remote = repository._remote
69 self._remote = repository._remote
70 # TODO: johbo: Tweak of raw_id should not be necessary
70 # TODO: johbo: Tweak of raw_id should not be necessary
71 self.raw_id = safe_str(raw_id)
71 self.raw_id = safe_str(raw_id)
72 self.idx = idx
72 self.idx = idx
73
73
74 self._set_bulk_properties(pre_load)
74 self._set_bulk_properties(pre_load)
75
75
76 # caches
76 # caches
77 self._stat_modes = {} # stat info for paths
77 self._stat_modes = {} # stat info for paths
78 self._paths = {} # path processed with parse_tree
78 self._paths = {} # path processed with parse_tree
79 self.nodes = {}
79 self.nodes = {}
80 self._submodules = None
80 self._submodules = None
81
81
82 def _set_bulk_properties(self, pre_load):
82 def _set_bulk_properties(self, pre_load):
83
83
84 if not pre_load:
84 if not pre_load:
85 return
85 return
86 pre_load = [entry for entry in pre_load
86 pre_load = [entry for entry in pre_load
87 if entry not in self._filter_pre_load]
87 if entry not in self._filter_pre_load]
88 if not pre_load:
88 if not pre_load:
89 return
89 return
90
90
91 result = self._remote.bulk_request(self.raw_id, pre_load)
91 result = self._remote.bulk_request(self.raw_id, pre_load)
92 for attr, value in result.items():
92 for attr, value in result.items():
93 if attr in ["author", "message"]:
93 if attr in ["author", "message"]:
94 if value:
94 if value:
95 value = safe_unicode(value)
95 value = safe_unicode(value)
96 elif attr == "date":
96 elif attr == "date":
97 value = utcdate_fromtimestamp(*value)
97 value = utcdate_fromtimestamp(*value)
98 elif attr == "parents":
98 elif attr == "parents":
99 value = self._make_commits(value)
99 value = self._make_commits(value)
100 elif attr == "branch":
100 elif attr == "branch":
101 value = value[0] if value else None
101 value = value[0] if value else None
102 self.__dict__[attr] = value
102 self.__dict__[attr] = value
103
103
104 @LazyProperty
104 @LazyProperty
105 def _commit(self):
105 def _commit(self):
106 return self._remote[self.raw_id]
106 return self._remote[self.raw_id]
107
107
108 @LazyProperty
108 @LazyProperty
109 def _tree_id(self):
109 def _tree_id(self):
110 return self._remote[self._commit['tree']]['id']
110 return self._remote[self._commit['tree']]['id']
111
111
112 @LazyProperty
112 @LazyProperty
113 def id(self):
113 def id(self):
114 return self.raw_id
114 return self.raw_id
115
115
116 @LazyProperty
116 @LazyProperty
117 def short_id(self):
117 def short_id(self):
118 return self.raw_id[:12]
118 return self.raw_id[:12]
119
119
120 @LazyProperty
120 @LazyProperty
121 def message(self):
121 def message(self):
122 return safe_unicode(self._remote.message(self.id))
122 return safe_unicode(self._remote.message(self.id))
123
123
124 @LazyProperty
124 @LazyProperty
125 def committer(self):
125 def committer(self):
126 return safe_unicode(self._remote.author(self.id))
126 return safe_unicode(self._remote.author(self.id))
127
127
128 @LazyProperty
128 @LazyProperty
129 def author(self):
129 def author(self):
130 return safe_unicode(self._remote.author(self.id))
130 return safe_unicode(self._remote.author(self.id))
131
131
132 @LazyProperty
132 @LazyProperty
133 def date(self):
133 def date(self):
134 unix_ts, tz = self._remote.date(self.raw_id)
134 unix_ts, tz = self._remote.date(self.raw_id)
135 return utcdate_fromtimestamp(unix_ts, tz)
135 return utcdate_fromtimestamp(unix_ts, tz)
136
136
137 @LazyProperty
137 @LazyProperty
138 def status(self):
138 def status(self):
139 """
139 """
140 Returns modified, added, removed, deleted files for current commit
140 Returns modified, added, removed, deleted files for current commit
141 """
141 """
142 return self.changed, self.added, self.removed
142 return self.changed, self.added, self.removed
143
143
144 @LazyProperty
144 @LazyProperty
145 def tags(self):
145 def tags(self):
146 tags = [safe_unicode(name) for name,
146 tags = [safe_unicode(name) for name,
147 commit_id in self.repository.tags.iteritems()
147 commit_id in self.repository.tags.iteritems()
148 if commit_id == self.raw_id]
148 if commit_id == self.raw_id]
149 return tags
149 return tags
150
150
151 @LazyProperty
151 @LazyProperty
152 def commit_branches(self):
152 def commit_branches(self):
153 branches = []
153 branches = []
154 for name, commit_id in self.repository.branches.iteritems():
154 for name, commit_id in self.repository.branches.iteritems():
155 if commit_id == self.raw_id:
155 if commit_id == self.raw_id:
156 branches.append(name)
156 branches.append(name)
157 return branches
157 return branches
158
158
159 @LazyProperty
159 @LazyProperty
160 def branch(self):
160 def branch(self):
161 branches = self._remote.branch(self.raw_id)
161 branches = self._remote.branch(self.raw_id)
162
162
163 if branches:
163 if branches:
164 # actually commit can have multiple branches in git
164 # actually commit can have multiple branches in git
165 return safe_unicode(branches[0])
165 return safe_unicode(branches[0])
166
166
167 def _get_tree_id_for_path(self, path):
167 def _get_tree_id_for_path(self, path):
168 path = safe_str(path)
168 path = safe_str(path)
169 if path in self._paths:
169 if path in self._paths:
170 return self._paths[path]
170 return self._paths[path]
171
171
172 tree_id = self._tree_id
172 tree_id = self._tree_id
173
173
174 path = path.strip('/')
174 path = path.strip('/')
175 if path == '':
175 if path == '':
176 data = [tree_id, "tree"]
176 data = [tree_id, "tree"]
177 self._paths[''] = data
177 self._paths[''] = data
178 return data
178 return data
179
179
180 tree_id, tree_type, tree_mode = \
180 tree_id, tree_type, tree_mode = \
181 self._remote.tree_and_type_for_path(self.raw_id, path)
181 self._remote.tree_and_type_for_path(self.raw_id, path)
182 if tree_id is None:
182 if tree_id is None:
183 raise self.no_node_at_path(path)
183 raise self.no_node_at_path(path)
184
184
185 self._paths[path] = [tree_id, tree_type]
185 self._paths[path] = [tree_id, tree_type]
186 self._stat_modes[path] = tree_mode
186 self._stat_modes[path] = tree_mode
187
187
188 if path not in self._paths:
188 if path not in self._paths:
189 raise self.no_node_at_path(path)
189 raise self.no_node_at_path(path)
190
190
191 return self._paths[path]
191 return self._paths[path]
192
192
193 def _get_kind(self, path):
193 def _get_kind(self, path):
194 tree_id, type_ = self._get_tree_id_for_path(path)
194 tree_id, type_ = self._get_tree_id_for_path(path)
195 if type_ == 'blob':
195 if type_ == 'blob':
196 return NodeKind.FILE
196 return NodeKind.FILE
197 elif type_ == 'tree':
197 elif type_ == 'tree':
198 return NodeKind.DIR
198 return NodeKind.DIR
199 elif type_ == 'link':
199 elif type_ == 'link':
200 return NodeKind.SUBMODULE
200 return NodeKind.SUBMODULE
201 return None
201 return None
202
202
203 def _get_filectx(self, path):
203 def _get_filectx(self, path):
204 path = self._fix_path(path)
204 path = self._fix_path(path)
205 if self._get_kind(path) != NodeKind.FILE:
205 if self._get_kind(path) != NodeKind.FILE:
206 raise CommitError(
206 raise CommitError(
207 "File does not exist for commit %s at '%s'" % (self.raw_id, path))
207 "File does not exist for commit %s at '%s'" % (self.raw_id, path))
208 return path
208 return path
209
209
210 def _get_file_nodes(self):
210 def _get_file_nodes(self):
211 return chain(*(t[2] for t in self.walk()))
211 return chain(*(t[2] for t in self.walk()))
212
212
213 @LazyProperty
213 @LazyProperty
214 def parents(self):
214 def parents(self):
215 """
215 """
216 Returns list of parent commits.
216 Returns list of parent commits.
217 """
217 """
218 parent_ids = self._remote.parents(self.id)
218 parent_ids = self._remote.parents(self.id)
219 return self._make_commits(parent_ids)
219 return self._make_commits(parent_ids)
220
220
221 @LazyProperty
221 @LazyProperty
222 def children(self):
222 def children(self):
223 """
223 """
224 Returns list of child commits.
224 Returns list of child commits.
225 """
225 """
226
226
227 children = self._remote.children(self.raw_id)
227 children = self._remote.children(self.raw_id)
228 return self._make_commits(children)
228 return self._make_commits(children)
229
229
230 def _make_commits(self, commit_ids):
230 def _make_commits(self, commit_ids):
231 def commit_maker(_commit_id):
231 def commit_maker(_commit_id):
232 return self.repository.get_commit(commit_id=commit_id)
232 return self.repository.get_commit(commit_id=commit_id)
233
233
234 return [commit_maker(commit_id) for commit_id in commit_ids]
234 return [commit_maker(commit_id) for commit_id in commit_ids]
235
235
236 def get_file_mode(self, path):
236 def get_file_mode(self, path):
237 """
237 """
238 Returns stat mode of the file at the given `path`.
238 Returns stat mode of the file at the given `path`.
239 """
239 """
240 path = safe_str(path)
240 path = safe_str(path)
241 # ensure path is traversed
241 # ensure path is traversed
242 self._get_tree_id_for_path(path)
242 self._get_tree_id_for_path(path)
243 return self._stat_modes[path]
243 return self._stat_modes[path]
244
244
245 def is_link(self, path):
245 def is_link(self, path):
246 return stat.S_ISLNK(self.get_file_mode(path))
246 return stat.S_ISLNK(self.get_file_mode(path))
247
247
248 def is_node_binary(self, path):
249 tree_id, _ = self._get_tree_id_for_path(path)
250 return self._remote.is_binary(tree_id)
251
248 def get_file_content(self, path):
252 def get_file_content(self, path):
249 """
253 """
250 Returns content of the file at given `path`.
254 Returns content of the file at given `path`.
251 """
255 """
252 tree_id, _ = self._get_tree_id_for_path(path)
256 tree_id, _ = self._get_tree_id_for_path(path)
253 return self._remote.blob_as_pretty_string(tree_id)
257 return self._remote.blob_as_pretty_string(tree_id)
254
258
255 def get_file_content_streamed(self, path):
259 def get_file_content_streamed(self, path):
256 tree_id, _ = self._get_tree_id_for_path(path)
260 tree_id, _ = self._get_tree_id_for_path(path)
257 stream_method = getattr(self._remote, 'stream:blob_as_pretty_string')
261 stream_method = getattr(self._remote, 'stream:blob_as_pretty_string')
258 return stream_method(tree_id)
262 return stream_method(tree_id)
259
263
260 def get_file_size(self, path):
264 def get_file_size(self, path):
261 """
265 """
262 Returns size of the file at given `path`.
266 Returns size of the file at given `path`.
263 """
267 """
264 tree_id, _ = self._get_tree_id_for_path(path)
268 tree_id, _ = self._get_tree_id_for_path(path)
265 return self._remote.blob_raw_length(tree_id)
269 return self._remote.blob_raw_length(tree_id)
266
270
267 def get_path_history(self, path, limit=None, pre_load=None):
271 def get_path_history(self, path, limit=None, pre_load=None):
268 """
272 """
269 Returns history of file as reversed list of `GitCommit` objects for
273 Returns history of file as reversed list of `GitCommit` objects for
270 which file at given `path` has been modified.
274 which file at given `path` has been modified.
271 """
275 """
272
276
273 path = self._get_filectx(path)
277 path = self._get_filectx(path)
274 hist = self._remote.node_history(self.raw_id, path, limit)
278 hist = self._remote.node_history(self.raw_id, path, limit)
275 return [
279 return [
276 self.repository.get_commit(commit_id=commit_id, pre_load=pre_load)
280 self.repository.get_commit(commit_id=commit_id, pre_load=pre_load)
277 for commit_id in hist]
281 for commit_id in hist]
278
282
279 def get_file_annotate(self, path, pre_load=None):
283 def get_file_annotate(self, path, pre_load=None):
280 """
284 """
281 Returns a generator of four element tuples with
285 Returns a generator of four element tuples with
282 lineno, commit_id, commit lazy loader and line
286 lineno, commit_id, commit lazy loader and line
283 """
287 """
284
288
285 result = self._remote.node_annotate(self.raw_id, path)
289 result = self._remote.node_annotate(self.raw_id, path)
286
290
287 for ln_no, commit_id, content in result:
291 for ln_no, commit_id, content in result:
288 yield (
292 yield (
289 ln_no, commit_id,
293 ln_no, commit_id,
290 lambda: self.repository.get_commit(commit_id=commit_id, pre_load=pre_load),
294 lambda: self.repository.get_commit(commit_id=commit_id, pre_load=pre_load),
291 content)
295 content)
292
296
293 def get_nodes(self, path):
297 def get_nodes(self, path):
294
298
295 if self._get_kind(path) != NodeKind.DIR:
299 if self._get_kind(path) != NodeKind.DIR:
296 raise CommitError(
300 raise CommitError(
297 "Directory does not exist for commit %s at '%s'" % (self.raw_id, path))
301 "Directory does not exist for commit %s at '%s'" % (self.raw_id, path))
298 path = self._fix_path(path)
302 path = self._fix_path(path)
299
303
300 tree_id, _ = self._get_tree_id_for_path(path)
304 tree_id, _ = self._get_tree_id_for_path(path)
301
305
302 dirnodes = []
306 dirnodes = []
303 filenodes = []
307 filenodes = []
304
308
305 # extracted tree ID gives us our files...
309 # extracted tree ID gives us our files...
306 for name, stat_, id_, type_ in self._remote.tree_items(tree_id):
310 for name, stat_, id_, type_ in self._remote.tree_items(tree_id):
307 if type_ == 'link':
311 if type_ == 'link':
308 url = self._get_submodule_url('/'.join((path, name)))
312 url = self._get_submodule_url('/'.join((path, name)))
309 dirnodes.append(SubModuleNode(
313 dirnodes.append(SubModuleNode(
310 name, url=url, commit=id_, alias=self.repository.alias))
314 name, url=url, commit=id_, alias=self.repository.alias))
311 continue
315 continue
312
316
313 if path != '':
317 if path != '':
314 obj_path = '/'.join((path, name))
318 obj_path = '/'.join((path, name))
315 else:
319 else:
316 obj_path = name
320 obj_path = name
317 if obj_path not in self._stat_modes:
321 if obj_path not in self._stat_modes:
318 self._stat_modes[obj_path] = stat_
322 self._stat_modes[obj_path] = stat_
319
323
320 if type_ == 'tree':
324 if type_ == 'tree':
321 dirnodes.append(DirNode(obj_path, commit=self))
325 dirnodes.append(DirNode(obj_path, commit=self))
322 elif type_ == 'blob':
326 elif type_ == 'blob':
323 filenodes.append(FileNode(obj_path, commit=self, mode=stat_))
327 filenodes.append(FileNode(obj_path, commit=self, mode=stat_))
324 else:
328 else:
325 raise CommitError(
329 raise CommitError(
326 "Requested object should be Tree or Blob, is %s", type_)
330 "Requested object should be Tree or Blob, is %s", type_)
327
331
328 nodes = dirnodes + filenodes
332 nodes = dirnodes + filenodes
329 for node in nodes:
333 for node in nodes:
330 if node.path not in self.nodes:
334 if node.path not in self.nodes:
331 self.nodes[node.path] = node
335 self.nodes[node.path] = node
332 nodes.sort()
336 nodes.sort()
333 return nodes
337 return nodes
334
338
335 def get_node(self, path, pre_load=None):
339 def get_node(self, path, pre_load=None):
336 if isinstance(path, unicode):
340 if isinstance(path, unicode):
337 path = path.encode('utf-8')
341 path = path.encode('utf-8')
338 path = self._fix_path(path)
342 path = self._fix_path(path)
339 if path not in self.nodes:
343 if path not in self.nodes:
340 try:
344 try:
341 tree_id, type_ = self._get_tree_id_for_path(path)
345 tree_id, type_ = self._get_tree_id_for_path(path)
342 except CommitError:
346 except CommitError:
343 raise NodeDoesNotExistError(
347 raise NodeDoesNotExistError(
344 "Cannot find one of parents' directories for a given "
348 "Cannot find one of parents' directories for a given "
345 "path: %s" % path)
349 "path: %s" % path)
346
350
347 if type_ == 'link':
351 if type_ == 'link':
348 url = self._get_submodule_url(path)
352 url = self._get_submodule_url(path)
349 node = SubModuleNode(path, url=url, commit=tree_id,
353 node = SubModuleNode(path, url=url, commit=tree_id,
350 alias=self.repository.alias)
354 alias=self.repository.alias)
351 elif type_ == 'tree':
355 elif type_ == 'tree':
352 if path == '':
356 if path == '':
353 node = RootNode(commit=self)
357 node = RootNode(commit=self)
354 else:
358 else:
355 node = DirNode(path, commit=self)
359 node = DirNode(path, commit=self)
356 elif type_ == 'blob':
360 elif type_ == 'blob':
357 node = FileNode(path, commit=self, pre_load=pre_load)
361 node = FileNode(path, commit=self, pre_load=pre_load)
358 self._stat_modes[path] = node.mode
362 self._stat_modes[path] = node.mode
359 else:
363 else:
360 raise self.no_node_at_path(path)
364 raise self.no_node_at_path(path)
361
365
362 # cache node
366 # cache node
363 self.nodes[path] = node
367 self.nodes[path] = node
364
368
365 return self.nodes[path]
369 return self.nodes[path]
366
370
367 def get_largefile_node(self, path):
371 def get_largefile_node(self, path):
368 tree_id, _ = self._get_tree_id_for_path(path)
372 tree_id, _ = self._get_tree_id_for_path(path)
369 pointer_spec = self._remote.is_large_file(tree_id)
373 pointer_spec = self._remote.is_large_file(tree_id)
370
374
371 if pointer_spec:
375 if pointer_spec:
372 # content of that file regular FileNode is the hash of largefile
376 # content of that file regular FileNode is the hash of largefile
373 file_id = pointer_spec.get('oid_hash')
377 file_id = pointer_spec.get('oid_hash')
374 if self._remote.in_largefiles_store(file_id):
378 if self._remote.in_largefiles_store(file_id):
375 lf_path = self._remote.store_path(file_id)
379 lf_path = self._remote.store_path(file_id)
376 return LargeFileNode(lf_path, commit=self, org_path=path)
380 return LargeFileNode(lf_path, commit=self, org_path=path)
377
381
378 @LazyProperty
382 @LazyProperty
379 def affected_files(self):
383 def affected_files(self):
380 """
384 """
381 Gets a fast accessible file changes for given commit
385 Gets a fast accessible file changes for given commit
382 """
386 """
383 added, modified, deleted = self._changes_cache
387 added, modified, deleted = self._changes_cache
384 return list(added.union(modified).union(deleted))
388 return list(added.union(modified).union(deleted))
385
389
386 @LazyProperty
390 @LazyProperty
387 def _changes_cache(self):
391 def _changes_cache(self):
388 added = set()
392 added = set()
389 modified = set()
393 modified = set()
390 deleted = set()
394 deleted = set()
391 _r = self._remote
395 _r = self._remote
392
396
393 parents = self.parents
397 parents = self.parents
394 if not self.parents:
398 if not self.parents:
395 parents = [base.EmptyCommit()]
399 parents = [base.EmptyCommit()]
396 for parent in parents:
400 for parent in parents:
397 if isinstance(parent, base.EmptyCommit):
401 if isinstance(parent, base.EmptyCommit):
398 oid = None
402 oid = None
399 else:
403 else:
400 oid = parent.raw_id
404 oid = parent.raw_id
401 changes = _r.tree_changes(oid, self.raw_id)
405 changes = _r.tree_changes(oid, self.raw_id)
402 for (oldpath, newpath), (_, _), (_, _) in changes:
406 for (oldpath, newpath), (_, _), (_, _) in changes:
403 if newpath and oldpath:
407 if newpath and oldpath:
404 modified.add(newpath)
408 modified.add(newpath)
405 elif newpath and not oldpath:
409 elif newpath and not oldpath:
406 added.add(newpath)
410 added.add(newpath)
407 elif not newpath and oldpath:
411 elif not newpath and oldpath:
408 deleted.add(oldpath)
412 deleted.add(oldpath)
409 return added, modified, deleted
413 return added, modified, deleted
410
414
411 def _get_paths_for_status(self, status):
415 def _get_paths_for_status(self, status):
412 """
416 """
413 Returns sorted list of paths for given ``status``.
417 Returns sorted list of paths for given ``status``.
414
418
415 :param status: one of: *added*, *modified* or *deleted*
419 :param status: one of: *added*, *modified* or *deleted*
416 """
420 """
417 added, modified, deleted = self._changes_cache
421 added, modified, deleted = self._changes_cache
418 return sorted({
422 return sorted({
419 'added': list(added),
423 'added': list(added),
420 'modified': list(modified),
424 'modified': list(modified),
421 'deleted': list(deleted)}[status]
425 'deleted': list(deleted)}[status]
422 )
426 )
423
427
424 @LazyProperty
428 @LazyProperty
425 def added(self):
429 def added(self):
426 """
430 """
427 Returns list of added ``FileNode`` objects.
431 Returns list of added ``FileNode`` objects.
428 """
432 """
429 if not self.parents:
433 if not self.parents:
430 return list(self._get_file_nodes())
434 return list(self._get_file_nodes())
431 return AddedFileNodesGenerator(
435 return AddedFileNodesGenerator(
432 [n for n in self._get_paths_for_status('added')], self)
436 [n for n in self._get_paths_for_status('added')], self)
433
437
434 @LazyProperty
438 @LazyProperty
435 def changed(self):
439 def changed(self):
436 """
440 """
437 Returns list of modified ``FileNode`` objects.
441 Returns list of modified ``FileNode`` objects.
438 """
442 """
439 if not self.parents:
443 if not self.parents:
440 return []
444 return []
441 return ChangedFileNodesGenerator(
445 return ChangedFileNodesGenerator(
442 [n for n in self._get_paths_for_status('modified')], self)
446 [n for n in self._get_paths_for_status('modified')], self)
443
447
444 @LazyProperty
448 @LazyProperty
445 def removed(self):
449 def removed(self):
446 """
450 """
447 Returns list of removed ``FileNode`` objects.
451 Returns list of removed ``FileNode`` objects.
448 """
452 """
449 if not self.parents:
453 if not self.parents:
450 return []
454 return []
451 return RemovedFileNodesGenerator(
455 return RemovedFileNodesGenerator(
452 [n for n in self._get_paths_for_status('deleted')], self)
456 [n for n in self._get_paths_for_status('deleted')], self)
453
457
454 def _get_submodule_url(self, submodule_path):
458 def _get_submodule_url(self, submodule_path):
455 git_modules_path = '.gitmodules'
459 git_modules_path = '.gitmodules'
456
460
457 if self._submodules is None:
461 if self._submodules is None:
458 self._submodules = {}
462 self._submodules = {}
459
463
460 try:
464 try:
461 submodules_node = self.get_node(git_modules_path)
465 submodules_node = self.get_node(git_modules_path)
462 except NodeDoesNotExistError:
466 except NodeDoesNotExistError:
463 return None
467 return None
464
468
465 content = submodules_node.content
469 content = submodules_node.content
466
470
467 # ConfigParser fails if there are whitespaces
471 # ConfigParser fails if there are whitespaces
468 content = '\n'.join(l.strip() for l in content.split('\n'))
472 content = '\n'.join(l.strip() for l in content.split('\n'))
469
473
470 parser = configparser.ConfigParser()
474 parser = configparser.ConfigParser()
471 parser.readfp(StringIO(content))
475 parser.readfp(StringIO(content))
472
476
473 for section in parser.sections():
477 for section in parser.sections():
474 path = parser.get(section, 'path')
478 path = parser.get(section, 'path')
475 url = parser.get(section, 'url')
479 url = parser.get(section, 'url')
476 if path and url:
480 if path and url:
477 self._submodules[path.strip('/')] = url
481 self._submodules[path.strip('/')] = url
478
482
479 return self._submodules.get(submodule_path.strip('/'))
483 return self._submodules.get(submodule_path.strip('/'))
@@ -1,104 +1,106 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2014-2019 RhodeCode GmbH
3 # Copyright (C) 2014-2019 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 GIT inmemory module
22 GIT inmemory module
23 """
23 """
24
24
25 from rhodecode.lib.datelib import date_to_timestamp_plus_offset
25 from rhodecode.lib.datelib import date_to_timestamp_plus_offset
26 from rhodecode.lib.utils import safe_str
26 from rhodecode.lib.utils import safe_str
27 from rhodecode.lib.vcs.backends import base
27 from rhodecode.lib.vcs.backends import base
28
28
29
29
30 class GitInMemoryCommit(base.BaseInMemoryCommit):
30 class GitInMemoryCommit(base.BaseInMemoryCommit):
31
31
32 def commit(self, message, author, parents=None, branch=None, date=None, **kwargs):
32 def commit(self, message, author, parents=None, branch=None, date=None, **kwargs):
33 """
33 """
34 Performs in-memory commit (doesn't check workdir in any way) and
34 Performs in-memory commit (doesn't check workdir in any way) and
35 returns newly created `GitCommit`. Updates repository's
35 returns newly created `GitCommit`. Updates repository's
36 `commit_ids`.
36 `commit_ids`.
37
37
38 :param message: message of the commit
38 :param message: message of the commit
39 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
39 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
40 :param parents: single parent or sequence of parents from which commit
40 :param parents: single parent or sequence of parents from which commit
41 would be derived
41 would be derived
42 :param date: `datetime.datetime` instance. Defaults to
42 :param date: `datetime.datetime` instance. Defaults to
43 ``datetime.datetime.now()``.
43 ``datetime.datetime.now()``.
44 :param branch: branch name, as string. If none given, default backend's
44 :param branch: branch name, as string. If none given, default backend's
45 branch would be used.
45 branch would be used.
46
46
47 :raises `CommitError`: if any error occurs while committing
47 :raises `CommitError`: if any error occurs while committing
48 """
48 """
49 self.check_integrity(parents)
49 self.check_integrity(parents)
50 if branch is None:
50 if branch is None:
51 branch = self.repository.DEFAULT_BRANCH_NAME
51 branch = self.repository.DEFAULT_BRANCH_NAME
52
52
53 ENCODING = "UTF-8"
53 ENCODING = "UTF-8"
54
54
55 commit_tree = None
55 commit_tree = None
56 if self.parents[0]:
56 if self.parents[0]:
57 commit_tree = self.parents[0]._commit['tree']
57 commit_tree = self.parents[0]._commit['tree']
58
58
59 updated = []
59 updated = []
60 for node in self.added + self.changed:
60 for node in self.added + self.changed:
61 if not node.is_binary:
61
62 if node.is_binary:
63 content = node.content
64 else:
62 content = node.content.encode(ENCODING)
65 content = node.content.encode(ENCODING)
63 else:
66
64 content = node.content
65 updated.append({
67 updated.append({
66 'path': node.path,
68 'path': node.path,
67 'node_path': node.name.encode(ENCODING),
69 'node_path': node.name.encode(ENCODING),
68 'content': content,
70 'content': content,
69 'mode': node.mode,
71 'mode': node.mode,
70 })
72 })
71
73
72 removed = [node.path for node in self.removed]
74 removed = [node.path for node in self.removed]
73
75
74 date, tz = date_to_timestamp_plus_offset(date)
76 date, tz = date_to_timestamp_plus_offset(date)
75
77
76 # TODO: johbo: Make kwargs explicit and check if this is needed.
78 # TODO: johbo: Make kwargs explicit and check if this is needed.
77 author_time = kwargs.pop('author_time', date)
79 author_time = kwargs.pop('author_time', date)
78 author_tz = kwargs.pop('author_timezone', tz)
80 author_tz = kwargs.pop('author_timezone', tz)
79
81
80 commit_data = {
82 commit_data = {
81 'parents': [p._commit['id'] for p in self.parents if p],
83 'parents': [p._commit['id'] for p in self.parents if p],
82 'author': safe_str(author),
84 'author': safe_str(author),
83 'committer': safe_str(author),
85 'committer': safe_str(author),
84 'encoding': ENCODING,
86 'encoding': ENCODING,
85 'message': safe_str(message),
87 'message': safe_str(message),
86 'commit_time': int(date),
88 'commit_time': int(date),
87 'author_time': int(author_time),
89 'author_time': int(author_time),
88 'commit_timezone': tz,
90 'commit_timezone': tz,
89 'author_timezone': author_tz,
91 'author_timezone': author_tz,
90 }
92 }
91
93
92 commit_id = self.repository._remote.commit(
94 commit_id = self.repository._remote.commit(
93 commit_data, branch, commit_tree, updated, removed)
95 commit_data, branch, commit_tree, updated, removed)
94
96
95 # Update vcs repository object
97 # Update vcs repository object
96 self.repository.append_commit_id(commit_id)
98 self.repository.append_commit_id(commit_id)
97
99
98 # invalidate parsed refs after commit
100 # invalidate parsed refs after commit
99 self.repository._refs = self.repository._get_refs()
101 self.repository._refs = self.repository._get_refs()
100 self.repository.branches = self.repository._get_branches()
102 self.repository.branches = self.repository._get_branches()
101 tip = self.repository.get_commit(commit_id)
103 tip = self.repository.get_commit(commit_id)
102
104
103 self.reset()
105 self.reset()
104 return tip
106 return tip
@@ -1,385 +1,389 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2014-2019 RhodeCode GmbH
3 # Copyright (C) 2014-2019 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 HG commit module
22 HG commit module
23 """
23 """
24
24
25 import os
25 import os
26
26
27 from zope.cachedescriptors.property import Lazy as LazyProperty
27 from zope.cachedescriptors.property import Lazy as LazyProperty
28
28
29 from rhodecode.lib.datelib import utcdate_fromtimestamp
29 from rhodecode.lib.datelib import utcdate_fromtimestamp
30 from rhodecode.lib.utils import safe_str, safe_unicode
30 from rhodecode.lib.utils import safe_str, safe_unicode
31 from rhodecode.lib.vcs import path as vcspath
31 from rhodecode.lib.vcs import path as vcspath
32 from rhodecode.lib.vcs.backends import base
32 from rhodecode.lib.vcs.backends import base
33 from rhodecode.lib.vcs.backends.hg.diff import MercurialDiff
33 from rhodecode.lib.vcs.backends.hg.diff import MercurialDiff
34 from rhodecode.lib.vcs.exceptions import CommitError
34 from rhodecode.lib.vcs.exceptions import CommitError
35 from rhodecode.lib.vcs.nodes import (
35 from rhodecode.lib.vcs.nodes import (
36 AddedFileNodesGenerator, ChangedFileNodesGenerator, DirNode, FileNode,
36 AddedFileNodesGenerator, ChangedFileNodesGenerator, DirNode, FileNode,
37 NodeKind, RemovedFileNodesGenerator, RootNode, SubModuleNode,
37 NodeKind, RemovedFileNodesGenerator, RootNode, SubModuleNode,
38 LargeFileNode, LARGEFILE_PREFIX)
38 LargeFileNode, LARGEFILE_PREFIX)
39 from rhodecode.lib.vcs.utils.paths import get_dirs_for_path
39 from rhodecode.lib.vcs.utils.paths import get_dirs_for_path
40
40
41
41
42 class MercurialCommit(base.BaseCommit):
42 class MercurialCommit(base.BaseCommit):
43 """
43 """
44 Represents state of the repository at the single commit.
44 Represents state of the repository at the single commit.
45 """
45 """
46
46
47 _filter_pre_load = [
47 _filter_pre_load = [
48 # git specific property not supported here
48 # git specific property not supported here
49 "_commit",
49 "_commit",
50 ]
50 ]
51
51
52 def __init__(self, repository, raw_id, idx, pre_load=None):
52 def __init__(self, repository, raw_id, idx, pre_load=None):
53 raw_id = safe_str(raw_id)
53 raw_id = safe_str(raw_id)
54
54
55 self.repository = repository
55 self.repository = repository
56 self._remote = repository._remote
56 self._remote = repository._remote
57
57
58 self.raw_id = raw_id
58 self.raw_id = raw_id
59 self.idx = idx
59 self.idx = idx
60
60
61 self._set_bulk_properties(pre_load)
61 self._set_bulk_properties(pre_load)
62
62
63 # caches
63 # caches
64 self.nodes = {}
64 self.nodes = {}
65
65
66 def _set_bulk_properties(self, pre_load):
66 def _set_bulk_properties(self, pre_load):
67 if not pre_load:
67 if not pre_load:
68 return
68 return
69 pre_load = [entry for entry in pre_load
69 pre_load = [entry for entry in pre_load
70 if entry not in self._filter_pre_load]
70 if entry not in self._filter_pre_load]
71 if not pre_load:
71 if not pre_load:
72 return
72 return
73
73
74 result = self._remote.bulk_request(self.raw_id, pre_load)
74 result = self._remote.bulk_request(self.raw_id, pre_load)
75 for attr, value in result.items():
75 for attr, value in result.items():
76 if attr in ["author", "branch", "message"]:
76 if attr in ["author", "branch", "message"]:
77 value = safe_unicode(value)
77 value = safe_unicode(value)
78 elif attr == "affected_files":
78 elif attr == "affected_files":
79 value = map(safe_unicode, value)
79 value = map(safe_unicode, value)
80 elif attr == "date":
80 elif attr == "date":
81 value = utcdate_fromtimestamp(*value)
81 value = utcdate_fromtimestamp(*value)
82 elif attr in ["children", "parents"]:
82 elif attr in ["children", "parents"]:
83 value = self._make_commits(value)
83 value = self._make_commits(value)
84 elif attr in ["phase"]:
84 elif attr in ["phase"]:
85 value = self._get_phase_text(value)
85 value = self._get_phase_text(value)
86 self.__dict__[attr] = value
86 self.__dict__[attr] = value
87
87
88 @LazyProperty
88 @LazyProperty
89 def tags(self):
89 def tags(self):
90 tags = [name for name, commit_id in self.repository.tags.iteritems()
90 tags = [name for name, commit_id in self.repository.tags.iteritems()
91 if commit_id == self.raw_id]
91 if commit_id == self.raw_id]
92 return tags
92 return tags
93
93
94 @LazyProperty
94 @LazyProperty
95 def branch(self):
95 def branch(self):
96 return safe_unicode(self._remote.ctx_branch(self.raw_id))
96 return safe_unicode(self._remote.ctx_branch(self.raw_id))
97
97
98 @LazyProperty
98 @LazyProperty
99 def bookmarks(self):
99 def bookmarks(self):
100 bookmarks = [
100 bookmarks = [
101 name for name, commit_id in self.repository.bookmarks.iteritems()
101 name for name, commit_id in self.repository.bookmarks.iteritems()
102 if commit_id == self.raw_id]
102 if commit_id == self.raw_id]
103 return bookmarks
103 return bookmarks
104
104
105 @LazyProperty
105 @LazyProperty
106 def message(self):
106 def message(self):
107 return safe_unicode(self._remote.ctx_description(self.raw_id))
107 return safe_unicode(self._remote.ctx_description(self.raw_id))
108
108
109 @LazyProperty
109 @LazyProperty
110 def committer(self):
110 def committer(self):
111 return safe_unicode(self.author)
111 return safe_unicode(self.author)
112
112
113 @LazyProperty
113 @LazyProperty
114 def author(self):
114 def author(self):
115 return safe_unicode(self._remote.ctx_user(self.raw_id))
115 return safe_unicode(self._remote.ctx_user(self.raw_id))
116
116
117 @LazyProperty
117 @LazyProperty
118 def date(self):
118 def date(self):
119 return utcdate_fromtimestamp(*self._remote.ctx_date(self.raw_id))
119 return utcdate_fromtimestamp(*self._remote.ctx_date(self.raw_id))
120
120
121 @LazyProperty
121 @LazyProperty
122 def status(self):
122 def status(self):
123 """
123 """
124 Returns modified, added, removed, deleted files for current commit
124 Returns modified, added, removed, deleted files for current commit
125 """
125 """
126 return self._remote.ctx_status(self.raw_id)
126 return self._remote.ctx_status(self.raw_id)
127
127
128 @LazyProperty
128 @LazyProperty
129 def _file_paths(self):
129 def _file_paths(self):
130 return self._remote.ctx_list(self.raw_id)
130 return self._remote.ctx_list(self.raw_id)
131
131
132 @LazyProperty
132 @LazyProperty
133 def _dir_paths(self):
133 def _dir_paths(self):
134 p = list(set(get_dirs_for_path(*self._file_paths)))
134 p = list(set(get_dirs_for_path(*self._file_paths)))
135 p.insert(0, '')
135 p.insert(0, '')
136 return p
136 return p
137
137
138 @LazyProperty
138 @LazyProperty
139 def _paths(self):
139 def _paths(self):
140 return self._dir_paths + self._file_paths
140 return self._dir_paths + self._file_paths
141
141
142 @LazyProperty
142 @LazyProperty
143 def id(self):
143 def id(self):
144 if self.last:
144 if self.last:
145 return u'tip'
145 return u'tip'
146 return self.short_id
146 return self.short_id
147
147
148 @LazyProperty
148 @LazyProperty
149 def short_id(self):
149 def short_id(self):
150 return self.raw_id[:12]
150 return self.raw_id[:12]
151
151
152 def _make_commits(self, indexes, pre_load=None):
152 def _make_commits(self, indexes, pre_load=None):
153 return [self.repository.get_commit(commit_idx=idx, pre_load=pre_load)
153 return [self.repository.get_commit(commit_idx=idx, pre_load=pre_load)
154 for idx in indexes if idx >= 0]
154 for idx in indexes if idx >= 0]
155
155
156 @LazyProperty
156 @LazyProperty
157 def parents(self):
157 def parents(self):
158 """
158 """
159 Returns list of parent commits.
159 Returns list of parent commits.
160 """
160 """
161 parents = self._remote.ctx_parents(self.raw_id)
161 parents = self._remote.ctx_parents(self.raw_id)
162 return self._make_commits(parents)
162 return self._make_commits(parents)
163
163
164 def _get_phase_text(self, phase_id):
164 def _get_phase_text(self, phase_id):
165 return {
165 return {
166 0: 'public',
166 0: 'public',
167 1: 'draft',
167 1: 'draft',
168 2: 'secret',
168 2: 'secret',
169 }.get(phase_id) or ''
169 }.get(phase_id) or ''
170
170
171 @LazyProperty
171 @LazyProperty
172 def phase(self):
172 def phase(self):
173 phase_id = self._remote.ctx_phase(self.raw_id)
173 phase_id = self._remote.ctx_phase(self.raw_id)
174 phase_text = self._get_phase_text(phase_id)
174 phase_text = self._get_phase_text(phase_id)
175
175
176 return safe_unicode(phase_text)
176 return safe_unicode(phase_text)
177
177
178 @LazyProperty
178 @LazyProperty
179 def obsolete(self):
179 def obsolete(self):
180 obsolete = self._remote.ctx_obsolete(self.raw_id)
180 obsolete = self._remote.ctx_obsolete(self.raw_id)
181 return obsolete
181 return obsolete
182
182
183 @LazyProperty
183 @LazyProperty
184 def hidden(self):
184 def hidden(self):
185 hidden = self._remote.ctx_hidden(self.raw_id)
185 hidden = self._remote.ctx_hidden(self.raw_id)
186 return hidden
186 return hidden
187
187
188 @LazyProperty
188 @LazyProperty
189 def children(self):
189 def children(self):
190 """
190 """
191 Returns list of child commits.
191 Returns list of child commits.
192 """
192 """
193 children = self._remote.ctx_children(self.raw_id)
193 children = self._remote.ctx_children(self.raw_id)
194 return self._make_commits(children)
194 return self._make_commits(children)
195
195
196 def _fix_path(self, path):
196 def _fix_path(self, path):
197 """
197 """
198 Mercurial keeps filenodes as str so we need to encode from unicode
198 Mercurial keeps filenodes as str so we need to encode from unicode
199 to str.
199 to str.
200 """
200 """
201 return safe_str(super(MercurialCommit, self)._fix_path(path))
201 return safe_str(super(MercurialCommit, self)._fix_path(path))
202
202
203 def _get_kind(self, path):
203 def _get_kind(self, path):
204 path = self._fix_path(path)
204 path = self._fix_path(path)
205 if path in self._file_paths:
205 if path in self._file_paths:
206 return NodeKind.FILE
206 return NodeKind.FILE
207 elif path in self._dir_paths:
207 elif path in self._dir_paths:
208 return NodeKind.DIR
208 return NodeKind.DIR
209 else:
209 else:
210 raise CommitError(
210 raise CommitError(
211 "Node does not exist at the given path '%s'" % (path, ))
211 "Node does not exist at the given path '%s'" % (path, ))
212
212
213 def _get_filectx(self, path):
213 def _get_filectx(self, path):
214 path = self._fix_path(path)
214 path = self._fix_path(path)
215 if self._get_kind(path) != NodeKind.FILE:
215 if self._get_kind(path) != NodeKind.FILE:
216 raise CommitError(
216 raise CommitError(
217 "File does not exist for idx %s at '%s'" % (self.raw_id, path))
217 "File does not exist for idx %s at '%s'" % (self.raw_id, path))
218 return path
218 return path
219
219
220 def get_file_mode(self, path):
220 def get_file_mode(self, path):
221 """
221 """
222 Returns stat mode of the file at the given ``path``.
222 Returns stat mode of the file at the given ``path``.
223 """
223 """
224 path = self._get_filectx(path)
224 path = self._get_filectx(path)
225 if 'x' in self._remote.fctx_flags(self.raw_id, path):
225 if 'x' in self._remote.fctx_flags(self.raw_id, path):
226 return base.FILEMODE_EXECUTABLE
226 return base.FILEMODE_EXECUTABLE
227 else:
227 else:
228 return base.FILEMODE_DEFAULT
228 return base.FILEMODE_DEFAULT
229
229
230 def is_link(self, path):
230 def is_link(self, path):
231 path = self._get_filectx(path)
231 path = self._get_filectx(path)
232 return 'l' in self._remote.fctx_flags(self.raw_id, path)
232 return 'l' in self._remote.fctx_flags(self.raw_id, path)
233
233
234 def is_node_binary(self, path):
235 path = self._get_filectx(path)
236 return self._remote.is_binary(self.raw_id, path)
237
234 def get_file_content(self, path):
238 def get_file_content(self, path):
235 """
239 """
236 Returns content of the file at given ``path``.
240 Returns content of the file at given ``path``.
237 """
241 """
238 path = self._get_filectx(path)
242 path = self._get_filectx(path)
239 return self._remote.fctx_node_data(self.raw_id, path)
243 return self._remote.fctx_node_data(self.raw_id, path)
240
244
241 def get_file_content_streamed(self, path):
245 def get_file_content_streamed(self, path):
242 path = self._get_filectx(path)
246 path = self._get_filectx(path)
243 stream_method = getattr(self._remote, 'stream:fctx_node_data')
247 stream_method = getattr(self._remote, 'stream:fctx_node_data')
244 return stream_method(self.raw_id, path)
248 return stream_method(self.raw_id, path)
245
249
246 def get_file_size(self, path):
250 def get_file_size(self, path):
247 """
251 """
248 Returns size of the file at given ``path``.
252 Returns size of the file at given ``path``.
249 """
253 """
250 path = self._get_filectx(path)
254 path = self._get_filectx(path)
251 return self._remote.fctx_size(self.raw_id, path)
255 return self._remote.fctx_size(self.raw_id, path)
252
256
253 def get_path_history(self, path, limit=None, pre_load=None):
257 def get_path_history(self, path, limit=None, pre_load=None):
254 """
258 """
255 Returns history of file as reversed list of `MercurialCommit` objects
259 Returns history of file as reversed list of `MercurialCommit` objects
256 for which file at given ``path`` has been modified.
260 for which file at given ``path`` has been modified.
257 """
261 """
258 path = self._get_filectx(path)
262 path = self._get_filectx(path)
259 hist = self._remote.node_history(self.raw_id, path, limit)
263 hist = self._remote.node_history(self.raw_id, path, limit)
260 return [
264 return [
261 self.repository.get_commit(commit_id=commit_id, pre_load=pre_load)
265 self.repository.get_commit(commit_id=commit_id, pre_load=pre_load)
262 for commit_id in hist]
266 for commit_id in hist]
263
267
264 def get_file_annotate(self, path, pre_load=None):
268 def get_file_annotate(self, path, pre_load=None):
265 """
269 """
266 Returns a generator of four element tuples with
270 Returns a generator of four element tuples with
267 lineno, commit_id, commit lazy loader and line
271 lineno, commit_id, commit lazy loader and line
268 """
272 """
269 result = self._remote.fctx_annotate(self.raw_id, path)
273 result = self._remote.fctx_annotate(self.raw_id, path)
270
274
271 for ln_no, commit_id, content in result:
275 for ln_no, commit_id, content in result:
272 yield (
276 yield (
273 ln_no, commit_id,
277 ln_no, commit_id,
274 lambda: self.repository.get_commit(commit_id=commit_id, pre_load=pre_load),
278 lambda: self.repository.get_commit(commit_id=commit_id, pre_load=pre_load),
275 content)
279 content)
276
280
277 def get_nodes(self, path):
281 def get_nodes(self, path):
278 """
282 """
279 Returns combined ``DirNode`` and ``FileNode`` objects list representing
283 Returns combined ``DirNode`` and ``FileNode`` objects list representing
280 state of commit at the given ``path``. If node at the given ``path``
284 state of commit at the given ``path``. If node at the given ``path``
281 is not instance of ``DirNode``, CommitError would be raised.
285 is not instance of ``DirNode``, CommitError would be raised.
282 """
286 """
283
287
284 if self._get_kind(path) != NodeKind.DIR:
288 if self._get_kind(path) != NodeKind.DIR:
285 raise CommitError(
289 raise CommitError(
286 "Directory does not exist for idx %s at '%s'" % (self.raw_id, path))
290 "Directory does not exist for idx %s at '%s'" % (self.raw_id, path))
287 path = self._fix_path(path)
291 path = self._fix_path(path)
288
292
289 filenodes = [
293 filenodes = [
290 FileNode(f, commit=self) for f in self._file_paths
294 FileNode(f, commit=self) for f in self._file_paths
291 if os.path.dirname(f) == path]
295 if os.path.dirname(f) == path]
292 # TODO: johbo: Check if this can be done in a more obvious way
296 # TODO: johbo: Check if this can be done in a more obvious way
293 dirs = path == '' and '' or [
297 dirs = path == '' and '' or [
294 d for d in self._dir_paths
298 d for d in self._dir_paths
295 if d and vcspath.dirname(d) == path]
299 if d and vcspath.dirname(d) == path]
296 dirnodes = [
300 dirnodes = [
297 DirNode(d, commit=self) for d in dirs
301 DirNode(d, commit=self) for d in dirs
298 if os.path.dirname(d) == path]
302 if os.path.dirname(d) == path]
299
303
300 alias = self.repository.alias
304 alias = self.repository.alias
301 for k, vals in self._submodules.iteritems():
305 for k, vals in self._submodules.iteritems():
302 if vcspath.dirname(k) == path:
306 if vcspath.dirname(k) == path:
303 loc = vals[0]
307 loc = vals[0]
304 commit = vals[1]
308 commit = vals[1]
305 dirnodes.append(SubModuleNode(k, url=loc, commit=commit, alias=alias))
309 dirnodes.append(SubModuleNode(k, url=loc, commit=commit, alias=alias))
306
310
307 nodes = dirnodes + filenodes
311 nodes = dirnodes + filenodes
308 for node in nodes:
312 for node in nodes:
309 if node.path not in self.nodes:
313 if node.path not in self.nodes:
310 self.nodes[node.path] = node
314 self.nodes[node.path] = node
311 nodes.sort()
315 nodes.sort()
312
316
313 return nodes
317 return nodes
314
318
315 def get_node(self, path, pre_load=None):
319 def get_node(self, path, pre_load=None):
316 """
320 """
317 Returns `Node` object from the given `path`. If there is no node at
321 Returns `Node` object from the given `path`. If there is no node at
318 the given `path`, `NodeDoesNotExistError` would be raised.
322 the given `path`, `NodeDoesNotExistError` would be raised.
319 """
323 """
320 path = self._fix_path(path)
324 path = self._fix_path(path)
321
325
322 if path not in self.nodes:
326 if path not in self.nodes:
323 if path in self._file_paths:
327 if path in self._file_paths:
324 node = FileNode(path, commit=self, pre_load=pre_load)
328 node = FileNode(path, commit=self, pre_load=pre_load)
325 elif path in self._dir_paths:
329 elif path in self._dir_paths:
326 if path == '':
330 if path == '':
327 node = RootNode(commit=self)
331 node = RootNode(commit=self)
328 else:
332 else:
329 node = DirNode(path, commit=self)
333 node = DirNode(path, commit=self)
330 else:
334 else:
331 raise self.no_node_at_path(path)
335 raise self.no_node_at_path(path)
332
336
333 # cache node
337 # cache node
334 self.nodes[path] = node
338 self.nodes[path] = node
335 return self.nodes[path]
339 return self.nodes[path]
336
340
337 def get_largefile_node(self, path):
341 def get_largefile_node(self, path):
338 pointer_spec = self._remote.is_large_file(path)
342 pointer_spec = self._remote.is_large_file(path)
339 if pointer_spec:
343 if pointer_spec:
340 # content of that file regular FileNode is the hash of largefile
344 # content of that file regular FileNode is the hash of largefile
341 file_id = self.get_file_content(path).strip()
345 file_id = self.get_file_content(path).strip()
342
346
343 if self._remote.in_largefiles_store(file_id):
347 if self._remote.in_largefiles_store(file_id):
344 lf_path = self._remote.store_path(file_id)
348 lf_path = self._remote.store_path(file_id)
345 return LargeFileNode(lf_path, commit=self, org_path=path)
349 return LargeFileNode(lf_path, commit=self, org_path=path)
346 elif self._remote.in_user_cache(file_id):
350 elif self._remote.in_user_cache(file_id):
347 lf_path = self._remote.store_path(file_id)
351 lf_path = self._remote.store_path(file_id)
348 self._remote.link(file_id, path)
352 self._remote.link(file_id, path)
349 return LargeFileNode(lf_path, commit=self, org_path=path)
353 return LargeFileNode(lf_path, commit=self, org_path=path)
350
354
351 @LazyProperty
355 @LazyProperty
352 def _submodules(self):
356 def _submodules(self):
353 """
357 """
354 Returns a dictionary with submodule information from substate file
358 Returns a dictionary with submodule information from substate file
355 of hg repository.
359 of hg repository.
356 """
360 """
357 return self._remote.ctx_substate(self.raw_id)
361 return self._remote.ctx_substate(self.raw_id)
358
362
359 @LazyProperty
363 @LazyProperty
360 def affected_files(self):
364 def affected_files(self):
361 """
365 """
362 Gets a fast accessible file changes for given commit
366 Gets a fast accessible file changes for given commit
363 """
367 """
364 return self._remote.ctx_files(self.raw_id)
368 return self._remote.ctx_files(self.raw_id)
365
369
366 @property
370 @property
367 def added(self):
371 def added(self):
368 """
372 """
369 Returns list of added ``FileNode`` objects.
373 Returns list of added ``FileNode`` objects.
370 """
374 """
371 return AddedFileNodesGenerator([n for n in self.status[1]], self)
375 return AddedFileNodesGenerator([n for n in self.status[1]], self)
372
376
373 @property
377 @property
374 def changed(self):
378 def changed(self):
375 """
379 """
376 Returns list of modified ``FileNode`` objects.
380 Returns list of modified ``FileNode`` objects.
377 """
381 """
378 return ChangedFileNodesGenerator([n for n in self.status[0]], self)
382 return ChangedFileNodesGenerator([n for n in self.status[0]], self)
379
383
380 @property
384 @property
381 def removed(self):
385 def removed(self):
382 """
386 """
383 Returns list of removed ``FileNode`` objects.
387 Returns list of removed ``FileNode`` objects.
384 """
388 """
385 return RemovedFileNodesGenerator([n for n in self.status[2]], self)
389 return RemovedFileNodesGenerator([n for n in self.status[2]], self)
@@ -1,241 +1,245 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2014-2019 RhodeCode GmbH
3 # Copyright (C) 2014-2019 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 SVN commit module
22 SVN commit module
23 """
23 """
24
24
25
25
26 import dateutil.parser
26 import dateutil.parser
27 from zope.cachedescriptors.property import Lazy as LazyProperty
27 from zope.cachedescriptors.property import Lazy as LazyProperty
28
28
29 from rhodecode.lib.utils import safe_str, safe_unicode
29 from rhodecode.lib.utils import safe_str, safe_unicode
30 from rhodecode.lib.vcs import nodes, path as vcspath
30 from rhodecode.lib.vcs import nodes, path as vcspath
31 from rhodecode.lib.vcs.backends import base
31 from rhodecode.lib.vcs.backends import base
32 from rhodecode.lib.vcs.exceptions import CommitError, NodeDoesNotExistError
32 from rhodecode.lib.vcs.exceptions import CommitError, NodeDoesNotExistError
33
33
34
34
35 _SVN_PROP_TRUE = '*'
35 _SVN_PROP_TRUE = '*'
36
36
37
37
38 class SubversionCommit(base.BaseCommit):
38 class SubversionCommit(base.BaseCommit):
39 """
39 """
40 Subversion specific implementation of commits
40 Subversion specific implementation of commits
41
41
42 .. attribute:: branch
42 .. attribute:: branch
43
43
44 The Subversion backend does not support to assign branches to
44 The Subversion backend does not support to assign branches to
45 specific commits. This attribute has always the value `None`.
45 specific commits. This attribute has always the value `None`.
46
46
47 """
47 """
48
48
49 def __init__(self, repository, commit_id):
49 def __init__(self, repository, commit_id):
50 self.repository = repository
50 self.repository = repository
51 self.idx = self.repository._get_commit_idx(commit_id)
51 self.idx = self.repository._get_commit_idx(commit_id)
52 self._svn_rev = self.idx + 1
52 self._svn_rev = self.idx + 1
53 self._remote = repository._remote
53 self._remote = repository._remote
54 # TODO: handling of raw_id should be a method on repository itself,
54 # TODO: handling of raw_id should be a method on repository itself,
55 # which knows how to translate commit index and commit id
55 # which knows how to translate commit index and commit id
56 self.raw_id = commit_id
56 self.raw_id = commit_id
57 self.short_id = commit_id
57 self.short_id = commit_id
58 self.id = 'r%s' % (commit_id, )
58 self.id = 'r%s' % (commit_id, )
59
59
60 # TODO: Implement the following placeholder attributes
60 # TODO: Implement the following placeholder attributes
61 self.nodes = {}
61 self.nodes = {}
62 self.tags = []
62 self.tags = []
63
63
64 @property
64 @property
65 def author(self):
65 def author(self):
66 return safe_unicode(self._properties.get('svn:author'))
66 return safe_unicode(self._properties.get('svn:author'))
67
67
68 @property
68 @property
69 def date(self):
69 def date(self):
70 return _date_from_svn_properties(self._properties)
70 return _date_from_svn_properties(self._properties)
71
71
72 @property
72 @property
73 def message(self):
73 def message(self):
74 return safe_unicode(self._properties.get('svn:log'))
74 return safe_unicode(self._properties.get('svn:log'))
75
75
76 @LazyProperty
76 @LazyProperty
77 def _properties(self):
77 def _properties(self):
78 return self._remote.revision_properties(self._svn_rev)
78 return self._remote.revision_properties(self._svn_rev)
79
79
80 @LazyProperty
80 @LazyProperty
81 def parents(self):
81 def parents(self):
82 parent_idx = self.idx - 1
82 parent_idx = self.idx - 1
83 if parent_idx >= 0:
83 if parent_idx >= 0:
84 parent = self.repository.get_commit(commit_idx=parent_idx)
84 parent = self.repository.get_commit(commit_idx=parent_idx)
85 return [parent]
85 return [parent]
86 return []
86 return []
87
87
88 @LazyProperty
88 @LazyProperty
89 def children(self):
89 def children(self):
90 child_idx = self.idx + 1
90 child_idx = self.idx + 1
91 if child_idx < len(self.repository.commit_ids):
91 if child_idx < len(self.repository.commit_ids):
92 child = self.repository.get_commit(commit_idx=child_idx)
92 child = self.repository.get_commit(commit_idx=child_idx)
93 return [child]
93 return [child]
94 return []
94 return []
95
95
96 def get_file_mode(self, path):
96 def get_file_mode(self, path):
97 # Note: Subversion flags files which are executable with a special
97 # Note: Subversion flags files which are executable with a special
98 # property `svn:executable` which is set to the value ``"*"``.
98 # property `svn:executable` which is set to the value ``"*"``.
99 if self._get_file_property(path, 'svn:executable') == _SVN_PROP_TRUE:
99 if self._get_file_property(path, 'svn:executable') == _SVN_PROP_TRUE:
100 return base.FILEMODE_EXECUTABLE
100 return base.FILEMODE_EXECUTABLE
101 else:
101 else:
102 return base.FILEMODE_DEFAULT
102 return base.FILEMODE_DEFAULT
103
103
104 def is_link(self, path):
104 def is_link(self, path):
105 # Note: Subversion has a flag for special files, the content of the
105 # Note: Subversion has a flag for special files, the content of the
106 # file contains the type of that file.
106 # file contains the type of that file.
107 if self._get_file_property(path, 'svn:special') == _SVN_PROP_TRUE:
107 if self._get_file_property(path, 'svn:special') == _SVN_PROP_TRUE:
108 return self.get_file_content(path).startswith('link')
108 return self.get_file_content(path).startswith('link')
109 return False
109 return False
110
110
111 def is_node_binary(self, path):
112 path = self._fix_path(path)
113 return self._remote.is_binary(self._svn_rev, safe_str(path))
114
111 def _get_file_property(self, path, name):
115 def _get_file_property(self, path, name):
112 file_properties = self._remote.node_properties(
116 file_properties = self._remote.node_properties(
113 safe_str(path), self._svn_rev)
117 safe_str(path), self._svn_rev)
114 return file_properties.get(name)
118 return file_properties.get(name)
115
119
116 def get_file_content(self, path):
120 def get_file_content(self, path):
117 path = self._fix_path(path)
121 path = self._fix_path(path)
118 return self._remote.get_file_content(safe_str(path), self._svn_rev)
122 return self._remote.get_file_content(safe_str(path), self._svn_rev)
119
123
120 def get_file_content_streamed(self, path):
124 def get_file_content_streamed(self, path):
121 path = self._fix_path(path)
125 path = self._fix_path(path)
122 stream_method = getattr(self._remote, 'stream:get_file_content')
126 stream_method = getattr(self._remote, 'stream:get_file_content')
123 return stream_method(safe_str(path), self._svn_rev)
127 return stream_method(safe_str(path), self._svn_rev)
124
128
125 def get_file_size(self, path):
129 def get_file_size(self, path):
126 path = self._fix_path(path)
130 path = self._fix_path(path)
127 return self._remote.get_file_size(safe_str(path), self._svn_rev)
131 return self._remote.get_file_size(safe_str(path), self._svn_rev)
128
132
129 def get_path_history(self, path, limit=None, pre_load=None):
133 def get_path_history(self, path, limit=None, pre_load=None):
130 path = safe_str(self._fix_path(path))
134 path = safe_str(self._fix_path(path))
131 history = self._remote.node_history(path, self._svn_rev, limit)
135 history = self._remote.node_history(path, self._svn_rev, limit)
132 return [
136 return [
133 self.repository.get_commit(commit_id=str(svn_rev))
137 self.repository.get_commit(commit_id=str(svn_rev))
134 for svn_rev in history]
138 for svn_rev in history]
135
139
136 def get_file_annotate(self, path, pre_load=None):
140 def get_file_annotate(self, path, pre_load=None):
137 result = self._remote.file_annotate(safe_str(path), self._svn_rev)
141 result = self._remote.file_annotate(safe_str(path), self._svn_rev)
138
142
139 for zero_based_line_no, svn_rev, content in result:
143 for zero_based_line_no, svn_rev, content in result:
140 commit_id = str(svn_rev)
144 commit_id = str(svn_rev)
141 line_no = zero_based_line_no + 1
145 line_no = zero_based_line_no + 1
142 yield (
146 yield (
143 line_no,
147 line_no,
144 commit_id,
148 commit_id,
145 lambda: self.repository.get_commit(commit_id=commit_id),
149 lambda: self.repository.get_commit(commit_id=commit_id),
146 content)
150 content)
147
151
148 def get_node(self, path, pre_load=None):
152 def get_node(self, path, pre_load=None):
149 path = self._fix_path(path)
153 path = self._fix_path(path)
150 if path not in self.nodes:
154 if path not in self.nodes:
151
155
152 if path == '':
156 if path == '':
153 node = nodes.RootNode(commit=self)
157 node = nodes.RootNode(commit=self)
154 else:
158 else:
155 node_type = self._remote.get_node_type(
159 node_type = self._remote.get_node_type(
156 safe_str(path), self._svn_rev)
160 safe_str(path), self._svn_rev)
157 if node_type == 'dir':
161 if node_type == 'dir':
158 node = nodes.DirNode(path, commit=self)
162 node = nodes.DirNode(path, commit=self)
159 elif node_type == 'file':
163 elif node_type == 'file':
160 node = nodes.FileNode(path, commit=self, pre_load=pre_load)
164 node = nodes.FileNode(path, commit=self, pre_load=pre_load)
161 else:
165 else:
162 raise self.no_node_at_path(path)
166 raise self.no_node_at_path(path)
163
167
164 self.nodes[path] = node
168 self.nodes[path] = node
165 return self.nodes[path]
169 return self.nodes[path]
166
170
167 def get_nodes(self, path):
171 def get_nodes(self, path):
168 if self._get_kind(path) != nodes.NodeKind.DIR:
172 if self._get_kind(path) != nodes.NodeKind.DIR:
169 raise CommitError(
173 raise CommitError(
170 "Directory does not exist for commit %s at "
174 "Directory does not exist for commit %s at "
171 " '%s'" % (self.raw_id, path))
175 " '%s'" % (self.raw_id, path))
172 path = self._fix_path(path)
176 path = self._fix_path(path)
173
177
174 path_nodes = []
178 path_nodes = []
175 for name, kind in self._remote.get_nodes(
179 for name, kind in self._remote.get_nodes(
176 safe_str(path), revision=self._svn_rev):
180 safe_str(path), revision=self._svn_rev):
177 node_path = vcspath.join(path, name)
181 node_path = vcspath.join(path, name)
178 if kind == 'dir':
182 if kind == 'dir':
179 node = nodes.DirNode(node_path, commit=self)
183 node = nodes.DirNode(node_path, commit=self)
180 elif kind == 'file':
184 elif kind == 'file':
181 node = nodes.FileNode(node_path, commit=self)
185 node = nodes.FileNode(node_path, commit=self)
182 else:
186 else:
183 raise ValueError("Node kind %s not supported." % (kind, ))
187 raise ValueError("Node kind %s not supported." % (kind, ))
184 self.nodes[node_path] = node
188 self.nodes[node_path] = node
185 path_nodes.append(node)
189 path_nodes.append(node)
186
190
187 return path_nodes
191 return path_nodes
188
192
189 def _get_kind(self, path):
193 def _get_kind(self, path):
190 path = self._fix_path(path)
194 path = self._fix_path(path)
191 kind = self._remote.get_node_type(path, self._svn_rev)
195 kind = self._remote.get_node_type(path, self._svn_rev)
192 if kind == 'file':
196 if kind == 'file':
193 return nodes.NodeKind.FILE
197 return nodes.NodeKind.FILE
194 elif kind == 'dir':
198 elif kind == 'dir':
195 return nodes.NodeKind.DIR
199 return nodes.NodeKind.DIR
196 else:
200 else:
197 raise CommitError(
201 raise CommitError(
198 "Node does not exist at the given path '%s'" % (path, ))
202 "Node does not exist at the given path '%s'" % (path, ))
199
203
200 @LazyProperty
204 @LazyProperty
201 def _changes_cache(self):
205 def _changes_cache(self):
202 return self._remote.revision_changes(self._svn_rev)
206 return self._remote.revision_changes(self._svn_rev)
203
207
204 @LazyProperty
208 @LazyProperty
205 def affected_files(self):
209 def affected_files(self):
206 changed_files = set()
210 changed_files = set()
207 for files in self._changes_cache.itervalues():
211 for files in self._changes_cache.itervalues():
208 changed_files.update(files)
212 changed_files.update(files)
209 return list(changed_files)
213 return list(changed_files)
210
214
211 @LazyProperty
215 @LazyProperty
212 def id(self):
216 def id(self):
213 return self.raw_id
217 return self.raw_id
214
218
215 @property
219 @property
216 def added(self):
220 def added(self):
217 return nodes.AddedFileNodesGenerator(
221 return nodes.AddedFileNodesGenerator(
218 self._changes_cache['added'], self)
222 self._changes_cache['added'], self)
219
223
220 @property
224 @property
221 def changed(self):
225 def changed(self):
222 return nodes.ChangedFileNodesGenerator(
226 return nodes.ChangedFileNodesGenerator(
223 self._changes_cache['changed'], self)
227 self._changes_cache['changed'], self)
224
228
225 @property
229 @property
226 def removed(self):
230 def removed(self):
227 return nodes.RemovedFileNodesGenerator(
231 return nodes.RemovedFileNodesGenerator(
228 self._changes_cache['removed'], self)
232 self._changes_cache['removed'], self)
229
233
230
234
231 def _date_from_svn_properties(properties):
235 def _date_from_svn_properties(properties):
232 """
236 """
233 Parses the date out of given svn properties.
237 Parses the date out of given svn properties.
234
238
235 :return: :class:`datetime.datetime` instance. The object is naive.
239 :return: :class:`datetime.datetime` instance. The object is naive.
236 """
240 """
237
241
238 aware_date = dateutil.parser.parse(properties.get('svn:date'))
242 aware_date = dateutil.parser.parse(properties.get('svn:date'))
239 # final_date = aware_date.astimezone(dateutil.tz.tzlocal())
243 # final_date = aware_date.astimezone(dateutil.tz.tzlocal())
240 final_date = aware_date
244 final_date = aware_date
241 return final_date.replace(tzinfo=None)
245 return final_date.replace(tzinfo=None)
@@ -1,870 +1,870 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2014-2019 RhodeCode GmbH
3 # Copyright (C) 2014-2019 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Module holding everything related to vcs nodes, with vcs2 architecture.
22 Module holding everything related to vcs nodes, with vcs2 architecture.
23 """
23 """
24
24
25 import os
25 import os
26 import stat
26 import stat
27
27
28 from zope.cachedescriptors.property import Lazy as LazyProperty
28 from zope.cachedescriptors.property import Lazy as LazyProperty
29
29
30 import rhodecode
30 import rhodecode
31 from rhodecode.config.conf import LANGUAGES_EXTENSIONS_MAP
31 from rhodecode.config.conf import LANGUAGES_EXTENSIONS_MAP
32 from rhodecode.lib.utils import safe_unicode, safe_str
32 from rhodecode.lib.utils import safe_unicode, safe_str
33 from rhodecode.lib.utils2 import md5
33 from rhodecode.lib.utils2 import md5
34 from rhodecode.lib.vcs import path as vcspath
34 from rhodecode.lib.vcs import path as vcspath
35 from rhodecode.lib.vcs.backends.base import EmptyCommit, FILEMODE_DEFAULT
35 from rhodecode.lib.vcs.backends.base import EmptyCommit, FILEMODE_DEFAULT
36 from rhodecode.lib.vcs.conf.mtypes import get_mimetypes_db
36 from rhodecode.lib.vcs.conf.mtypes import get_mimetypes_db
37 from rhodecode.lib.vcs.exceptions import NodeError, RemovedFileNodeError
37 from rhodecode.lib.vcs.exceptions import NodeError, RemovedFileNodeError
38
38
39 LARGEFILE_PREFIX = '.hglf'
39 LARGEFILE_PREFIX = '.hglf'
40
40
41
41
42 class NodeKind:
42 class NodeKind:
43 SUBMODULE = -1
43 SUBMODULE = -1
44 DIR = 1
44 DIR = 1
45 FILE = 2
45 FILE = 2
46 LARGEFILE = 3
46 LARGEFILE = 3
47
47
48
48
49 class NodeState:
49 class NodeState:
50 ADDED = u'added'
50 ADDED = u'added'
51 CHANGED = u'changed'
51 CHANGED = u'changed'
52 NOT_CHANGED = u'not changed'
52 NOT_CHANGED = u'not changed'
53 REMOVED = u'removed'
53 REMOVED = u'removed'
54
54
55
55
56 class NodeGeneratorBase(object):
56 class NodeGeneratorBase(object):
57 """
57 """
58 Base class for removed added and changed filenodes, it's a lazy generator
58 Base class for removed added and changed filenodes, it's a lazy generator
59 class that will create filenodes only on iteration or call
59 class that will create filenodes only on iteration or call
60
60
61 The len method doesn't need to create filenodes at all
61 The len method doesn't need to create filenodes at all
62 """
62 """
63
63
64 def __init__(self, current_paths, cs):
64 def __init__(self, current_paths, cs):
65 self.cs = cs
65 self.cs = cs
66 self.current_paths = current_paths
66 self.current_paths = current_paths
67
67
68 def __call__(self):
68 def __call__(self):
69 return [n for n in self]
69 return [n for n in self]
70
70
71 def __getslice__(self, i, j):
71 def __getslice__(self, i, j):
72 for p in self.current_paths[i:j]:
72 for p in self.current_paths[i:j]:
73 yield self.cs.get_node(p)
73 yield self.cs.get_node(p)
74
74
75 def __len__(self):
75 def __len__(self):
76 return len(self.current_paths)
76 return len(self.current_paths)
77
77
78 def __iter__(self):
78 def __iter__(self):
79 for p in self.current_paths:
79 for p in self.current_paths:
80 yield self.cs.get_node(p)
80 yield self.cs.get_node(p)
81
81
82
82
83 class AddedFileNodesGenerator(NodeGeneratorBase):
83 class AddedFileNodesGenerator(NodeGeneratorBase):
84 """
84 """
85 Class holding added files for current commit
85 Class holding added files for current commit
86 """
86 """
87
87
88
88
89 class ChangedFileNodesGenerator(NodeGeneratorBase):
89 class ChangedFileNodesGenerator(NodeGeneratorBase):
90 """
90 """
91 Class holding changed files for current commit
91 Class holding changed files for current commit
92 """
92 """
93
93
94
94
95 class RemovedFileNodesGenerator(NodeGeneratorBase):
95 class RemovedFileNodesGenerator(NodeGeneratorBase):
96 """
96 """
97 Class holding removed files for current commit
97 Class holding removed files for current commit
98 """
98 """
99 def __iter__(self):
99 def __iter__(self):
100 for p in self.current_paths:
100 for p in self.current_paths:
101 yield RemovedFileNode(path=p)
101 yield RemovedFileNode(path=p)
102
102
103 def __getslice__(self, i, j):
103 def __getslice__(self, i, j):
104 for p in self.current_paths[i:j]:
104 for p in self.current_paths[i:j]:
105 yield RemovedFileNode(path=p)
105 yield RemovedFileNode(path=p)
106
106
107
107
108 class Node(object):
108 class Node(object):
109 """
109 """
110 Simplest class representing file or directory on repository. SCM backends
110 Simplest class representing file or directory on repository. SCM backends
111 should use ``FileNode`` and ``DirNode`` subclasses rather than ``Node``
111 should use ``FileNode`` and ``DirNode`` subclasses rather than ``Node``
112 directly.
112 directly.
113
113
114 Node's ``path`` cannot start with slash as we operate on *relative* paths
114 Node's ``path`` cannot start with slash as we operate on *relative* paths
115 only. Moreover, every single node is identified by the ``path`` attribute,
115 only. Moreover, every single node is identified by the ``path`` attribute,
116 so it cannot end with slash, too. Otherwise, path could lead to mistakes.
116 so it cannot end with slash, too. Otherwise, path could lead to mistakes.
117 """
117 """
118 RTLO_MARKER = u"\u202E" # RTLO marker allows swapping text, and certain
118 RTLO_MARKER = u"\u202E" # RTLO marker allows swapping text, and certain
119 # security attacks could be used with this
119 # security attacks could be used with this
120 commit = None
120 commit = None
121
121
122 def __init__(self, path, kind):
122 def __init__(self, path, kind):
123 self._validate_path(path) # can throw exception if path is invalid
123 self._validate_path(path) # can throw exception if path is invalid
124 self.path = safe_str(path.rstrip('/')) # we store paths as str
124 self.path = safe_str(path.rstrip('/')) # we store paths as str
125 if path == '' and kind != NodeKind.DIR:
125 if path == '' and kind != NodeKind.DIR:
126 raise NodeError("Only DirNode and its subclasses may be "
126 raise NodeError("Only DirNode and its subclasses may be "
127 "initialized with empty path")
127 "initialized with empty path")
128 self.kind = kind
128 self.kind = kind
129
129
130 if self.is_root() and not self.is_dir():
130 if self.is_root() and not self.is_dir():
131 raise NodeError("Root node cannot be FILE kind")
131 raise NodeError("Root node cannot be FILE kind")
132
132
133 def _validate_path(self, path):
133 def _validate_path(self, path):
134 if path.startswith('/'):
134 if path.startswith('/'):
135 raise NodeError(
135 raise NodeError(
136 "Cannot initialize Node objects with slash at "
136 "Cannot initialize Node objects with slash at "
137 "the beginning as only relative paths are supported. "
137 "the beginning as only relative paths are supported. "
138 "Got %s" % (path,))
138 "Got %s" % (path,))
139
139
140 @LazyProperty
140 @LazyProperty
141 def parent(self):
141 def parent(self):
142 parent_path = self.get_parent_path()
142 parent_path = self.get_parent_path()
143 if parent_path:
143 if parent_path:
144 if self.commit:
144 if self.commit:
145 return self.commit.get_node(parent_path)
145 return self.commit.get_node(parent_path)
146 return DirNode(parent_path)
146 return DirNode(parent_path)
147 return None
147 return None
148
148
149 @LazyProperty
149 @LazyProperty
150 def unicode_path(self):
150 def unicode_path(self):
151 return safe_unicode(self.path)
151 return safe_unicode(self.path)
152
152
153 @LazyProperty
153 @LazyProperty
154 def has_rtlo(self):
154 def has_rtlo(self):
155 """Detects if a path has right-to-left-override marker"""
155 """Detects if a path has right-to-left-override marker"""
156 return self.RTLO_MARKER in self.unicode_path
156 return self.RTLO_MARKER in self.unicode_path
157
157
158 @LazyProperty
158 @LazyProperty
159 def unicode_path_safe(self):
159 def unicode_path_safe(self):
160 """
160 """
161 Special SAFE representation of path without the right-to-left-override.
161 Special SAFE representation of path without the right-to-left-override.
162 This should be only used for "showing" the file, cannot be used for any
162 This should be only used for "showing" the file, cannot be used for any
163 urls etc.
163 urls etc.
164 """
164 """
165 return safe_unicode(self.path).replace(self.RTLO_MARKER, '')
165 return safe_unicode(self.path).replace(self.RTLO_MARKER, '')
166
166
167 @LazyProperty
167 @LazyProperty
168 def dir_path(self):
168 def dir_path(self):
169 """
169 """
170 Returns name of the directory from full path of this vcs node. Empty
170 Returns name of the directory from full path of this vcs node. Empty
171 string is returned if there's no directory in the path
171 string is returned if there's no directory in the path
172 """
172 """
173 _parts = self.path.rstrip('/').rsplit('/', 1)
173 _parts = self.path.rstrip('/').rsplit('/', 1)
174 if len(_parts) == 2:
174 if len(_parts) == 2:
175 return safe_unicode(_parts[0])
175 return safe_unicode(_parts[0])
176 return u''
176 return u''
177
177
178 @LazyProperty
178 @LazyProperty
179 def name(self):
179 def name(self):
180 """
180 """
181 Returns name of the node so if its path
181 Returns name of the node so if its path
182 then only last part is returned.
182 then only last part is returned.
183 """
183 """
184 return safe_unicode(self.path.rstrip('/').split('/')[-1])
184 return safe_unicode(self.path.rstrip('/').split('/')[-1])
185
185
186 @property
186 @property
187 def kind(self):
187 def kind(self):
188 return self._kind
188 return self._kind
189
189
190 @kind.setter
190 @kind.setter
191 def kind(self, kind):
191 def kind(self, kind):
192 if hasattr(self, '_kind'):
192 if hasattr(self, '_kind'):
193 raise NodeError("Cannot change node's kind")
193 raise NodeError("Cannot change node's kind")
194 else:
194 else:
195 self._kind = kind
195 self._kind = kind
196 # Post setter check (path's trailing slash)
196 # Post setter check (path's trailing slash)
197 if self.path.endswith('/'):
197 if self.path.endswith('/'):
198 raise NodeError("Node's path cannot end with slash")
198 raise NodeError("Node's path cannot end with slash")
199
199
200 def __cmp__(self, other):
200 def __cmp__(self, other):
201 """
201 """
202 Comparator using name of the node, needed for quick list sorting.
202 Comparator using name of the node, needed for quick list sorting.
203 """
203 """
204
204
205 kind_cmp = cmp(self.kind, other.kind)
205 kind_cmp = cmp(self.kind, other.kind)
206 if kind_cmp:
206 if kind_cmp:
207 if isinstance(self, SubModuleNode):
207 if isinstance(self, SubModuleNode):
208 # we make submodules equal to dirnode for "sorting" purposes
208 # we make submodules equal to dirnode for "sorting" purposes
209 return NodeKind.DIR
209 return NodeKind.DIR
210 return kind_cmp
210 return kind_cmp
211 return cmp(self.name, other.name)
211 return cmp(self.name, other.name)
212
212
213 def __eq__(self, other):
213 def __eq__(self, other):
214 for attr in ['name', 'path', 'kind']:
214 for attr in ['name', 'path', 'kind']:
215 if getattr(self, attr) != getattr(other, attr):
215 if getattr(self, attr) != getattr(other, attr):
216 return False
216 return False
217 if self.is_file():
217 if self.is_file():
218 if self.content != other.content:
218 if self.content != other.content:
219 return False
219 return False
220 else:
220 else:
221 # For DirNode's check without entering each dir
221 # For DirNode's check without entering each dir
222 self_nodes_paths = list(sorted(n.path for n in self.nodes))
222 self_nodes_paths = list(sorted(n.path for n in self.nodes))
223 other_nodes_paths = list(sorted(n.path for n in self.nodes))
223 other_nodes_paths = list(sorted(n.path for n in self.nodes))
224 if self_nodes_paths != other_nodes_paths:
224 if self_nodes_paths != other_nodes_paths:
225 return False
225 return False
226 return True
226 return True
227
227
228 def __ne__(self, other):
228 def __ne__(self, other):
229 return not self.__eq__(other)
229 return not self.__eq__(other)
230
230
231 def __repr__(self):
231 def __repr__(self):
232 return '<%s %r>' % (self.__class__.__name__, self.path)
232 return '<%s %r>' % (self.__class__.__name__, self.path)
233
233
234 def __str__(self):
234 def __str__(self):
235 return self.__repr__()
235 return self.__repr__()
236
236
237 def __unicode__(self):
237 def __unicode__(self):
238 return self.name
238 return self.name
239
239
240 def get_parent_path(self):
240 def get_parent_path(self):
241 """
241 """
242 Returns node's parent path or empty string if node is root.
242 Returns node's parent path or empty string if node is root.
243 """
243 """
244 if self.is_root():
244 if self.is_root():
245 return ''
245 return ''
246 return vcspath.dirname(self.path.rstrip('/')) + '/'
246 return vcspath.dirname(self.path.rstrip('/')) + '/'
247
247
248 def is_file(self):
248 def is_file(self):
249 """
249 """
250 Returns ``True`` if node's kind is ``NodeKind.FILE``, ``False``
250 Returns ``True`` if node's kind is ``NodeKind.FILE``, ``False``
251 otherwise.
251 otherwise.
252 """
252 """
253 return self.kind == NodeKind.FILE
253 return self.kind == NodeKind.FILE
254
254
255 def is_dir(self):
255 def is_dir(self):
256 """
256 """
257 Returns ``True`` if node's kind is ``NodeKind.DIR``, ``False``
257 Returns ``True`` if node's kind is ``NodeKind.DIR``, ``False``
258 otherwise.
258 otherwise.
259 """
259 """
260 return self.kind == NodeKind.DIR
260 return self.kind == NodeKind.DIR
261
261
262 def is_root(self):
262 def is_root(self):
263 """
263 """
264 Returns ``True`` if node is a root node and ``False`` otherwise.
264 Returns ``True`` if node is a root node and ``False`` otherwise.
265 """
265 """
266 return self.kind == NodeKind.DIR and self.path == ''
266 return self.kind == NodeKind.DIR and self.path == ''
267
267
268 def is_submodule(self):
268 def is_submodule(self):
269 """
269 """
270 Returns ``True`` if node's kind is ``NodeKind.SUBMODULE``, ``False``
270 Returns ``True`` if node's kind is ``NodeKind.SUBMODULE``, ``False``
271 otherwise.
271 otherwise.
272 """
272 """
273 return self.kind == NodeKind.SUBMODULE
273 return self.kind == NodeKind.SUBMODULE
274
274
275 def is_largefile(self):
275 def is_largefile(self):
276 """
276 """
277 Returns ``True`` if node's kind is ``NodeKind.LARGEFILE``, ``False``
277 Returns ``True`` if node's kind is ``NodeKind.LARGEFILE``, ``False``
278 otherwise
278 otherwise
279 """
279 """
280 return self.kind == NodeKind.LARGEFILE
280 return self.kind == NodeKind.LARGEFILE
281
281
282 def is_link(self):
282 def is_link(self):
283 if self.commit:
283 if self.commit:
284 return self.commit.is_link(self.path)
284 return self.commit.is_link(self.path)
285 return False
285 return False
286
286
287 @LazyProperty
287 @LazyProperty
288 def added(self):
288 def added(self):
289 return self.state is NodeState.ADDED
289 return self.state is NodeState.ADDED
290
290
291 @LazyProperty
291 @LazyProperty
292 def changed(self):
292 def changed(self):
293 return self.state is NodeState.CHANGED
293 return self.state is NodeState.CHANGED
294
294
295 @LazyProperty
295 @LazyProperty
296 def not_changed(self):
296 def not_changed(self):
297 return self.state is NodeState.NOT_CHANGED
297 return self.state is NodeState.NOT_CHANGED
298
298
299 @LazyProperty
299 @LazyProperty
300 def removed(self):
300 def removed(self):
301 return self.state is NodeState.REMOVED
301 return self.state is NodeState.REMOVED
302
302
303
303
304 class FileNode(Node):
304 class FileNode(Node):
305 """
305 """
306 Class representing file nodes.
306 Class representing file nodes.
307
307
308 :attribute: path: path to the node, relative to repository's root
308 :attribute: path: path to the node, relative to repository's root
309 :attribute: content: if given arbitrary sets content of the file
309 :attribute: content: if given arbitrary sets content of the file
310 :attribute: commit: if given, first time content is accessed, callback
310 :attribute: commit: if given, first time content is accessed, callback
311 :attribute: mode: stat mode for a node. Default is `FILEMODE_DEFAULT`.
311 :attribute: mode: stat mode for a node. Default is `FILEMODE_DEFAULT`.
312 """
312 """
313 _filter_pre_load = []
313 _filter_pre_load = []
314
314
315 def __init__(self, path, content=None, commit=None, mode=None, pre_load=None):
315 def __init__(self, path, content=None, commit=None, mode=None, pre_load=None):
316 """
316 """
317 Only one of ``content`` and ``commit`` may be given. Passing both
317 Only one of ``content`` and ``commit`` may be given. Passing both
318 would raise ``NodeError`` exception.
318 would raise ``NodeError`` exception.
319
319
320 :param path: relative path to the node
320 :param path: relative path to the node
321 :param content: content may be passed to constructor
321 :param content: content may be passed to constructor
322 :param commit: if given, will use it to lazily fetch content
322 :param commit: if given, will use it to lazily fetch content
323 :param mode: ST_MODE (i.e. 0100644)
323 :param mode: ST_MODE (i.e. 0100644)
324 """
324 """
325 if content and commit:
325 if content and commit:
326 raise NodeError("Cannot use both content and commit")
326 raise NodeError("Cannot use both content and commit")
327 super(FileNode, self).__init__(path, kind=NodeKind.FILE)
327 super(FileNode, self).__init__(path, kind=NodeKind.FILE)
328 self.commit = commit
328 self.commit = commit
329 self._content = content
329 self._content = content
330 self._mode = mode or FILEMODE_DEFAULT
330 self._mode = mode or FILEMODE_DEFAULT
331
331
332 self._set_bulk_properties(pre_load)
332 self._set_bulk_properties(pre_load)
333
333
334 def _set_bulk_properties(self, pre_load):
334 def _set_bulk_properties(self, pre_load):
335 if not pre_load:
335 if not pre_load:
336 return
336 return
337 pre_load = [entry for entry in pre_load
337 pre_load = [entry for entry in pre_load
338 if entry not in self._filter_pre_load]
338 if entry not in self._filter_pre_load]
339 if not pre_load:
339 if not pre_load:
340 return
340 return
341
341
342 for attr_name in pre_load:
342 for attr_name in pre_load:
343 result = getattr(self, attr_name)
343 result = getattr(self, attr_name)
344 if callable(result):
344 if callable(result):
345 result = result()
345 result = result()
346 self.__dict__[attr_name] = result
346 self.__dict__[attr_name] = result
347
347
348 @LazyProperty
348 @LazyProperty
349 def mode(self):
349 def mode(self):
350 """
350 """
351 Returns lazily mode of the FileNode. If `commit` is not set, would
351 Returns lazily mode of the FileNode. If `commit` is not set, would
352 use value given at initialization or `FILEMODE_DEFAULT` (default).
352 use value given at initialization or `FILEMODE_DEFAULT` (default).
353 """
353 """
354 if self.commit:
354 if self.commit:
355 mode = self.commit.get_file_mode(self.path)
355 mode = self.commit.get_file_mode(self.path)
356 else:
356 else:
357 mode = self._mode
357 mode = self._mode
358 return mode
358 return mode
359
359
360 @LazyProperty
360 @LazyProperty
361 def raw_bytes(self):
361 def raw_bytes(self):
362 """
362 """
363 Returns lazily the raw bytes of the FileNode.
363 Returns lazily the raw bytes of the FileNode.
364 """
364 """
365 if self.commit:
365 if self.commit:
366 if self._content is None:
366 if self._content is None:
367 self._content = self.commit.get_file_content(self.path)
367 self._content = self.commit.get_file_content(self.path)
368 content = self._content
368 content = self._content
369 else:
369 else:
370 content = self._content
370 content = self._content
371 return content
371 return content
372
372
373 def stream_bytes(self):
373 def stream_bytes(self):
374 """
374 """
375 Returns an iterator that will stream the content of the file directly from
375 Returns an iterator that will stream the content of the file directly from
376 vcsserver without loading it to memory.
376 vcsserver without loading it to memory.
377 """
377 """
378 if self.commit:
378 if self.commit:
379 return self.commit.get_file_content_streamed(self.path)
379 return self.commit.get_file_content_streamed(self.path)
380 raise NodeError(
380 raise NodeError("Cannot retrieve stream_bytes without related commit attribute")
381 "Cannot retrieve message of the file without related "
382 "commit attribute")
383
381
384 @LazyProperty
382 @LazyProperty
385 def md5(self):
383 def md5(self):
386 """
384 """
387 Returns md5 of the file node.
385 Returns md5 of the file node.
388 """
386 """
389 return md5(self.raw_bytes)
387 return md5(self.raw_bytes)
390
388
391 def metadata_uncached(self):
389 def metadata_uncached(self):
392 """
390 """
393 Returns md5, binary flag of the file node, without any cache usage.
391 Returns md5, binary flag of the file node, without any cache usage.
394 """
392 """
395
393
396 content = self.content_uncached()
394 content = self.content_uncached()
397
395
398 is_binary = content and '\0' in content
396 is_binary = content and '\0' in content
399 size = 0
397 size = 0
400 if content:
398 if content:
401 size = len(content)
399 size = len(content)
402
400
403 return is_binary, md5(content), size, content
401 return is_binary, md5(content), size, content
404
402
405 def content_uncached(self):
403 def content_uncached(self):
406 """
404 """
407 Returns lazily content of the FileNode. If possible, would try to
405 Returns lazily content of the FileNode. If possible, would try to
408 decode content from UTF-8.
406 decode content from UTF-8.
409 """
407 """
410 if self.commit:
408 if self.commit:
411 content = self.commit.get_file_content(self.path)
409 content = self.commit.get_file_content(self.path)
412 else:
410 else:
413 content = self._content
411 content = self._content
414 return content
412 return content
415
413
416 @LazyProperty
414 @LazyProperty
417 def content(self):
415 def content(self):
418 """
416 """
419 Returns lazily content of the FileNode. If possible, would try to
417 Returns lazily content of the FileNode. If possible, would try to
420 decode content from UTF-8.
418 decode content from UTF-8.
421 """
419 """
422 content = self.raw_bytes
420 content = self.raw_bytes
423
421
424 if self.is_binary:
422 if self.is_binary:
425 return content
423 return content
426 return safe_unicode(content)
424 return safe_unicode(content)
427
425
428 @LazyProperty
426 @LazyProperty
429 def size(self):
427 def size(self):
430 if self.commit:
428 if self.commit:
431 return self.commit.get_file_size(self.path)
429 return self.commit.get_file_size(self.path)
432 raise NodeError(
430 raise NodeError(
433 "Cannot retrieve size of the file without related "
431 "Cannot retrieve size of the file without related "
434 "commit attribute")
432 "commit attribute")
435
433
436 @LazyProperty
434 @LazyProperty
437 def message(self):
435 def message(self):
438 if self.commit:
436 if self.commit:
439 return self.last_commit.message
437 return self.last_commit.message
440 raise NodeError(
438 raise NodeError(
441 "Cannot retrieve message of the file without related "
439 "Cannot retrieve message of the file without related "
442 "commit attribute")
440 "commit attribute")
443
441
444 @LazyProperty
442 @LazyProperty
445 def last_commit(self):
443 def last_commit(self):
446 if self.commit:
444 if self.commit:
447 pre_load = ["author", "date", "message", "parents"]
445 pre_load = ["author", "date", "message", "parents"]
448 return self.commit.get_path_commit(self.path, pre_load=pre_load)
446 return self.commit.get_path_commit(self.path, pre_load=pre_load)
449 raise NodeError(
447 raise NodeError(
450 "Cannot retrieve last commit of the file without "
448 "Cannot retrieve last commit of the file without "
451 "related commit attribute")
449 "related commit attribute")
452
450
453 def get_mimetype(self):
451 def get_mimetype(self):
454 """
452 """
455 Mimetype is calculated based on the file's content. If ``_mimetype``
453 Mimetype is calculated based on the file's content. If ``_mimetype``
456 attribute is available, it will be returned (backends which store
454 attribute is available, it will be returned (backends which store
457 mimetypes or can easily recognize them, should set this private
455 mimetypes or can easily recognize them, should set this private
458 attribute to indicate that type should *NOT* be calculated).
456 attribute to indicate that type should *NOT* be calculated).
459 """
457 """
460
458
461 if hasattr(self, '_mimetype'):
459 if hasattr(self, '_mimetype'):
462 if (isinstance(self._mimetype, (tuple, list,)) and
460 if (isinstance(self._mimetype, (tuple, list,)) and
463 len(self._mimetype) == 2):
461 len(self._mimetype) == 2):
464 return self._mimetype
462 return self._mimetype
465 else:
463 else:
466 raise NodeError('given _mimetype attribute must be an 2 '
464 raise NodeError('given _mimetype attribute must be an 2 '
467 'element list or tuple')
465 'element list or tuple')
468
466
469 db = get_mimetypes_db()
467 db = get_mimetypes_db()
470 mtype, encoding = db.guess_type(self.name)
468 mtype, encoding = db.guess_type(self.name)
471
469
472 if mtype is None:
470 if mtype is None:
473 if self.is_binary:
471 if self.is_binary:
474 mtype = 'application/octet-stream'
472 mtype = 'application/octet-stream'
475 encoding = None
473 encoding = None
476 else:
474 else:
477 mtype = 'text/plain'
475 mtype = 'text/plain'
478 encoding = None
476 encoding = None
479
477
480 # try with pygments
478 # try with pygments
481 try:
479 try:
482 from pygments.lexers import get_lexer_for_filename
480 from pygments.lexers import get_lexer_for_filename
483 mt = get_lexer_for_filename(self.name).mimetypes
481 mt = get_lexer_for_filename(self.name).mimetypes
484 except Exception:
482 except Exception:
485 mt = None
483 mt = None
486
484
487 if mt:
485 if mt:
488 mtype = mt[0]
486 mtype = mt[0]
489
487
490 return mtype, encoding
488 return mtype, encoding
491
489
492 @LazyProperty
490 @LazyProperty
493 def mimetype(self):
491 def mimetype(self):
494 """
492 """
495 Wrapper around full mimetype info. It returns only type of fetched
493 Wrapper around full mimetype info. It returns only type of fetched
496 mimetype without the encoding part. use get_mimetype function to fetch
494 mimetype without the encoding part. use get_mimetype function to fetch
497 full set of (type,encoding)
495 full set of (type,encoding)
498 """
496 """
499 return self.get_mimetype()[0]
497 return self.get_mimetype()[0]
500
498
501 @LazyProperty
499 @LazyProperty
502 def mimetype_main(self):
500 def mimetype_main(self):
503 return self.mimetype.split('/')[0]
501 return self.mimetype.split('/')[0]
504
502
505 @classmethod
503 @classmethod
506 def get_lexer(cls, filename, content=None):
504 def get_lexer(cls, filename, content=None):
507 from pygments import lexers
505 from pygments import lexers
508
506
509 extension = filename.split('.')[-1]
507 extension = filename.split('.')[-1]
510 lexer = None
508 lexer = None
511
509
512 try:
510 try:
513 lexer = lexers.guess_lexer_for_filename(
511 lexer = lexers.guess_lexer_for_filename(
514 filename, content, stripnl=False)
512 filename, content, stripnl=False)
515 except lexers.ClassNotFound:
513 except lexers.ClassNotFound:
516 lexer = None
514 lexer = None
517
515
518 # try our EXTENSION_MAP
516 # try our EXTENSION_MAP
519 if not lexer:
517 if not lexer:
520 try:
518 try:
521 lexer_class = LANGUAGES_EXTENSIONS_MAP.get(extension)
519 lexer_class = LANGUAGES_EXTENSIONS_MAP.get(extension)
522 if lexer_class:
520 if lexer_class:
523 lexer = lexers.get_lexer_by_name(lexer_class[0])
521 lexer = lexers.get_lexer_by_name(lexer_class[0])
524 except lexers.ClassNotFound:
522 except lexers.ClassNotFound:
525 lexer = None
523 lexer = None
526
524
527 if not lexer:
525 if not lexer:
528 lexer = lexers.TextLexer(stripnl=False)
526 lexer = lexers.TextLexer(stripnl=False)
529
527
530 return lexer
528 return lexer
531
529
532 @LazyProperty
530 @LazyProperty
533 def lexer(self):
531 def lexer(self):
534 """
532 """
535 Returns pygment's lexer class. Would try to guess lexer taking file's
533 Returns pygment's lexer class. Would try to guess lexer taking file's
536 content, name and mimetype.
534 content, name and mimetype.
537 """
535 """
538 return self.get_lexer(self.name, self.content)
536 return self.get_lexer(self.name, self.content)
539
537
540 @LazyProperty
538 @LazyProperty
541 def lexer_alias(self):
539 def lexer_alias(self):
542 """
540 """
543 Returns first alias of the lexer guessed for this file.
541 Returns first alias of the lexer guessed for this file.
544 """
542 """
545 return self.lexer.aliases[0]
543 return self.lexer.aliases[0]
546
544
547 @LazyProperty
545 @LazyProperty
548 def history(self):
546 def history(self):
549 """
547 """
550 Returns a list of commit for this file in which the file was changed
548 Returns a list of commit for this file in which the file was changed
551 """
549 """
552 if self.commit is None:
550 if self.commit is None:
553 raise NodeError('Unable to get commit for this FileNode')
551 raise NodeError('Unable to get commit for this FileNode')
554 return self.commit.get_path_history(self.path)
552 return self.commit.get_path_history(self.path)
555
553
556 @LazyProperty
554 @LazyProperty
557 def annotate(self):
555 def annotate(self):
558 """
556 """
559 Returns a list of three element tuples with lineno, commit and line
557 Returns a list of three element tuples with lineno, commit and line
560 """
558 """
561 if self.commit is None:
559 if self.commit is None:
562 raise NodeError('Unable to get commit for this FileNode')
560 raise NodeError('Unable to get commit for this FileNode')
563 pre_load = ["author", "date", "message", "parents"]
561 pre_load = ["author", "date", "message", "parents"]
564 return self.commit.get_file_annotate(self.path, pre_load=pre_load)
562 return self.commit.get_file_annotate(self.path, pre_load=pre_load)
565
563
566 @LazyProperty
564 @LazyProperty
567 def state(self):
565 def state(self):
568 if not self.commit:
566 if not self.commit:
569 raise NodeError(
567 raise NodeError(
570 "Cannot check state of the node if it's not "
568 "Cannot check state of the node if it's not "
571 "linked with commit")
569 "linked with commit")
572 elif self.path in (node.path for node in self.commit.added):
570 elif self.path in (node.path for node in self.commit.added):
573 return NodeState.ADDED
571 return NodeState.ADDED
574 elif self.path in (node.path for node in self.commit.changed):
572 elif self.path in (node.path for node in self.commit.changed):
575 return NodeState.CHANGED
573 return NodeState.CHANGED
576 else:
574 else:
577 return NodeState.NOT_CHANGED
575 return NodeState.NOT_CHANGED
578
576
579 @LazyProperty
577 @LazyProperty
580 def is_binary(self):
578 def is_binary(self):
581 """
579 """
582 Returns True if file has binary content.
580 Returns True if file has binary content.
583 """
581 """
584 _bin = self.raw_bytes and '\0' in self.raw_bytes
582 if self.commit:
585 return _bin
583 return self.commit.is_node_binary(self.path)
584 else:
585 raw_bytes = self._content
586 return raw_bytes and '\0' in raw_bytes
586
587
587 @LazyProperty
588 @LazyProperty
588 def extension(self):
589 def extension(self):
589 """Returns filenode extension"""
590 """Returns filenode extension"""
590 return self.name.split('.')[-1]
591 return self.name.split('.')[-1]
591
592
592 @property
593 @property
593 def is_executable(self):
594 def is_executable(self):
594 """
595 """
595 Returns ``True`` if file has executable flag turned on.
596 Returns ``True`` if file has executable flag turned on.
596 """
597 """
597 return bool(self.mode & stat.S_IXUSR)
598 return bool(self.mode & stat.S_IXUSR)
598
599
599 def get_largefile_node(self):
600 def get_largefile_node(self):
600 """
601 """
601 Try to return a Mercurial FileNode from this node. It does internal
602 Try to return a Mercurial FileNode from this node. It does internal
602 checks inside largefile store, if that file exist there it will
603 checks inside largefile store, if that file exist there it will
603 create special instance of LargeFileNode which can get content from
604 create special instance of LargeFileNode which can get content from
604 LF store.
605 LF store.
605 """
606 """
606 if self.commit:
607 if self.commit:
607 return self.commit.get_largefile_node(self.path)
608 return self.commit.get_largefile_node(self.path)
608
609
609 def lines(self, count_empty=False):
610 def lines(self, count_empty=False):
610 all_lines, empty_lines = 0, 0
611 all_lines, empty_lines = 0, 0
611
612
612 if not self.is_binary:
613 if not self.is_binary:
613 content = self.content
614 content = self.content
614 if count_empty:
615 if count_empty:
615 all_lines = 0
616 all_lines = 0
616 empty_lines = 0
617 empty_lines = 0
617 for line in content.splitlines(True):
618 for line in content.splitlines(True):
618 if line == '\n':
619 if line == '\n':
619 empty_lines += 1
620 empty_lines += 1
620 all_lines += 1
621 all_lines += 1
621
622
622 return all_lines, all_lines - empty_lines
623 return all_lines, all_lines - empty_lines
623 else:
624 else:
624 # fast method
625 # fast method
625 empty_lines = all_lines = content.count('\n')
626 empty_lines = all_lines = content.count('\n')
626 if all_lines == 0 and content:
627 if all_lines == 0 and content:
627 # one-line without a newline
628 # one-line without a newline
628 empty_lines = all_lines = 1
629 empty_lines = all_lines = 1
629
630
630 return all_lines, empty_lines
631 return all_lines, empty_lines
631
632
632 def __repr__(self):
633 def __repr__(self):
633 return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
634 return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
634 getattr(self.commit, 'short_id', ''))
635 getattr(self.commit, 'short_id', ''))
635
636
636
637
637 class RemovedFileNode(FileNode):
638 class RemovedFileNode(FileNode):
638 """
639 """
639 Dummy FileNode class - trying to access any public attribute except path,
640 Dummy FileNode class - trying to access any public attribute except path,
640 name, kind or state (or methods/attributes checking those two) would raise
641 name, kind or state (or methods/attributes checking those two) would raise
641 RemovedFileNodeError.
642 RemovedFileNodeError.
642 """
643 """
643 ALLOWED_ATTRIBUTES = [
644 ALLOWED_ATTRIBUTES = [
644 'name', 'path', 'state', 'is_root', 'is_file', 'is_dir', 'kind',
645 'name', 'path', 'state', 'is_root', 'is_file', 'is_dir', 'kind',
645 'added', 'changed', 'not_changed', 'removed'
646 'added', 'changed', 'not_changed', 'removed'
646 ]
647 ]
647
648
648 def __init__(self, path):
649 def __init__(self, path):
649 """
650 """
650 :param path: relative path to the node
651 :param path: relative path to the node
651 """
652 """
652 super(RemovedFileNode, self).__init__(path=path)
653 super(RemovedFileNode, self).__init__(path=path)
653
654
654 def __getattribute__(self, attr):
655 def __getattribute__(self, attr):
655 if attr.startswith('_') or attr in RemovedFileNode.ALLOWED_ATTRIBUTES:
656 if attr.startswith('_') or attr in RemovedFileNode.ALLOWED_ATTRIBUTES:
656 return super(RemovedFileNode, self).__getattribute__(attr)
657 return super(RemovedFileNode, self).__getattribute__(attr)
657 raise RemovedFileNodeError(
658 raise RemovedFileNodeError(
658 "Cannot access attribute %s on RemovedFileNode" % attr)
659 "Cannot access attribute %s on RemovedFileNode" % attr)
659
660
660 @LazyProperty
661 @LazyProperty
661 def state(self):
662 def state(self):
662 return NodeState.REMOVED
663 return NodeState.REMOVED
663
664
664
665
665 class DirNode(Node):
666 class DirNode(Node):
666 """
667 """
667 DirNode stores list of files and directories within this node.
668 DirNode stores list of files and directories within this node.
668 Nodes may be used standalone but within repository context they
669 Nodes may be used standalone but within repository context they
669 lazily fetch data within same repositorty's commit.
670 lazily fetch data within same repositorty's commit.
670 """
671 """
671
672
672 def __init__(self, path, nodes=(), commit=None):
673 def __init__(self, path, nodes=(), commit=None):
673 """
674 """
674 Only one of ``nodes`` and ``commit`` may be given. Passing both
675 Only one of ``nodes`` and ``commit`` may be given. Passing both
675 would raise ``NodeError`` exception.
676 would raise ``NodeError`` exception.
676
677
677 :param path: relative path to the node
678 :param path: relative path to the node
678 :param nodes: content may be passed to constructor
679 :param nodes: content may be passed to constructor
679 :param commit: if given, will use it to lazily fetch content
680 :param commit: if given, will use it to lazily fetch content
680 """
681 """
681 if nodes and commit:
682 if nodes and commit:
682 raise NodeError("Cannot use both nodes and commit")
683 raise NodeError("Cannot use both nodes and commit")
683 super(DirNode, self).__init__(path, NodeKind.DIR)
684 super(DirNode, self).__init__(path, NodeKind.DIR)
684 self.commit = commit
685 self.commit = commit
685 self._nodes = nodes
686 self._nodes = nodes
686
687
687 @LazyProperty
688 @LazyProperty
688 def content(self):
689 def content(self):
689 raise NodeError(
690 raise NodeError(
690 "%s represents a dir and has no `content` attribute" % self)
691 "%s represents a dir and has no `content` attribute" % self)
691
692
692 @LazyProperty
693 @LazyProperty
693 def nodes(self):
694 def nodes(self):
694 if self.commit:
695 if self.commit:
695 nodes = self.commit.get_nodes(self.path)
696 nodes = self.commit.get_nodes(self.path)
696 else:
697 else:
697 nodes = self._nodes
698 nodes = self._nodes
698 self._nodes_dict = dict((node.path, node) for node in nodes)
699 self._nodes_dict = dict((node.path, node) for node in nodes)
699 return sorted(nodes)
700 return sorted(nodes)
700
701
701 @LazyProperty
702 @LazyProperty
702 def files(self):
703 def files(self):
703 return sorted((node for node in self.nodes if node.is_file()))
704 return sorted((node for node in self.nodes if node.is_file()))
704
705
705 @LazyProperty
706 @LazyProperty
706 def dirs(self):
707 def dirs(self):
707 return sorted((node for node in self.nodes if node.is_dir()))
708 return sorted((node for node in self.nodes if node.is_dir()))
708
709
709 def __iter__(self):
710 def __iter__(self):
710 for node in self.nodes:
711 for node in self.nodes:
711 yield node
712 yield node
712
713
713 def get_node(self, path):
714 def get_node(self, path):
714 """
715 """
715 Returns node from within this particular ``DirNode``, so it is now
716 Returns node from within this particular ``DirNode``, so it is now
716 allowed to fetch, i.e. node located at 'docs/api/index.rst' from node
717 allowed to fetch, i.e. node located at 'docs/api/index.rst' from node
717 'docs'. In order to access deeper nodes one must fetch nodes between
718 'docs'. In order to access deeper nodes one must fetch nodes between
718 them first - this would work::
719 them first - this would work::
719
720
720 docs = root.get_node('docs')
721 docs = root.get_node('docs')
721 docs.get_node('api').get_node('index.rst')
722 docs.get_node('api').get_node('index.rst')
722
723
723 :param: path - relative to the current node
724 :param: path - relative to the current node
724
725
725 .. note::
726 .. note::
726 To access lazily (as in example above) node have to be initialized
727 To access lazily (as in example above) node have to be initialized
727 with related commit object - without it node is out of
728 with related commit object - without it node is out of
728 context and may know nothing about anything else than nearest
729 context and may know nothing about anything else than nearest
729 (located at same level) nodes.
730 (located at same level) nodes.
730 """
731 """
731 try:
732 try:
732 path = path.rstrip('/')
733 path = path.rstrip('/')
733 if path == '':
734 if path == '':
734 raise NodeError("Cannot retrieve node without path")
735 raise NodeError("Cannot retrieve node without path")
735 self.nodes # access nodes first in order to set _nodes_dict
736 self.nodes # access nodes first in order to set _nodes_dict
736 paths = path.split('/')
737 paths = path.split('/')
737 if len(paths) == 1:
738 if len(paths) == 1:
738 if not self.is_root():
739 if not self.is_root():
739 path = '/'.join((self.path, paths[0]))
740 path = '/'.join((self.path, paths[0]))
740 else:
741 else:
741 path = paths[0]
742 path = paths[0]
742 return self._nodes_dict[path]
743 return self._nodes_dict[path]
743 elif len(paths) > 1:
744 elif len(paths) > 1:
744 if self.commit is None:
745 if self.commit is None:
745 raise NodeError(
746 raise NodeError("Cannot access deeper nodes without commit")
746 "Cannot access deeper nodes without commit")
747 else:
747 else:
748 path1, path2 = paths[0], '/'.join(paths[1:])
748 path1, path2 = paths[0], '/'.join(paths[1:])
749 return self.get_node(path1).get_node(path2)
749 return self.get_node(path1).get_node(path2)
750 else:
750 else:
751 raise KeyError
751 raise KeyError
752 except KeyError:
752 except KeyError:
753 raise NodeError("Node does not exist at %s" % path)
753 raise NodeError("Node does not exist at %s" % path)
754
754
755 @LazyProperty
755 @LazyProperty
756 def state(self):
756 def state(self):
757 raise NodeError("Cannot access state of DirNode")
757 raise NodeError("Cannot access state of DirNode")
758
758
759 @LazyProperty
759 @LazyProperty
760 def size(self):
760 def size(self):
761 size = 0
761 size = 0
762 for root, dirs, files in self.commit.walk(self.path):
762 for root, dirs, files in self.commit.walk(self.path):
763 for f in files:
763 for f in files:
764 size += f.size
764 size += f.size
765
765
766 return size
766 return size
767
767
768 @LazyProperty
768 @LazyProperty
769 def last_commit(self):
769 def last_commit(self):
770 if self.commit:
770 if self.commit:
771 pre_load = ["author", "date", "message", "parents"]
771 pre_load = ["author", "date", "message", "parents"]
772 return self.commit.get_path_commit(self.path, pre_load=pre_load)
772 return self.commit.get_path_commit(self.path, pre_load=pre_load)
773 raise NodeError(
773 raise NodeError(
774 "Cannot retrieve last commit of the file without "
774 "Cannot retrieve last commit of the file without "
775 "related commit attribute")
775 "related commit attribute")
776
776
777 def __repr__(self):
777 def __repr__(self):
778 return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
778 return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
779 getattr(self.commit, 'short_id', ''))
779 getattr(self.commit, 'short_id', ''))
780
780
781
781
782 class RootNode(DirNode):
782 class RootNode(DirNode):
783 """
783 """
784 DirNode being the root node of the repository.
784 DirNode being the root node of the repository.
785 """
785 """
786
786
787 def __init__(self, nodes=(), commit=None):
787 def __init__(self, nodes=(), commit=None):
788 super(RootNode, self).__init__(path='', nodes=nodes, commit=commit)
788 super(RootNode, self).__init__(path='', nodes=nodes, commit=commit)
789
789
790 def __repr__(self):
790 def __repr__(self):
791 return '<%s>' % self.__class__.__name__
791 return '<%s>' % self.__class__.__name__
792
792
793
793
794 class SubModuleNode(Node):
794 class SubModuleNode(Node):
795 """
795 """
796 represents a SubModule of Git or SubRepo of Mercurial
796 represents a SubModule of Git or SubRepo of Mercurial
797 """
797 """
798 is_binary = False
798 is_binary = False
799 size = 0
799 size = 0
800
800
801 def __init__(self, name, url=None, commit=None, alias=None):
801 def __init__(self, name, url=None, commit=None, alias=None):
802 self.path = name
802 self.path = name
803 self.kind = NodeKind.SUBMODULE
803 self.kind = NodeKind.SUBMODULE
804 self.alias = alias
804 self.alias = alias
805
805
806 # we have to use EmptyCommit here since this can point to svn/git/hg
806 # we have to use EmptyCommit here since this can point to svn/git/hg
807 # submodules we cannot get from repository
807 # submodules we cannot get from repository
808 self.commit = EmptyCommit(str(commit), alias=alias)
808 self.commit = EmptyCommit(str(commit), alias=alias)
809 self.url = url or self._extract_submodule_url()
809 self.url = url or self._extract_submodule_url()
810
810
811 def __repr__(self):
811 def __repr__(self):
812 return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
812 return '<%s %r @ %s>' % (self.__class__.__name__, self.path,
813 getattr(self.commit, 'short_id', ''))
813 getattr(self.commit, 'short_id', ''))
814
814
815 def _extract_submodule_url(self):
815 def _extract_submodule_url(self):
816 # TODO: find a way to parse gits submodule file and extract the
816 # TODO: find a way to parse gits submodule file and extract the
817 # linking URL
817 # linking URL
818 return self.path
818 return self.path
819
819
820 @LazyProperty
820 @LazyProperty
821 def name(self):
821 def name(self):
822 """
822 """
823 Returns name of the node so if its path
823 Returns name of the node so if its path
824 then only last part is returned.
824 then only last part is returned.
825 """
825 """
826 org = safe_unicode(self.path.rstrip('/').split('/')[-1])
826 org = safe_unicode(self.path.rstrip('/').split('/')[-1])
827 return u'%s @ %s' % (org, self.commit.short_id)
827 return u'%s @ %s' % (org, self.commit.short_id)
828
828
829
829
830 class LargeFileNode(FileNode):
830 class LargeFileNode(FileNode):
831
831
832 def __init__(self, path, url=None, commit=None, alias=None, org_path=None):
832 def __init__(self, path, url=None, commit=None, alias=None, org_path=None):
833 self.path = path
833 self.path = path
834 self.org_path = org_path
834 self.org_path = org_path
835 self.kind = NodeKind.LARGEFILE
835 self.kind = NodeKind.LARGEFILE
836 self.alias = alias
836 self.alias = alias
837
837
838 def _validate_path(self, path):
838 def _validate_path(self, path):
839 """
839 """
840 we override check since the LargeFileNode path is system absolute
840 we override check since the LargeFileNode path is system absolute
841 """
841 """
842 pass
842 pass
843
843
844 def __repr__(self):
844 def __repr__(self):
845 return '<%s %r>' % (self.__class__.__name__, self.path)
845 return '<%s %r>' % (self.__class__.__name__, self.path)
846
846
847 @LazyProperty
847 @LazyProperty
848 def size(self):
848 def size(self):
849 return os.stat(self.path).st_size
849 return os.stat(self.path).st_size
850
850
851 @LazyProperty
851 @LazyProperty
852 def raw_bytes(self):
852 def raw_bytes(self):
853 with open(self.path, 'rb') as f:
853 with open(self.path, 'rb') as f:
854 content = f.read()
854 content = f.read()
855 return content
855 return content
856
856
857 @LazyProperty
857 @LazyProperty
858 def name(self):
858 def name(self):
859 """
859 """
860 Overwrites name to be the org lf path
860 Overwrites name to be the org lf path
861 """
861 """
862 return self.org_path
862 return self.org_path
863
863
864 def stream_bytes(self):
864 def stream_bytes(self):
865 with open(self.path, 'rb') as stream:
865 with open(self.path, 'rb') as stream:
866 while True:
866 while True:
867 data = stream.read(16 * 1024)
867 data = stream.read(16 * 1024)
868 if not data:
868 if not data:
869 break
869 break
870 yield data
870 yield data
General Comments 0
You need to be logged in to leave comments. Login now