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