##// END OF EJS Templates
vcs: Add enumeration for pull request update failure reasons.
Martin Bornhold -
r1069:dab9d8bd default
parent child Browse files
Show More
@@ -1,1514 +1,1544 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2014-2016 RhodeCode GmbH
3 # Copyright (C) 2014-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Base module for all VCS systems
22 Base module for all VCS systems
23 """
23 """
24
24
25 import collections
25 import collections
26 import datetime
26 import datetime
27 import itertools
27 import itertools
28 import logging
28 import logging
29 import os
29 import os
30 import time
30 import time
31 import warnings
31 import warnings
32
32
33 from zope.cachedescriptors.property import Lazy as LazyProperty
33 from zope.cachedescriptors.property import Lazy as LazyProperty
34
34
35 from rhodecode.lib.utils2 import safe_str, safe_unicode
35 from rhodecode.lib.utils2 import safe_str, safe_unicode
36 from rhodecode.lib.vcs import connection
36 from rhodecode.lib.vcs import connection
37 from rhodecode.lib.vcs.utils import author_name, author_email
37 from rhodecode.lib.vcs.utils import author_name, author_email
38 from rhodecode.lib.vcs.conf import settings
38 from rhodecode.lib.vcs.conf import settings
39 from rhodecode.lib.vcs.exceptions import (
39 from rhodecode.lib.vcs.exceptions import (
40 CommitError, EmptyRepositoryError, NodeAlreadyAddedError,
40 CommitError, EmptyRepositoryError, NodeAlreadyAddedError,
41 NodeAlreadyChangedError, NodeAlreadyExistsError, NodeAlreadyRemovedError,
41 NodeAlreadyChangedError, NodeAlreadyExistsError, NodeAlreadyRemovedError,
42 NodeDoesNotExistError, NodeNotChangedError, VCSError,
42 NodeDoesNotExistError, NodeNotChangedError, VCSError,
43 ImproperArchiveTypeError, BranchDoesNotExistError, CommitDoesNotExistError,
43 ImproperArchiveTypeError, BranchDoesNotExistError, CommitDoesNotExistError,
44 RepositoryError)
44 RepositoryError)
45
45
46
46
47 log = logging.getLogger(__name__)
47 log = logging.getLogger(__name__)
48
48
49
49
50 FILEMODE_DEFAULT = 0100644
50 FILEMODE_DEFAULT = 0100644
51 FILEMODE_EXECUTABLE = 0100755
51 FILEMODE_EXECUTABLE = 0100755
52
52
53 Reference = collections.namedtuple('Reference', ('type', 'name', 'commit_id'))
53 Reference = collections.namedtuple('Reference', ('type', 'name', 'commit_id'))
54 MergeResponse = collections.namedtuple(
54 MergeResponse = collections.namedtuple(
55 'MergeResponse',
55 'MergeResponse',
56 ('possible', 'executed', 'merge_ref', 'failure_reason'))
56 ('possible', 'executed', 'merge_ref', 'failure_reason'))
57
57
58
58
59 class MergeFailureReason(object):
59 class MergeFailureReason(object):
60 """
60 """
61 Enumeration with all the reasons why the server side merge could fail.
61 Enumeration with all the reasons why the server side merge could fail.
62
62
63 DO NOT change the number of the reasons, as they may be stored in the
63 DO NOT change the number of the reasons, as they may be stored in the
64 database.
64 database.
65
65
66 Changing the name of a reason is acceptable and encouraged to deprecate old
66 Changing the name of a reason is acceptable and encouraged to deprecate old
67 reasons.
67 reasons.
68 """
68 """
69
69
70 # Everything went well.
70 # Everything went well.
71 NONE = 0
71 NONE = 0
72
72
73 # An unexpected exception was raised. Check the logs for more details.
73 # An unexpected exception was raised. Check the logs for more details.
74 UNKNOWN = 1
74 UNKNOWN = 1
75
75
76 # The merge was not successful, there are conflicts.
76 # The merge was not successful, there are conflicts.
77 MERGE_FAILED = 2
77 MERGE_FAILED = 2
78
78
79 # The merge succeeded but we could not push it to the target repository.
79 # The merge succeeded but we could not push it to the target repository.
80 PUSH_FAILED = 3
80 PUSH_FAILED = 3
81
81
82 # The specified target is not a head in the target repository.
82 # The specified target is not a head in the target repository.
83 TARGET_IS_NOT_HEAD = 4
83 TARGET_IS_NOT_HEAD = 4
84
84
85 # The source repository contains more branches than the target. Pushing
85 # The source repository contains more branches than the target. Pushing
86 # the merge will create additional branches in the target.
86 # the merge will create additional branches in the target.
87 HG_SOURCE_HAS_MORE_BRANCHES = 5
87 HG_SOURCE_HAS_MORE_BRANCHES = 5
88
88
89 # The target reference has multiple heads. That does not allow to correctly
89 # The target reference has multiple heads. That does not allow to correctly
90 # identify the target location. This could only happen for mercurial
90 # identify the target location. This could only happen for mercurial
91 # branches.
91 # branches.
92 HG_TARGET_HAS_MULTIPLE_HEADS = 6
92 HG_TARGET_HAS_MULTIPLE_HEADS = 6
93
93
94 # The target repository is locked
94 # The target repository is locked
95 TARGET_IS_LOCKED = 7
95 TARGET_IS_LOCKED = 7
96
96
97 # A involved commit could not be found.
97 # A involved commit could not be found.
98 MISSING_COMMIT = 8
98 MISSING_COMMIT = 8
99
99
100 # The target repo reference is missing.
100 # The target repo reference is missing.
101 MISSING_TARGET_REF = 9
101 MISSING_TARGET_REF = 9
102
102
103 # The source repo reference is missing.
103 # The source repo reference is missing.
104 MISSING_SOURCE_REF = 10
104 MISSING_SOURCE_REF = 10
105
105
106
106
107 class UpdateFailureReason(object):
108 """
109 Enumeration with all the reasons why the pull request update could fail.
110
111 DO NOT change the number of the reasons, as they may be stored in the
112 database.
113
114 Changing the name of a reason is acceptable and encouraged to deprecate old
115 reasons.
116 """
117
118 # Everything went well.
119 NONE = 0
120
121 # An unexpected exception was raised. Check the logs for more details.
122 UNKNOWN = 1
123
124 # The pull request is up to date.
125 NO_CHANGE = 2
126
127 # The pull request has a reference type that is not supported for update.
128 WRONG_REF_TPYE = 3
129
130 # Update failed because the target reference is missing.
131 MISSING_TARGET_REF = 4
132
133 # Update failed because the source reference is missing.
134 MISSING_SOURCE_REF = 5
135
136
107 class BaseRepository(object):
137 class BaseRepository(object):
108 """
138 """
109 Base Repository for final backends
139 Base Repository for final backends
110
140
111 .. attribute:: DEFAULT_BRANCH_NAME
141 .. attribute:: DEFAULT_BRANCH_NAME
112
142
113 name of default branch (i.e. "trunk" for svn, "master" for git etc.
143 name of default branch (i.e. "trunk" for svn, "master" for git etc.
114
144
115 .. attribute:: commit_ids
145 .. attribute:: commit_ids
116
146
117 list of all available commit ids, in ascending order
147 list of all available commit ids, in ascending order
118
148
119 .. attribute:: path
149 .. attribute:: path
120
150
121 absolute path to the repository
151 absolute path to the repository
122
152
123 .. attribute:: bookmarks
153 .. attribute:: bookmarks
124
154
125 Mapping from name to :term:`Commit ID` of the bookmark. Empty in case
155 Mapping from name to :term:`Commit ID` of the bookmark. Empty in case
126 there are no bookmarks or the backend implementation does not support
156 there are no bookmarks or the backend implementation does not support
127 bookmarks.
157 bookmarks.
128
158
129 .. attribute:: tags
159 .. attribute:: tags
130
160
131 Mapping from name to :term:`Commit ID` of the tag.
161 Mapping from name to :term:`Commit ID` of the tag.
132
162
133 """
163 """
134
164
135 DEFAULT_BRANCH_NAME = None
165 DEFAULT_BRANCH_NAME = None
136 DEFAULT_CONTACT = u"Unknown"
166 DEFAULT_CONTACT = u"Unknown"
137 DEFAULT_DESCRIPTION = u"unknown"
167 DEFAULT_DESCRIPTION = u"unknown"
138 EMPTY_COMMIT_ID = '0' * 40
168 EMPTY_COMMIT_ID = '0' * 40
139
169
140 path = None
170 path = None
141
171
142 def __init__(self, repo_path, config=None, create=False, **kwargs):
172 def __init__(self, repo_path, config=None, create=False, **kwargs):
143 """
173 """
144 Initializes repository. Raises RepositoryError if repository could
174 Initializes repository. Raises RepositoryError if repository could
145 not be find at the given ``repo_path`` or directory at ``repo_path``
175 not be find at the given ``repo_path`` or directory at ``repo_path``
146 exists and ``create`` is set to True.
176 exists and ``create`` is set to True.
147
177
148 :param repo_path: local path of the repository
178 :param repo_path: local path of the repository
149 :param config: repository configuration
179 :param config: repository configuration
150 :param create=False: if set to True, would try to create repository.
180 :param create=False: if set to True, would try to create repository.
151 :param src_url=None: if set, should be proper url from which repository
181 :param src_url=None: if set, should be proper url from which repository
152 would be cloned; requires ``create`` parameter to be set to True -
182 would be cloned; requires ``create`` parameter to be set to True -
153 raises RepositoryError if src_url is set and create evaluates to
183 raises RepositoryError if src_url is set and create evaluates to
154 False
184 False
155 """
185 """
156 raise NotImplementedError
186 raise NotImplementedError
157
187
158 def __repr__(self):
188 def __repr__(self):
159 return '<%s at %s>' % (self.__class__.__name__, self.path)
189 return '<%s at %s>' % (self.__class__.__name__, self.path)
160
190
161 def __len__(self):
191 def __len__(self):
162 return self.count()
192 return self.count()
163
193
164 def __eq__(self, other):
194 def __eq__(self, other):
165 same_instance = isinstance(other, self.__class__)
195 same_instance = isinstance(other, self.__class__)
166 return same_instance and other.path == self.path
196 return same_instance and other.path == self.path
167
197
168 def __ne__(self, other):
198 def __ne__(self, other):
169 return not self.__eq__(other)
199 return not self.__eq__(other)
170
200
171 @LazyProperty
201 @LazyProperty
172 def EMPTY_COMMIT(self):
202 def EMPTY_COMMIT(self):
173 return EmptyCommit(self.EMPTY_COMMIT_ID)
203 return EmptyCommit(self.EMPTY_COMMIT_ID)
174
204
175 @LazyProperty
205 @LazyProperty
176 def alias(self):
206 def alias(self):
177 for k, v in settings.BACKENDS.items():
207 for k, v in settings.BACKENDS.items():
178 if v.split('.')[-1] == str(self.__class__.__name__):
208 if v.split('.')[-1] == str(self.__class__.__name__):
179 return k
209 return k
180
210
181 @LazyProperty
211 @LazyProperty
182 def name(self):
212 def name(self):
183 return safe_unicode(os.path.basename(self.path))
213 return safe_unicode(os.path.basename(self.path))
184
214
185 @LazyProperty
215 @LazyProperty
186 def description(self):
216 def description(self):
187 raise NotImplementedError
217 raise NotImplementedError
188
218
189 def refs(self):
219 def refs(self):
190 """
220 """
191 returns a `dict` with branches, bookmarks, tags, and closed_branches
221 returns a `dict` with branches, bookmarks, tags, and closed_branches
192 for this repository
222 for this repository
193 """
223 """
194 raise NotImplementedError
224 raise NotImplementedError
195
225
196 @LazyProperty
226 @LazyProperty
197 def branches(self):
227 def branches(self):
198 """
228 """
199 A `dict` which maps branch names to commit ids.
229 A `dict` which maps branch names to commit ids.
200 """
230 """
201 raise NotImplementedError
231 raise NotImplementedError
202
232
203 @LazyProperty
233 @LazyProperty
204 def size(self):
234 def size(self):
205 """
235 """
206 Returns combined size in bytes for all repository files
236 Returns combined size in bytes for all repository files
207 """
237 """
208 tip = self.get_commit()
238 tip = self.get_commit()
209 return tip.size
239 return tip.size
210
240
211 def size_at_commit(self, commit_id):
241 def size_at_commit(self, commit_id):
212 commit = self.get_commit(commit_id)
242 commit = self.get_commit(commit_id)
213 return commit.size
243 return commit.size
214
244
215 def is_empty(self):
245 def is_empty(self):
216 return not bool(self.commit_ids)
246 return not bool(self.commit_ids)
217
247
218 @staticmethod
248 @staticmethod
219 def check_url(url, config):
249 def check_url(url, config):
220 """
250 """
221 Function will check given url and try to verify if it's a valid
251 Function will check given url and try to verify if it's a valid
222 link.
252 link.
223 """
253 """
224 raise NotImplementedError
254 raise NotImplementedError
225
255
226 @staticmethod
256 @staticmethod
227 def is_valid_repository(path):
257 def is_valid_repository(path):
228 """
258 """
229 Check if given `path` contains a valid repository of this backend
259 Check if given `path` contains a valid repository of this backend
230 """
260 """
231 raise NotImplementedError
261 raise NotImplementedError
232
262
233 # ==========================================================================
263 # ==========================================================================
234 # COMMITS
264 # COMMITS
235 # ==========================================================================
265 # ==========================================================================
236
266
237 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
267 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
238 """
268 """
239 Returns instance of `BaseCommit` class. If `commit_id` and `commit_idx`
269 Returns instance of `BaseCommit` class. If `commit_id` and `commit_idx`
240 are both None, most recent commit is returned.
270 are both None, most recent commit is returned.
241
271
242 :param pre_load: Optional. List of commit attributes to load.
272 :param pre_load: Optional. List of commit attributes to load.
243
273
244 :raises ``EmptyRepositoryError``: if there are no commits
274 :raises ``EmptyRepositoryError``: if there are no commits
245 """
275 """
246 raise NotImplementedError
276 raise NotImplementedError
247
277
248 def __iter__(self):
278 def __iter__(self):
249 for commit_id in self.commit_ids:
279 for commit_id in self.commit_ids:
250 yield self.get_commit(commit_id=commit_id)
280 yield self.get_commit(commit_id=commit_id)
251
281
252 def get_commits(
282 def get_commits(
253 self, start_id=None, end_id=None, start_date=None, end_date=None,
283 self, start_id=None, end_id=None, start_date=None, end_date=None,
254 branch_name=None, pre_load=None):
284 branch_name=None, pre_load=None):
255 """
285 """
256 Returns iterator of `BaseCommit` objects from start to end
286 Returns iterator of `BaseCommit` objects from start to end
257 not inclusive. This should behave just like a list, ie. end is not
287 not inclusive. This should behave just like a list, ie. end is not
258 inclusive.
288 inclusive.
259
289
260 :param start_id: None or str, must be a valid commit id
290 :param start_id: None or str, must be a valid commit id
261 :param end_id: None or str, must be a valid commit id
291 :param end_id: None or str, must be a valid commit id
262 :param start_date:
292 :param start_date:
263 :param end_date:
293 :param end_date:
264 :param branch_name:
294 :param branch_name:
265 :param pre_load:
295 :param pre_load:
266 """
296 """
267 raise NotImplementedError
297 raise NotImplementedError
268
298
269 def __getitem__(self, key):
299 def __getitem__(self, key):
270 """
300 """
271 Allows index based access to the commit objects of this repository.
301 Allows index based access to the commit objects of this repository.
272 """
302 """
273 pre_load = ["author", "branch", "date", "message", "parents"]
303 pre_load = ["author", "branch", "date", "message", "parents"]
274 if isinstance(key, slice):
304 if isinstance(key, slice):
275 return self._get_range(key, pre_load)
305 return self._get_range(key, pre_load)
276 return self.get_commit(commit_idx=key, pre_load=pre_load)
306 return self.get_commit(commit_idx=key, pre_load=pre_load)
277
307
278 def _get_range(self, slice_obj, pre_load):
308 def _get_range(self, slice_obj, pre_load):
279 for commit_id in self.commit_ids.__getitem__(slice_obj):
309 for commit_id in self.commit_ids.__getitem__(slice_obj):
280 yield self.get_commit(commit_id=commit_id, pre_load=pre_load)
310 yield self.get_commit(commit_id=commit_id, pre_load=pre_load)
281
311
282 def count(self):
312 def count(self):
283 return len(self.commit_ids)
313 return len(self.commit_ids)
284
314
285 def tag(self, name, user, commit_id=None, message=None, date=None, **opts):
315 def tag(self, name, user, commit_id=None, message=None, date=None, **opts):
286 """
316 """
287 Creates and returns a tag for the given ``commit_id``.
317 Creates and returns a tag for the given ``commit_id``.
288
318
289 :param name: name for new tag
319 :param name: name for new tag
290 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
320 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
291 :param commit_id: commit id for which new tag would be created
321 :param commit_id: commit id for which new tag would be created
292 :param message: message of the tag's commit
322 :param message: message of the tag's commit
293 :param date: date of tag's commit
323 :param date: date of tag's commit
294
324
295 :raises TagAlreadyExistError: if tag with same name already exists
325 :raises TagAlreadyExistError: if tag with same name already exists
296 """
326 """
297 raise NotImplementedError
327 raise NotImplementedError
298
328
299 def remove_tag(self, name, user, message=None, date=None):
329 def remove_tag(self, name, user, message=None, date=None):
300 """
330 """
301 Removes tag with the given ``name``.
331 Removes tag with the given ``name``.
302
332
303 :param name: name of the tag to be removed
333 :param name: name of the tag to be removed
304 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
334 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
305 :param message: message of the tag's removal commit
335 :param message: message of the tag's removal commit
306 :param date: date of tag's removal commit
336 :param date: date of tag's removal commit
307
337
308 :raises TagDoesNotExistError: if tag with given name does not exists
338 :raises TagDoesNotExistError: if tag with given name does not exists
309 """
339 """
310 raise NotImplementedError
340 raise NotImplementedError
311
341
312 def get_diff(
342 def get_diff(
313 self, commit1, commit2, path=None, ignore_whitespace=False,
343 self, commit1, commit2, path=None, ignore_whitespace=False,
314 context=3, path1=None):
344 context=3, path1=None):
315 """
345 """
316 Returns (git like) *diff*, as plain text. Shows changes introduced by
346 Returns (git like) *diff*, as plain text. Shows changes introduced by
317 `commit2` since `commit1`.
347 `commit2` since `commit1`.
318
348
319 :param commit1: Entry point from which diff is shown. Can be
349 :param commit1: Entry point from which diff is shown. Can be
320 ``self.EMPTY_COMMIT`` - in this case, patch showing all
350 ``self.EMPTY_COMMIT`` - in this case, patch showing all
321 the changes since empty state of the repository until `commit2`
351 the changes since empty state of the repository until `commit2`
322 :param commit2: Until which commit changes should be shown.
352 :param commit2: Until which commit changes should be shown.
323 :param path: Can be set to a path of a file to create a diff of that
353 :param path: Can be set to a path of a file to create a diff of that
324 file. If `path1` is also set, this value is only associated to
354 file. If `path1` is also set, this value is only associated to
325 `commit2`.
355 `commit2`.
326 :param ignore_whitespace: If set to ``True``, would not show whitespace
356 :param ignore_whitespace: If set to ``True``, would not show whitespace
327 changes. Defaults to ``False``.
357 changes. Defaults to ``False``.
328 :param context: How many lines before/after changed lines should be
358 :param context: How many lines before/after changed lines should be
329 shown. Defaults to ``3``.
359 shown. Defaults to ``3``.
330 :param path1: Can be set to a path to associate with `commit1`. This
360 :param path1: Can be set to a path to associate with `commit1`. This
331 parameter works only for backends which support diff generation for
361 parameter works only for backends which support diff generation for
332 different paths. Other backends will raise a `ValueError` if `path1`
362 different paths. Other backends will raise a `ValueError` if `path1`
333 is set and has a different value than `path`.
363 is set and has a different value than `path`.
334 """
364 """
335 raise NotImplementedError
365 raise NotImplementedError
336
366
337 def strip(self, commit_id, branch=None):
367 def strip(self, commit_id, branch=None):
338 """
368 """
339 Strip given commit_id from the repository
369 Strip given commit_id from the repository
340 """
370 """
341 raise NotImplementedError
371 raise NotImplementedError
342
372
343 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
373 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
344 """
374 """
345 Return a latest common ancestor commit if one exists for this repo
375 Return a latest common ancestor commit if one exists for this repo
346 `commit_id1` vs `commit_id2` from `repo2`.
376 `commit_id1` vs `commit_id2` from `repo2`.
347
377
348 :param commit_id1: Commit it from this repository to use as a
378 :param commit_id1: Commit it from this repository to use as a
349 target for the comparison.
379 target for the comparison.
350 :param commit_id2: Source commit id to use for comparison.
380 :param commit_id2: Source commit id to use for comparison.
351 :param repo2: Source repository to use for comparison.
381 :param repo2: Source repository to use for comparison.
352 """
382 """
353 raise NotImplementedError
383 raise NotImplementedError
354
384
355 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
385 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
356 """
386 """
357 Compare this repository's revision `commit_id1` with `commit_id2`.
387 Compare this repository's revision `commit_id1` with `commit_id2`.
358
388
359 Returns a tuple(commits, ancestor) that would be merged from
389 Returns a tuple(commits, ancestor) that would be merged from
360 `commit_id2`. Doing a normal compare (``merge=False``), ``None``
390 `commit_id2`. Doing a normal compare (``merge=False``), ``None``
361 will be returned as ancestor.
391 will be returned as ancestor.
362
392
363 :param commit_id1: Commit it from this repository to use as a
393 :param commit_id1: Commit it from this repository to use as a
364 target for the comparison.
394 target for the comparison.
365 :param commit_id2: Source commit id to use for comparison.
395 :param commit_id2: Source commit id to use for comparison.
366 :param repo2: Source repository to use for comparison.
396 :param repo2: Source repository to use for comparison.
367 :param merge: If set to ``True`` will do a merge compare which also
397 :param merge: If set to ``True`` will do a merge compare which also
368 returns the common ancestor.
398 returns the common ancestor.
369 :param pre_load: Optional. List of commit attributes to load.
399 :param pre_load: Optional. List of commit attributes to load.
370 """
400 """
371 raise NotImplementedError
401 raise NotImplementedError
372
402
373 def merge(self, target_ref, source_repo, source_ref, workspace_id,
403 def merge(self, target_ref, source_repo, source_ref, workspace_id,
374 user_name='', user_email='', message='', dry_run=False,
404 user_name='', user_email='', message='', dry_run=False,
375 use_rebase=False):
405 use_rebase=False):
376 """
406 """
377 Merge the revisions specified in `source_ref` from `source_repo`
407 Merge the revisions specified in `source_ref` from `source_repo`
378 onto the `target_ref` of this repository.
408 onto the `target_ref` of this repository.
379
409
380 `source_ref` and `target_ref` are named tupls with the following
410 `source_ref` and `target_ref` are named tupls with the following
381 fields `type`, `name` and `commit_id`.
411 fields `type`, `name` and `commit_id`.
382
412
383 Returns a MergeResponse named tuple with the following fields
413 Returns a MergeResponse named tuple with the following fields
384 'possible', 'executed', 'source_commit', 'target_commit',
414 'possible', 'executed', 'source_commit', 'target_commit',
385 'merge_commit'.
415 'merge_commit'.
386
416
387 :param target_ref: `target_ref` points to the commit on top of which
417 :param target_ref: `target_ref` points to the commit on top of which
388 the `source_ref` should be merged.
418 the `source_ref` should be merged.
389 :param source_repo: The repository that contains the commits to be
419 :param source_repo: The repository that contains the commits to be
390 merged.
420 merged.
391 :param source_ref: `source_ref` points to the topmost commit from
421 :param source_ref: `source_ref` points to the topmost commit from
392 the `source_repo` which should be merged.
422 the `source_repo` which should be merged.
393 :param workspace_id: `workspace_id` unique identifier.
423 :param workspace_id: `workspace_id` unique identifier.
394 :param user_name: Merge commit `user_name`.
424 :param user_name: Merge commit `user_name`.
395 :param user_email: Merge commit `user_email`.
425 :param user_email: Merge commit `user_email`.
396 :param message: Merge commit `message`.
426 :param message: Merge commit `message`.
397 :param dry_run: If `True` the merge will not take place.
427 :param dry_run: If `True` the merge will not take place.
398 :param use_rebase: If `True` commits from the source will be rebased
428 :param use_rebase: If `True` commits from the source will be rebased
399 on top of the target instead of being merged.
429 on top of the target instead of being merged.
400 """
430 """
401 if dry_run:
431 if dry_run:
402 message = message or 'dry_run_merge_message'
432 message = message or 'dry_run_merge_message'
403 user_email = user_email or 'dry-run-merge@rhodecode.com'
433 user_email = user_email or 'dry-run-merge@rhodecode.com'
404 user_name = user_name or 'Dry-Run User'
434 user_name = user_name or 'Dry-Run User'
405 else:
435 else:
406 if not user_name:
436 if not user_name:
407 raise ValueError('user_name cannot be empty')
437 raise ValueError('user_name cannot be empty')
408 if not user_email:
438 if not user_email:
409 raise ValueError('user_email cannot be empty')
439 raise ValueError('user_email cannot be empty')
410 if not message:
440 if not message:
411 raise ValueError('message cannot be empty')
441 raise ValueError('message cannot be empty')
412
442
413 shadow_repository_path = self._maybe_prepare_merge_workspace(
443 shadow_repository_path = self._maybe_prepare_merge_workspace(
414 workspace_id, target_ref)
444 workspace_id, target_ref)
415
445
416 try:
446 try:
417 return self._merge_repo(
447 return self._merge_repo(
418 shadow_repository_path, target_ref, source_repo,
448 shadow_repository_path, target_ref, source_repo,
419 source_ref, message, user_name, user_email, dry_run=dry_run,
449 source_ref, message, user_name, user_email, dry_run=dry_run,
420 use_rebase=use_rebase)
450 use_rebase=use_rebase)
421 except RepositoryError:
451 except RepositoryError:
422 log.exception(
452 log.exception(
423 'Unexpected failure when running merge, dry-run=%s',
453 'Unexpected failure when running merge, dry-run=%s',
424 dry_run)
454 dry_run)
425 return MergeResponse(
455 return MergeResponse(
426 False, False, None, MergeFailureReason.UNKNOWN)
456 False, False, None, MergeFailureReason.UNKNOWN)
427
457
428 def _merge_repo(self, shadow_repository_path, target_ref,
458 def _merge_repo(self, shadow_repository_path, target_ref,
429 source_repo, source_ref, merge_message,
459 source_repo, source_ref, merge_message,
430 merger_name, merger_email, dry_run=False, use_rebase=False):
460 merger_name, merger_email, dry_run=False, use_rebase=False):
431 """Internal implementation of merge."""
461 """Internal implementation of merge."""
432 raise NotImplementedError
462 raise NotImplementedError
433
463
434 def _maybe_prepare_merge_workspace(self, workspace_id, target_ref):
464 def _maybe_prepare_merge_workspace(self, workspace_id, target_ref):
435 """
465 """
436 Create the merge workspace.
466 Create the merge workspace.
437
467
438 :param workspace_id: `workspace_id` unique identifier.
468 :param workspace_id: `workspace_id` unique identifier.
439 """
469 """
440 raise NotImplementedError
470 raise NotImplementedError
441
471
442 def cleanup_merge_workspace(self, workspace_id):
472 def cleanup_merge_workspace(self, workspace_id):
443 """
473 """
444 Remove merge workspace.
474 Remove merge workspace.
445
475
446 This function MUST not fail in case there is no workspace associated to
476 This function MUST not fail in case there is no workspace associated to
447 the given `workspace_id`.
477 the given `workspace_id`.
448
478
449 :param workspace_id: `workspace_id` unique identifier.
479 :param workspace_id: `workspace_id` unique identifier.
450 """
480 """
451 raise NotImplementedError
481 raise NotImplementedError
452
482
453 # ========== #
483 # ========== #
454 # COMMIT API #
484 # COMMIT API #
455 # ========== #
485 # ========== #
456
486
457 @LazyProperty
487 @LazyProperty
458 def in_memory_commit(self):
488 def in_memory_commit(self):
459 """
489 """
460 Returns :class:`InMemoryCommit` object for this repository.
490 Returns :class:`InMemoryCommit` object for this repository.
461 """
491 """
462 raise NotImplementedError
492 raise NotImplementedError
463
493
464 # ======================== #
494 # ======================== #
465 # UTILITIES FOR SUBCLASSES #
495 # UTILITIES FOR SUBCLASSES #
466 # ======================== #
496 # ======================== #
467
497
468 def _validate_diff_commits(self, commit1, commit2):
498 def _validate_diff_commits(self, commit1, commit2):
469 """
499 """
470 Validates that the given commits are related to this repository.
500 Validates that the given commits are related to this repository.
471
501
472 Intended as a utility for sub classes to have a consistent validation
502 Intended as a utility for sub classes to have a consistent validation
473 of input parameters in methods like :meth:`get_diff`.
503 of input parameters in methods like :meth:`get_diff`.
474 """
504 """
475 self._validate_commit(commit1)
505 self._validate_commit(commit1)
476 self._validate_commit(commit2)
506 self._validate_commit(commit2)
477 if (isinstance(commit1, EmptyCommit) and
507 if (isinstance(commit1, EmptyCommit) and
478 isinstance(commit2, EmptyCommit)):
508 isinstance(commit2, EmptyCommit)):
479 raise ValueError("Cannot compare two empty commits")
509 raise ValueError("Cannot compare two empty commits")
480
510
481 def _validate_commit(self, commit):
511 def _validate_commit(self, commit):
482 if not isinstance(commit, BaseCommit):
512 if not isinstance(commit, BaseCommit):
483 raise TypeError(
513 raise TypeError(
484 "%s is not of type BaseCommit" % repr(commit))
514 "%s is not of type BaseCommit" % repr(commit))
485 if commit.repository != self and not isinstance(commit, EmptyCommit):
515 if commit.repository != self and not isinstance(commit, EmptyCommit):
486 raise ValueError(
516 raise ValueError(
487 "Commit %s must be a valid commit from this repository %s, "
517 "Commit %s must be a valid commit from this repository %s, "
488 "related to this repository instead %s." %
518 "related to this repository instead %s." %
489 (commit, self, commit.repository))
519 (commit, self, commit.repository))
490
520
491 def _validate_commit_id(self, commit_id):
521 def _validate_commit_id(self, commit_id):
492 if not isinstance(commit_id, basestring):
522 if not isinstance(commit_id, basestring):
493 raise TypeError("commit_id must be a string value")
523 raise TypeError("commit_id must be a string value")
494
524
495 def _validate_commit_idx(self, commit_idx):
525 def _validate_commit_idx(self, commit_idx):
496 if not isinstance(commit_idx, (int, long)):
526 if not isinstance(commit_idx, (int, long)):
497 raise TypeError("commit_idx must be a numeric value")
527 raise TypeError("commit_idx must be a numeric value")
498
528
499 def _validate_branch_name(self, branch_name):
529 def _validate_branch_name(self, branch_name):
500 if branch_name and branch_name not in self.branches_all:
530 if branch_name and branch_name not in self.branches_all:
501 msg = ("Branch %s not found in %s" % (branch_name, self))
531 msg = ("Branch %s not found in %s" % (branch_name, self))
502 raise BranchDoesNotExistError(msg)
532 raise BranchDoesNotExistError(msg)
503
533
504 #
534 #
505 # Supporting deprecated API parts
535 # Supporting deprecated API parts
506 # TODO: johbo: consider to move this into a mixin
536 # TODO: johbo: consider to move this into a mixin
507 #
537 #
508
538
509 @property
539 @property
510 def EMPTY_CHANGESET(self):
540 def EMPTY_CHANGESET(self):
511 warnings.warn(
541 warnings.warn(
512 "Use EMPTY_COMMIT or EMPTY_COMMIT_ID instead", DeprecationWarning)
542 "Use EMPTY_COMMIT or EMPTY_COMMIT_ID instead", DeprecationWarning)
513 return self.EMPTY_COMMIT_ID
543 return self.EMPTY_COMMIT_ID
514
544
515 @property
545 @property
516 def revisions(self):
546 def revisions(self):
517 warnings.warn("Use commits attribute instead", DeprecationWarning)
547 warnings.warn("Use commits attribute instead", DeprecationWarning)
518 return self.commit_ids
548 return self.commit_ids
519
549
520 @revisions.setter
550 @revisions.setter
521 def revisions(self, value):
551 def revisions(self, value):
522 warnings.warn("Use commits attribute instead", DeprecationWarning)
552 warnings.warn("Use commits attribute instead", DeprecationWarning)
523 self.commit_ids = value
553 self.commit_ids = value
524
554
525 def get_changeset(self, revision=None, pre_load=None):
555 def get_changeset(self, revision=None, pre_load=None):
526 warnings.warn("Use get_commit instead", DeprecationWarning)
556 warnings.warn("Use get_commit instead", DeprecationWarning)
527 commit_id = None
557 commit_id = None
528 commit_idx = None
558 commit_idx = None
529 if isinstance(revision, basestring):
559 if isinstance(revision, basestring):
530 commit_id = revision
560 commit_id = revision
531 else:
561 else:
532 commit_idx = revision
562 commit_idx = revision
533 return self.get_commit(
563 return self.get_commit(
534 commit_id=commit_id, commit_idx=commit_idx, pre_load=pre_load)
564 commit_id=commit_id, commit_idx=commit_idx, pre_load=pre_load)
535
565
536 def get_changesets(
566 def get_changesets(
537 self, start=None, end=None, start_date=None, end_date=None,
567 self, start=None, end=None, start_date=None, end_date=None,
538 branch_name=None, pre_load=None):
568 branch_name=None, pre_load=None):
539 warnings.warn("Use get_commits instead", DeprecationWarning)
569 warnings.warn("Use get_commits instead", DeprecationWarning)
540 start_id = self._revision_to_commit(start)
570 start_id = self._revision_to_commit(start)
541 end_id = self._revision_to_commit(end)
571 end_id = self._revision_to_commit(end)
542 return self.get_commits(
572 return self.get_commits(
543 start_id=start_id, end_id=end_id, start_date=start_date,
573 start_id=start_id, end_id=end_id, start_date=start_date,
544 end_date=end_date, branch_name=branch_name, pre_load=pre_load)
574 end_date=end_date, branch_name=branch_name, pre_load=pre_load)
545
575
546 def _revision_to_commit(self, revision):
576 def _revision_to_commit(self, revision):
547 """
577 """
548 Translates a revision to a commit_id
578 Translates a revision to a commit_id
549
579
550 Helps to support the old changeset based API which allows to use
580 Helps to support the old changeset based API which allows to use
551 commit ids and commit indices interchangeable.
581 commit ids and commit indices interchangeable.
552 """
582 """
553 if revision is None:
583 if revision is None:
554 return revision
584 return revision
555
585
556 if isinstance(revision, basestring):
586 if isinstance(revision, basestring):
557 commit_id = revision
587 commit_id = revision
558 else:
588 else:
559 commit_id = self.commit_ids[revision]
589 commit_id = self.commit_ids[revision]
560 return commit_id
590 return commit_id
561
591
562 @property
592 @property
563 def in_memory_changeset(self):
593 def in_memory_changeset(self):
564 warnings.warn("Use in_memory_commit instead", DeprecationWarning)
594 warnings.warn("Use in_memory_commit instead", DeprecationWarning)
565 return self.in_memory_commit
595 return self.in_memory_commit
566
596
567
597
568 class BaseCommit(object):
598 class BaseCommit(object):
569 """
599 """
570 Each backend should implement it's commit representation.
600 Each backend should implement it's commit representation.
571
601
572 **Attributes**
602 **Attributes**
573
603
574 ``repository``
604 ``repository``
575 repository object within which commit exists
605 repository object within which commit exists
576
606
577 ``id``
607 ``id``
578 The commit id, may be ``raw_id`` or i.e. for mercurial's tip
608 The commit id, may be ``raw_id`` or i.e. for mercurial's tip
579 just ``tip``.
609 just ``tip``.
580
610
581 ``raw_id``
611 ``raw_id``
582 raw commit representation (i.e. full 40 length sha for git
612 raw commit representation (i.e. full 40 length sha for git
583 backend)
613 backend)
584
614
585 ``short_id``
615 ``short_id``
586 shortened (if apply) version of ``raw_id``; it would be simple
616 shortened (if apply) version of ``raw_id``; it would be simple
587 shortcut for ``raw_id[:12]`` for git/mercurial backends or same
617 shortcut for ``raw_id[:12]`` for git/mercurial backends or same
588 as ``raw_id`` for subversion
618 as ``raw_id`` for subversion
589
619
590 ``idx``
620 ``idx``
591 commit index
621 commit index
592
622
593 ``files``
623 ``files``
594 list of ``FileNode`` (``Node`` with NodeKind.FILE) objects
624 list of ``FileNode`` (``Node`` with NodeKind.FILE) objects
595
625
596 ``dirs``
626 ``dirs``
597 list of ``DirNode`` (``Node`` with NodeKind.DIR) objects
627 list of ``DirNode`` (``Node`` with NodeKind.DIR) objects
598
628
599 ``nodes``
629 ``nodes``
600 combined list of ``Node`` objects
630 combined list of ``Node`` objects
601
631
602 ``author``
632 ``author``
603 author of the commit, as unicode
633 author of the commit, as unicode
604
634
605 ``message``
635 ``message``
606 message of the commit, as unicode
636 message of the commit, as unicode
607
637
608 ``parents``
638 ``parents``
609 list of parent commits
639 list of parent commits
610
640
611 """
641 """
612
642
613 branch = None
643 branch = None
614 """
644 """
615 Depending on the backend this should be set to the branch name of the
645 Depending on the backend this should be set to the branch name of the
616 commit. Backends not supporting branches on commits should leave this
646 commit. Backends not supporting branches on commits should leave this
617 value as ``None``.
647 value as ``None``.
618 """
648 """
619
649
620 _ARCHIVE_PREFIX_TEMPLATE = b'{repo_name}-{short_id}'
650 _ARCHIVE_PREFIX_TEMPLATE = b'{repo_name}-{short_id}'
621 """
651 """
622 This template is used to generate a default prefix for repository archives
652 This template is used to generate a default prefix for repository archives
623 if no prefix has been specified.
653 if no prefix has been specified.
624 """
654 """
625
655
626 def __str__(self):
656 def __str__(self):
627 return '<%s at %s:%s>' % (
657 return '<%s at %s:%s>' % (
628 self.__class__.__name__, self.idx, self.short_id)
658 self.__class__.__name__, self.idx, self.short_id)
629
659
630 def __repr__(self):
660 def __repr__(self):
631 return self.__str__()
661 return self.__str__()
632
662
633 def __unicode__(self):
663 def __unicode__(self):
634 return u'%s:%s' % (self.idx, self.short_id)
664 return u'%s:%s' % (self.idx, self.short_id)
635
665
636 def __eq__(self, other):
666 def __eq__(self, other):
637 same_instance = isinstance(other, self.__class__)
667 same_instance = isinstance(other, self.__class__)
638 return same_instance and self.raw_id == other.raw_id
668 return same_instance and self.raw_id == other.raw_id
639
669
640 def __json__(self):
670 def __json__(self):
641 parents = []
671 parents = []
642 try:
672 try:
643 for parent in self.parents:
673 for parent in self.parents:
644 parents.append({'raw_id': parent.raw_id})
674 parents.append({'raw_id': parent.raw_id})
645 except NotImplementedError:
675 except NotImplementedError:
646 # empty commit doesn't have parents implemented
676 # empty commit doesn't have parents implemented
647 pass
677 pass
648
678
649 return {
679 return {
650 'short_id': self.short_id,
680 'short_id': self.short_id,
651 'raw_id': self.raw_id,
681 'raw_id': self.raw_id,
652 'revision': self.idx,
682 'revision': self.idx,
653 'message': self.message,
683 'message': self.message,
654 'date': self.date,
684 'date': self.date,
655 'author': self.author,
685 'author': self.author,
656 'parents': parents,
686 'parents': parents,
657 'branch': self.branch
687 'branch': self.branch
658 }
688 }
659
689
660 @LazyProperty
690 @LazyProperty
661 def last(self):
691 def last(self):
662 """
692 """
663 ``True`` if this is last commit in repository, ``False``
693 ``True`` if this is last commit in repository, ``False``
664 otherwise; trying to access this attribute while there is no
694 otherwise; trying to access this attribute while there is no
665 commits would raise `EmptyRepositoryError`
695 commits would raise `EmptyRepositoryError`
666 """
696 """
667 if self.repository is None:
697 if self.repository is None:
668 raise CommitError("Cannot check if it's most recent commit")
698 raise CommitError("Cannot check if it's most recent commit")
669 return self.raw_id == self.repository.commit_ids[-1]
699 return self.raw_id == self.repository.commit_ids[-1]
670
700
671 @LazyProperty
701 @LazyProperty
672 def parents(self):
702 def parents(self):
673 """
703 """
674 Returns list of parent commits.
704 Returns list of parent commits.
675 """
705 """
676 raise NotImplementedError
706 raise NotImplementedError
677
707
678 @property
708 @property
679 def merge(self):
709 def merge(self):
680 """
710 """
681 Returns boolean if commit is a merge.
711 Returns boolean if commit is a merge.
682 """
712 """
683 return len(self.parents) > 1
713 return len(self.parents) > 1
684
714
685 @LazyProperty
715 @LazyProperty
686 def children(self):
716 def children(self):
687 """
717 """
688 Returns list of child commits.
718 Returns list of child commits.
689 """
719 """
690 raise NotImplementedError
720 raise NotImplementedError
691
721
692 @LazyProperty
722 @LazyProperty
693 def id(self):
723 def id(self):
694 """
724 """
695 Returns string identifying this commit.
725 Returns string identifying this commit.
696 """
726 """
697 raise NotImplementedError
727 raise NotImplementedError
698
728
699 @LazyProperty
729 @LazyProperty
700 def raw_id(self):
730 def raw_id(self):
701 """
731 """
702 Returns raw string identifying this commit.
732 Returns raw string identifying this commit.
703 """
733 """
704 raise NotImplementedError
734 raise NotImplementedError
705
735
706 @LazyProperty
736 @LazyProperty
707 def short_id(self):
737 def short_id(self):
708 """
738 """
709 Returns shortened version of ``raw_id`` attribute, as string,
739 Returns shortened version of ``raw_id`` attribute, as string,
710 identifying this commit, useful for presentation to users.
740 identifying this commit, useful for presentation to users.
711 """
741 """
712 raise NotImplementedError
742 raise NotImplementedError
713
743
714 @LazyProperty
744 @LazyProperty
715 def idx(self):
745 def idx(self):
716 """
746 """
717 Returns integer identifying this commit.
747 Returns integer identifying this commit.
718 """
748 """
719 raise NotImplementedError
749 raise NotImplementedError
720
750
721 @LazyProperty
751 @LazyProperty
722 def committer(self):
752 def committer(self):
723 """
753 """
724 Returns committer for this commit
754 Returns committer for this commit
725 """
755 """
726 raise NotImplementedError
756 raise NotImplementedError
727
757
728 @LazyProperty
758 @LazyProperty
729 def committer_name(self):
759 def committer_name(self):
730 """
760 """
731 Returns committer name for this commit
761 Returns committer name for this commit
732 """
762 """
733
763
734 return author_name(self.committer)
764 return author_name(self.committer)
735
765
736 @LazyProperty
766 @LazyProperty
737 def committer_email(self):
767 def committer_email(self):
738 """
768 """
739 Returns committer email address for this commit
769 Returns committer email address for this commit
740 """
770 """
741
771
742 return author_email(self.committer)
772 return author_email(self.committer)
743
773
744 @LazyProperty
774 @LazyProperty
745 def author(self):
775 def author(self):
746 """
776 """
747 Returns author for this commit
777 Returns author for this commit
748 """
778 """
749
779
750 raise NotImplementedError
780 raise NotImplementedError
751
781
752 @LazyProperty
782 @LazyProperty
753 def author_name(self):
783 def author_name(self):
754 """
784 """
755 Returns author name for this commit
785 Returns author name for this commit
756 """
786 """
757
787
758 return author_name(self.author)
788 return author_name(self.author)
759
789
760 @LazyProperty
790 @LazyProperty
761 def author_email(self):
791 def author_email(self):
762 """
792 """
763 Returns author email address for this commit
793 Returns author email address for this commit
764 """
794 """
765
795
766 return author_email(self.author)
796 return author_email(self.author)
767
797
768 def get_file_mode(self, path):
798 def get_file_mode(self, path):
769 """
799 """
770 Returns stat mode of the file at `path`.
800 Returns stat mode of the file at `path`.
771 """
801 """
772 raise NotImplementedError
802 raise NotImplementedError
773
803
774 def is_link(self, path):
804 def is_link(self, path):
775 """
805 """
776 Returns ``True`` if given `path` is a symlink
806 Returns ``True`` if given `path` is a symlink
777 """
807 """
778 raise NotImplementedError
808 raise NotImplementedError
779
809
780 def get_file_content(self, path):
810 def get_file_content(self, path):
781 """
811 """
782 Returns content of the file at the given `path`.
812 Returns content of the file at the given `path`.
783 """
813 """
784 raise NotImplementedError
814 raise NotImplementedError
785
815
786 def get_file_size(self, path):
816 def get_file_size(self, path):
787 """
817 """
788 Returns size of the file at the given `path`.
818 Returns size of the file at the given `path`.
789 """
819 """
790 raise NotImplementedError
820 raise NotImplementedError
791
821
792 def get_file_commit(self, path, pre_load=None):
822 def get_file_commit(self, path, pre_load=None):
793 """
823 """
794 Returns last commit of the file at the given `path`.
824 Returns last commit of the file at the given `path`.
795
825
796 :param pre_load: Optional. List of commit attributes to load.
826 :param pre_load: Optional. List of commit attributes to load.
797 """
827 """
798 return self.get_file_history(path, limit=1, pre_load=pre_load)[0]
828 return self.get_file_history(path, limit=1, pre_load=pre_load)[0]
799
829
800 def get_file_history(self, path, limit=None, pre_load=None):
830 def get_file_history(self, path, limit=None, pre_load=None):
801 """
831 """
802 Returns history of file as reversed list of :class:`BaseCommit`
832 Returns history of file as reversed list of :class:`BaseCommit`
803 objects for which file at given `path` has been modified.
833 objects for which file at given `path` has been modified.
804
834
805 :param limit: Optional. Allows to limit the size of the returned
835 :param limit: Optional. Allows to limit the size of the returned
806 history. This is intended as a hint to the underlying backend, so
836 history. This is intended as a hint to the underlying backend, so
807 that it can apply optimizations depending on the limit.
837 that it can apply optimizations depending on the limit.
808 :param pre_load: Optional. List of commit attributes to load.
838 :param pre_load: Optional. List of commit attributes to load.
809 """
839 """
810 raise NotImplementedError
840 raise NotImplementedError
811
841
812 def get_file_annotate(self, path, pre_load=None):
842 def get_file_annotate(self, path, pre_load=None):
813 """
843 """
814 Returns a generator of four element tuples with
844 Returns a generator of four element tuples with
815 lineno, sha, commit lazy loader and line
845 lineno, sha, commit lazy loader and line
816
846
817 :param pre_load: Optional. List of commit attributes to load.
847 :param pre_load: Optional. List of commit attributes to load.
818 """
848 """
819 raise NotImplementedError
849 raise NotImplementedError
820
850
821 def get_nodes(self, path):
851 def get_nodes(self, path):
822 """
852 """
823 Returns combined ``DirNode`` and ``FileNode`` objects list representing
853 Returns combined ``DirNode`` and ``FileNode`` objects list representing
824 state of commit at the given ``path``.
854 state of commit at the given ``path``.
825
855
826 :raises ``CommitError``: if node at the given ``path`` is not
856 :raises ``CommitError``: if node at the given ``path`` is not
827 instance of ``DirNode``
857 instance of ``DirNode``
828 """
858 """
829 raise NotImplementedError
859 raise NotImplementedError
830
860
831 def get_node(self, path):
861 def get_node(self, path):
832 """
862 """
833 Returns ``Node`` object from the given ``path``.
863 Returns ``Node`` object from the given ``path``.
834
864
835 :raises ``NodeDoesNotExistError``: if there is no node at the given
865 :raises ``NodeDoesNotExistError``: if there is no node at the given
836 ``path``
866 ``path``
837 """
867 """
838 raise NotImplementedError
868 raise NotImplementedError
839
869
840 def get_largefile_node(self, path):
870 def get_largefile_node(self, path):
841 """
871 """
842 Returns the path to largefile from Mercurial storage.
872 Returns the path to largefile from Mercurial storage.
843 """
873 """
844 raise NotImplementedError
874 raise NotImplementedError
845
875
846 def archive_repo(self, file_path, kind='tgz', subrepos=None,
876 def archive_repo(self, file_path, kind='tgz', subrepos=None,
847 prefix=None, write_metadata=False, mtime=None):
877 prefix=None, write_metadata=False, mtime=None):
848 """
878 """
849 Creates an archive containing the contents of the repository.
879 Creates an archive containing the contents of the repository.
850
880
851 :param file_path: path to the file which to create the archive.
881 :param file_path: path to the file which to create the archive.
852 :param kind: one of following: ``"tbz2"``, ``"tgz"``, ``"zip"``.
882 :param kind: one of following: ``"tbz2"``, ``"tgz"``, ``"zip"``.
853 :param prefix: name of root directory in archive.
883 :param prefix: name of root directory in archive.
854 Default is repository name and commit's short_id joined with dash:
884 Default is repository name and commit's short_id joined with dash:
855 ``"{repo_name}-{short_id}"``.
885 ``"{repo_name}-{short_id}"``.
856 :param write_metadata: write a metadata file into archive.
886 :param write_metadata: write a metadata file into archive.
857 :param mtime: custom modification time for archive creation, defaults
887 :param mtime: custom modification time for archive creation, defaults
858 to time.time() if not given.
888 to time.time() if not given.
859
889
860 :raise VCSError: If prefix has a problem.
890 :raise VCSError: If prefix has a problem.
861 """
891 """
862 allowed_kinds = settings.ARCHIVE_SPECS.keys()
892 allowed_kinds = settings.ARCHIVE_SPECS.keys()
863 if kind not in allowed_kinds:
893 if kind not in allowed_kinds:
864 raise ImproperArchiveTypeError(
894 raise ImproperArchiveTypeError(
865 'Archive kind (%s) not supported use one of %s' %
895 'Archive kind (%s) not supported use one of %s' %
866 (kind, allowed_kinds))
896 (kind, allowed_kinds))
867
897
868 prefix = self._validate_archive_prefix(prefix)
898 prefix = self._validate_archive_prefix(prefix)
869
899
870 mtime = mtime or time.mktime(self.date.timetuple())
900 mtime = mtime or time.mktime(self.date.timetuple())
871
901
872 file_info = []
902 file_info = []
873 cur_rev = self.repository.get_commit(commit_id=self.raw_id)
903 cur_rev = self.repository.get_commit(commit_id=self.raw_id)
874 for _r, _d, files in cur_rev.walk('/'):
904 for _r, _d, files in cur_rev.walk('/'):
875 for f in files:
905 for f in files:
876 f_path = os.path.join(prefix, f.path)
906 f_path = os.path.join(prefix, f.path)
877 file_info.append(
907 file_info.append(
878 (f_path, f.mode, f.is_link(), f.raw_bytes))
908 (f_path, f.mode, f.is_link(), f.raw_bytes))
879
909
880 if write_metadata:
910 if write_metadata:
881 metadata = [
911 metadata = [
882 ('repo_name', self.repository.name),
912 ('repo_name', self.repository.name),
883 ('rev', self.raw_id),
913 ('rev', self.raw_id),
884 ('create_time', mtime),
914 ('create_time', mtime),
885 ('branch', self.branch),
915 ('branch', self.branch),
886 ('tags', ','.join(self.tags)),
916 ('tags', ','.join(self.tags)),
887 ]
917 ]
888 meta = ["%s:%s" % (f_name, value) for f_name, value in metadata]
918 meta = ["%s:%s" % (f_name, value) for f_name, value in metadata]
889 file_info.append(('.archival.txt', 0644, False, '\n'.join(meta)))
919 file_info.append(('.archival.txt', 0644, False, '\n'.join(meta)))
890
920
891 connection.Hg.archive_repo(file_path, mtime, file_info, kind)
921 connection.Hg.archive_repo(file_path, mtime, file_info, kind)
892
922
893 def _validate_archive_prefix(self, prefix):
923 def _validate_archive_prefix(self, prefix):
894 if prefix is None:
924 if prefix is None:
895 prefix = self._ARCHIVE_PREFIX_TEMPLATE.format(
925 prefix = self._ARCHIVE_PREFIX_TEMPLATE.format(
896 repo_name=safe_str(self.repository.name),
926 repo_name=safe_str(self.repository.name),
897 short_id=self.short_id)
927 short_id=self.short_id)
898 elif not isinstance(prefix, str):
928 elif not isinstance(prefix, str):
899 raise ValueError("prefix not a bytes object: %s" % repr(prefix))
929 raise ValueError("prefix not a bytes object: %s" % repr(prefix))
900 elif prefix.startswith('/'):
930 elif prefix.startswith('/'):
901 raise VCSError("Prefix cannot start with leading slash")
931 raise VCSError("Prefix cannot start with leading slash")
902 elif prefix.strip() == '':
932 elif prefix.strip() == '':
903 raise VCSError("Prefix cannot be empty")
933 raise VCSError("Prefix cannot be empty")
904 return prefix
934 return prefix
905
935
906 @LazyProperty
936 @LazyProperty
907 def root(self):
937 def root(self):
908 """
938 """
909 Returns ``RootNode`` object for this commit.
939 Returns ``RootNode`` object for this commit.
910 """
940 """
911 return self.get_node('')
941 return self.get_node('')
912
942
913 def next(self, branch=None):
943 def next(self, branch=None):
914 """
944 """
915 Returns next commit from current, if branch is gives it will return
945 Returns next commit from current, if branch is gives it will return
916 next commit belonging to this branch
946 next commit belonging to this branch
917
947
918 :param branch: show commits within the given named branch
948 :param branch: show commits within the given named branch
919 """
949 """
920 indexes = xrange(self.idx + 1, self.repository.count())
950 indexes = xrange(self.idx + 1, self.repository.count())
921 return self._find_next(indexes, branch)
951 return self._find_next(indexes, branch)
922
952
923 def prev(self, branch=None):
953 def prev(self, branch=None):
924 """
954 """
925 Returns previous commit from current, if branch is gives it will
955 Returns previous commit from current, if branch is gives it will
926 return previous commit belonging to this branch
956 return previous commit belonging to this branch
927
957
928 :param branch: show commit within the given named branch
958 :param branch: show commit within the given named branch
929 """
959 """
930 indexes = xrange(self.idx - 1, -1, -1)
960 indexes = xrange(self.idx - 1, -1, -1)
931 return self._find_next(indexes, branch)
961 return self._find_next(indexes, branch)
932
962
933 def _find_next(self, indexes, branch=None):
963 def _find_next(self, indexes, branch=None):
934 if branch and self.branch != branch:
964 if branch and self.branch != branch:
935 raise VCSError('Branch option used on commit not belonging '
965 raise VCSError('Branch option used on commit not belonging '
936 'to that branch')
966 'to that branch')
937
967
938 for next_idx in indexes:
968 for next_idx in indexes:
939 commit = self.repository.get_commit(commit_idx=next_idx)
969 commit = self.repository.get_commit(commit_idx=next_idx)
940 if branch and branch != commit.branch:
970 if branch and branch != commit.branch:
941 continue
971 continue
942 return commit
972 return commit
943 raise CommitDoesNotExistError
973 raise CommitDoesNotExistError
944
974
945 def diff(self, ignore_whitespace=True, context=3):
975 def diff(self, ignore_whitespace=True, context=3):
946 """
976 """
947 Returns a `Diff` object representing the change made by this commit.
977 Returns a `Diff` object representing the change made by this commit.
948 """
978 """
949 parent = (
979 parent = (
950 self.parents[0] if self.parents else self.repository.EMPTY_COMMIT)
980 self.parents[0] if self.parents else self.repository.EMPTY_COMMIT)
951 diff = self.repository.get_diff(
981 diff = self.repository.get_diff(
952 parent, self,
982 parent, self,
953 ignore_whitespace=ignore_whitespace,
983 ignore_whitespace=ignore_whitespace,
954 context=context)
984 context=context)
955 return diff
985 return diff
956
986
957 @LazyProperty
987 @LazyProperty
958 def added(self):
988 def added(self):
959 """
989 """
960 Returns list of added ``FileNode`` objects.
990 Returns list of added ``FileNode`` objects.
961 """
991 """
962 raise NotImplementedError
992 raise NotImplementedError
963
993
964 @LazyProperty
994 @LazyProperty
965 def changed(self):
995 def changed(self):
966 """
996 """
967 Returns list of modified ``FileNode`` objects.
997 Returns list of modified ``FileNode`` objects.
968 """
998 """
969 raise NotImplementedError
999 raise NotImplementedError
970
1000
971 @LazyProperty
1001 @LazyProperty
972 def removed(self):
1002 def removed(self):
973 """
1003 """
974 Returns list of removed ``FileNode`` objects.
1004 Returns list of removed ``FileNode`` objects.
975 """
1005 """
976 raise NotImplementedError
1006 raise NotImplementedError
977
1007
978 @LazyProperty
1008 @LazyProperty
979 def size(self):
1009 def size(self):
980 """
1010 """
981 Returns total number of bytes from contents of all filenodes.
1011 Returns total number of bytes from contents of all filenodes.
982 """
1012 """
983 return sum((node.size for node in self.get_filenodes_generator()))
1013 return sum((node.size for node in self.get_filenodes_generator()))
984
1014
985 def walk(self, topurl=''):
1015 def walk(self, topurl=''):
986 """
1016 """
987 Similar to os.walk method. Insted of filesystem it walks through
1017 Similar to os.walk method. Insted of filesystem it walks through
988 commit starting at given ``topurl``. Returns generator of tuples
1018 commit starting at given ``topurl``. Returns generator of tuples
989 (topnode, dirnodes, filenodes).
1019 (topnode, dirnodes, filenodes).
990 """
1020 """
991 topnode = self.get_node(topurl)
1021 topnode = self.get_node(topurl)
992 if not topnode.is_dir():
1022 if not topnode.is_dir():
993 return
1023 return
994 yield (topnode, topnode.dirs, topnode.files)
1024 yield (topnode, topnode.dirs, topnode.files)
995 for dirnode in topnode.dirs:
1025 for dirnode in topnode.dirs:
996 for tup in self.walk(dirnode.path):
1026 for tup in self.walk(dirnode.path):
997 yield tup
1027 yield tup
998
1028
999 def get_filenodes_generator(self):
1029 def get_filenodes_generator(self):
1000 """
1030 """
1001 Returns generator that yields *all* file nodes.
1031 Returns generator that yields *all* file nodes.
1002 """
1032 """
1003 for topnode, dirs, files in self.walk():
1033 for topnode, dirs, files in self.walk():
1004 for node in files:
1034 for node in files:
1005 yield node
1035 yield node
1006
1036
1007 #
1037 #
1008 # Utilities for sub classes to support consistent behavior
1038 # Utilities for sub classes to support consistent behavior
1009 #
1039 #
1010
1040
1011 def no_node_at_path(self, path):
1041 def no_node_at_path(self, path):
1012 return NodeDoesNotExistError(
1042 return NodeDoesNotExistError(
1013 "There is no file nor directory at the given path: "
1043 "There is no file nor directory at the given path: "
1014 "'%s' at commit %s" % (path, self.short_id))
1044 "'%s' at commit %s" % (path, self.short_id))
1015
1045
1016 def _fix_path(self, path):
1046 def _fix_path(self, path):
1017 """
1047 """
1018 Paths are stored without trailing slash so we need to get rid off it if
1048 Paths are stored without trailing slash so we need to get rid off it if
1019 needed.
1049 needed.
1020 """
1050 """
1021 return path.rstrip('/')
1051 return path.rstrip('/')
1022
1052
1023 #
1053 #
1024 # Deprecated API based on changesets
1054 # Deprecated API based on changesets
1025 #
1055 #
1026
1056
1027 @property
1057 @property
1028 def revision(self):
1058 def revision(self):
1029 warnings.warn("Use idx instead", DeprecationWarning)
1059 warnings.warn("Use idx instead", DeprecationWarning)
1030 return self.idx
1060 return self.idx
1031
1061
1032 @revision.setter
1062 @revision.setter
1033 def revision(self, value):
1063 def revision(self, value):
1034 warnings.warn("Use idx instead", DeprecationWarning)
1064 warnings.warn("Use idx instead", DeprecationWarning)
1035 self.idx = value
1065 self.idx = value
1036
1066
1037 def get_file_changeset(self, path):
1067 def get_file_changeset(self, path):
1038 warnings.warn("Use get_file_commit instead", DeprecationWarning)
1068 warnings.warn("Use get_file_commit instead", DeprecationWarning)
1039 return self.get_file_commit(path)
1069 return self.get_file_commit(path)
1040
1070
1041
1071
1042 class BaseChangesetClass(type):
1072 class BaseChangesetClass(type):
1043
1073
1044 def __instancecheck__(self, instance):
1074 def __instancecheck__(self, instance):
1045 return isinstance(instance, BaseCommit)
1075 return isinstance(instance, BaseCommit)
1046
1076
1047
1077
1048 class BaseChangeset(BaseCommit):
1078 class BaseChangeset(BaseCommit):
1049
1079
1050 __metaclass__ = BaseChangesetClass
1080 __metaclass__ = BaseChangesetClass
1051
1081
1052 def __new__(cls, *args, **kwargs):
1082 def __new__(cls, *args, **kwargs):
1053 warnings.warn(
1083 warnings.warn(
1054 "Use BaseCommit instead of BaseChangeset", DeprecationWarning)
1084 "Use BaseCommit instead of BaseChangeset", DeprecationWarning)
1055 return super(BaseChangeset, cls).__new__(cls, *args, **kwargs)
1085 return super(BaseChangeset, cls).__new__(cls, *args, **kwargs)
1056
1086
1057
1087
1058 class BaseInMemoryCommit(object):
1088 class BaseInMemoryCommit(object):
1059 """
1089 """
1060 Represents differences between repository's state (most recent head) and
1090 Represents differences between repository's state (most recent head) and
1061 changes made *in place*.
1091 changes made *in place*.
1062
1092
1063 **Attributes**
1093 **Attributes**
1064
1094
1065 ``repository``
1095 ``repository``
1066 repository object for this in-memory-commit
1096 repository object for this in-memory-commit
1067
1097
1068 ``added``
1098 ``added``
1069 list of ``FileNode`` objects marked as *added*
1099 list of ``FileNode`` objects marked as *added*
1070
1100
1071 ``changed``
1101 ``changed``
1072 list of ``FileNode`` objects marked as *changed*
1102 list of ``FileNode`` objects marked as *changed*
1073
1103
1074 ``removed``
1104 ``removed``
1075 list of ``FileNode`` or ``RemovedFileNode`` objects marked to be
1105 list of ``FileNode`` or ``RemovedFileNode`` objects marked to be
1076 *removed*
1106 *removed*
1077
1107
1078 ``parents``
1108 ``parents``
1079 list of :class:`BaseCommit` instances representing parents of
1109 list of :class:`BaseCommit` instances representing parents of
1080 in-memory commit. Should always be 2-element sequence.
1110 in-memory commit. Should always be 2-element sequence.
1081
1111
1082 """
1112 """
1083
1113
1084 def __init__(self, repository):
1114 def __init__(self, repository):
1085 self.repository = repository
1115 self.repository = repository
1086 self.added = []
1116 self.added = []
1087 self.changed = []
1117 self.changed = []
1088 self.removed = []
1118 self.removed = []
1089 self.parents = []
1119 self.parents = []
1090
1120
1091 def add(self, *filenodes):
1121 def add(self, *filenodes):
1092 """
1122 """
1093 Marks given ``FileNode`` objects as *to be committed*.
1123 Marks given ``FileNode`` objects as *to be committed*.
1094
1124
1095 :raises ``NodeAlreadyExistsError``: if node with same path exists at
1125 :raises ``NodeAlreadyExistsError``: if node with same path exists at
1096 latest commit
1126 latest commit
1097 :raises ``NodeAlreadyAddedError``: if node with same path is already
1127 :raises ``NodeAlreadyAddedError``: if node with same path is already
1098 marked as *added*
1128 marked as *added*
1099 """
1129 """
1100 # Check if not already marked as *added* first
1130 # Check if not already marked as *added* first
1101 for node in filenodes:
1131 for node in filenodes:
1102 if node.path in (n.path for n in self.added):
1132 if node.path in (n.path for n in self.added):
1103 raise NodeAlreadyAddedError(
1133 raise NodeAlreadyAddedError(
1104 "Such FileNode %s is already marked for addition"
1134 "Such FileNode %s is already marked for addition"
1105 % node.path)
1135 % node.path)
1106 for node in filenodes:
1136 for node in filenodes:
1107 self.added.append(node)
1137 self.added.append(node)
1108
1138
1109 def change(self, *filenodes):
1139 def change(self, *filenodes):
1110 """
1140 """
1111 Marks given ``FileNode`` objects to be *changed* in next commit.
1141 Marks given ``FileNode`` objects to be *changed* in next commit.
1112
1142
1113 :raises ``EmptyRepositoryError``: if there are no commits yet
1143 :raises ``EmptyRepositoryError``: if there are no commits yet
1114 :raises ``NodeAlreadyExistsError``: if node with same path is already
1144 :raises ``NodeAlreadyExistsError``: if node with same path is already
1115 marked to be *changed*
1145 marked to be *changed*
1116 :raises ``NodeAlreadyRemovedError``: if node with same path is already
1146 :raises ``NodeAlreadyRemovedError``: if node with same path is already
1117 marked to be *removed*
1147 marked to be *removed*
1118 :raises ``NodeDoesNotExistError``: if node doesn't exist in latest
1148 :raises ``NodeDoesNotExistError``: if node doesn't exist in latest
1119 commit
1149 commit
1120 :raises ``NodeNotChangedError``: if node hasn't really be changed
1150 :raises ``NodeNotChangedError``: if node hasn't really be changed
1121 """
1151 """
1122 for node in filenodes:
1152 for node in filenodes:
1123 if node.path in (n.path for n in self.removed):
1153 if node.path in (n.path for n in self.removed):
1124 raise NodeAlreadyRemovedError(
1154 raise NodeAlreadyRemovedError(
1125 "Node at %s is already marked as removed" % node.path)
1155 "Node at %s is already marked as removed" % node.path)
1126 try:
1156 try:
1127 self.repository.get_commit()
1157 self.repository.get_commit()
1128 except EmptyRepositoryError:
1158 except EmptyRepositoryError:
1129 raise EmptyRepositoryError(
1159 raise EmptyRepositoryError(
1130 "Nothing to change - try to *add* new nodes rather than "
1160 "Nothing to change - try to *add* new nodes rather than "
1131 "changing them")
1161 "changing them")
1132 for node in filenodes:
1162 for node in filenodes:
1133 if node.path in (n.path for n in self.changed):
1163 if node.path in (n.path for n in self.changed):
1134 raise NodeAlreadyChangedError(
1164 raise NodeAlreadyChangedError(
1135 "Node at '%s' is already marked as changed" % node.path)
1165 "Node at '%s' is already marked as changed" % node.path)
1136 self.changed.append(node)
1166 self.changed.append(node)
1137
1167
1138 def remove(self, *filenodes):
1168 def remove(self, *filenodes):
1139 """
1169 """
1140 Marks given ``FileNode`` (or ``RemovedFileNode``) objects to be
1170 Marks given ``FileNode`` (or ``RemovedFileNode``) objects to be
1141 *removed* in next commit.
1171 *removed* in next commit.
1142
1172
1143 :raises ``NodeAlreadyRemovedError``: if node has been already marked to
1173 :raises ``NodeAlreadyRemovedError``: if node has been already marked to
1144 be *removed*
1174 be *removed*
1145 :raises ``NodeAlreadyChangedError``: if node has been already marked to
1175 :raises ``NodeAlreadyChangedError``: if node has been already marked to
1146 be *changed*
1176 be *changed*
1147 """
1177 """
1148 for node in filenodes:
1178 for node in filenodes:
1149 if node.path in (n.path for n in self.removed):
1179 if node.path in (n.path for n in self.removed):
1150 raise NodeAlreadyRemovedError(
1180 raise NodeAlreadyRemovedError(
1151 "Node is already marked to for removal at %s" % node.path)
1181 "Node is already marked to for removal at %s" % node.path)
1152 if node.path in (n.path for n in self.changed):
1182 if node.path in (n.path for n in self.changed):
1153 raise NodeAlreadyChangedError(
1183 raise NodeAlreadyChangedError(
1154 "Node is already marked to be changed at %s" % node.path)
1184 "Node is already marked to be changed at %s" % node.path)
1155 # We only mark node as *removed* - real removal is done by
1185 # We only mark node as *removed* - real removal is done by
1156 # commit method
1186 # commit method
1157 self.removed.append(node)
1187 self.removed.append(node)
1158
1188
1159 def reset(self):
1189 def reset(self):
1160 """
1190 """
1161 Resets this instance to initial state (cleans ``added``, ``changed``
1191 Resets this instance to initial state (cleans ``added``, ``changed``
1162 and ``removed`` lists).
1192 and ``removed`` lists).
1163 """
1193 """
1164 self.added = []
1194 self.added = []
1165 self.changed = []
1195 self.changed = []
1166 self.removed = []
1196 self.removed = []
1167 self.parents = []
1197 self.parents = []
1168
1198
1169 def get_ipaths(self):
1199 def get_ipaths(self):
1170 """
1200 """
1171 Returns generator of paths from nodes marked as added, changed or
1201 Returns generator of paths from nodes marked as added, changed or
1172 removed.
1202 removed.
1173 """
1203 """
1174 for node in itertools.chain(self.added, self.changed, self.removed):
1204 for node in itertools.chain(self.added, self.changed, self.removed):
1175 yield node.path
1205 yield node.path
1176
1206
1177 def get_paths(self):
1207 def get_paths(self):
1178 """
1208 """
1179 Returns list of paths from nodes marked as added, changed or removed.
1209 Returns list of paths from nodes marked as added, changed or removed.
1180 """
1210 """
1181 return list(self.get_ipaths())
1211 return list(self.get_ipaths())
1182
1212
1183 def check_integrity(self, parents=None):
1213 def check_integrity(self, parents=None):
1184 """
1214 """
1185 Checks in-memory commit's integrity. Also, sets parents if not
1215 Checks in-memory commit's integrity. Also, sets parents if not
1186 already set.
1216 already set.
1187
1217
1188 :raises CommitError: if any error occurs (i.e.
1218 :raises CommitError: if any error occurs (i.e.
1189 ``NodeDoesNotExistError``).
1219 ``NodeDoesNotExistError``).
1190 """
1220 """
1191 if not self.parents:
1221 if not self.parents:
1192 parents = parents or []
1222 parents = parents or []
1193 if len(parents) == 0:
1223 if len(parents) == 0:
1194 try:
1224 try:
1195 parents = [self.repository.get_commit(), None]
1225 parents = [self.repository.get_commit(), None]
1196 except EmptyRepositoryError:
1226 except EmptyRepositoryError:
1197 parents = [None, None]
1227 parents = [None, None]
1198 elif len(parents) == 1:
1228 elif len(parents) == 1:
1199 parents += [None]
1229 parents += [None]
1200 self.parents = parents
1230 self.parents = parents
1201
1231
1202 # Local parents, only if not None
1232 # Local parents, only if not None
1203 parents = [p for p in self.parents if p]
1233 parents = [p for p in self.parents if p]
1204
1234
1205 # Check nodes marked as added
1235 # Check nodes marked as added
1206 for p in parents:
1236 for p in parents:
1207 for node in self.added:
1237 for node in self.added:
1208 try:
1238 try:
1209 p.get_node(node.path)
1239 p.get_node(node.path)
1210 except NodeDoesNotExistError:
1240 except NodeDoesNotExistError:
1211 pass
1241 pass
1212 else:
1242 else:
1213 raise NodeAlreadyExistsError(
1243 raise NodeAlreadyExistsError(
1214 "Node `%s` already exists at %s" % (node.path, p))
1244 "Node `%s` already exists at %s" % (node.path, p))
1215
1245
1216 # Check nodes marked as changed
1246 # Check nodes marked as changed
1217 missing = set(self.changed)
1247 missing = set(self.changed)
1218 not_changed = set(self.changed)
1248 not_changed = set(self.changed)
1219 if self.changed and not parents:
1249 if self.changed and not parents:
1220 raise NodeDoesNotExistError(str(self.changed[0].path))
1250 raise NodeDoesNotExistError(str(self.changed[0].path))
1221 for p in parents:
1251 for p in parents:
1222 for node in self.changed:
1252 for node in self.changed:
1223 try:
1253 try:
1224 old = p.get_node(node.path)
1254 old = p.get_node(node.path)
1225 missing.remove(node)
1255 missing.remove(node)
1226 # if content actually changed, remove node from not_changed
1256 # if content actually changed, remove node from not_changed
1227 if old.content != node.content:
1257 if old.content != node.content:
1228 not_changed.remove(node)
1258 not_changed.remove(node)
1229 except NodeDoesNotExistError:
1259 except NodeDoesNotExistError:
1230 pass
1260 pass
1231 if self.changed and missing:
1261 if self.changed and missing:
1232 raise NodeDoesNotExistError(
1262 raise NodeDoesNotExistError(
1233 "Node `%s` marked as modified but missing in parents: %s"
1263 "Node `%s` marked as modified but missing in parents: %s"
1234 % (node.path, parents))
1264 % (node.path, parents))
1235
1265
1236 if self.changed and not_changed:
1266 if self.changed and not_changed:
1237 raise NodeNotChangedError(
1267 raise NodeNotChangedError(
1238 "Node `%s` wasn't actually changed (parents: %s)"
1268 "Node `%s` wasn't actually changed (parents: %s)"
1239 % (not_changed.pop().path, parents))
1269 % (not_changed.pop().path, parents))
1240
1270
1241 # Check nodes marked as removed
1271 # Check nodes marked as removed
1242 if self.removed and not parents:
1272 if self.removed and not parents:
1243 raise NodeDoesNotExistError(
1273 raise NodeDoesNotExistError(
1244 "Cannot remove node at %s as there "
1274 "Cannot remove node at %s as there "
1245 "were no parents specified" % self.removed[0].path)
1275 "were no parents specified" % self.removed[0].path)
1246 really_removed = set()
1276 really_removed = set()
1247 for p in parents:
1277 for p in parents:
1248 for node in self.removed:
1278 for node in self.removed:
1249 try:
1279 try:
1250 p.get_node(node.path)
1280 p.get_node(node.path)
1251 really_removed.add(node)
1281 really_removed.add(node)
1252 except CommitError:
1282 except CommitError:
1253 pass
1283 pass
1254 not_removed = set(self.removed) - really_removed
1284 not_removed = set(self.removed) - really_removed
1255 if not_removed:
1285 if not_removed:
1256 # TODO: johbo: This code branch does not seem to be covered
1286 # TODO: johbo: This code branch does not seem to be covered
1257 raise NodeDoesNotExistError(
1287 raise NodeDoesNotExistError(
1258 "Cannot remove node at %s from "
1288 "Cannot remove node at %s from "
1259 "following parents: %s" % (not_removed, parents))
1289 "following parents: %s" % (not_removed, parents))
1260
1290
1261 def commit(
1291 def commit(
1262 self, message, author, parents=None, branch=None, date=None,
1292 self, message, author, parents=None, branch=None, date=None,
1263 **kwargs):
1293 **kwargs):
1264 """
1294 """
1265 Performs in-memory commit (doesn't check workdir in any way) and
1295 Performs in-memory commit (doesn't check workdir in any way) and
1266 returns newly created :class:`BaseCommit`. Updates repository's
1296 returns newly created :class:`BaseCommit`. Updates repository's
1267 attribute `commits`.
1297 attribute `commits`.
1268
1298
1269 .. note::
1299 .. note::
1270
1300
1271 While overriding this method each backend's should call
1301 While overriding this method each backend's should call
1272 ``self.check_integrity(parents)`` in the first place.
1302 ``self.check_integrity(parents)`` in the first place.
1273
1303
1274 :param message: message of the commit
1304 :param message: message of the commit
1275 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
1305 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
1276 :param parents: single parent or sequence of parents from which commit
1306 :param parents: single parent or sequence of parents from which commit
1277 would be derived
1307 would be derived
1278 :param date: ``datetime.datetime`` instance. Defaults to
1308 :param date: ``datetime.datetime`` instance. Defaults to
1279 ``datetime.datetime.now()``.
1309 ``datetime.datetime.now()``.
1280 :param branch: branch name, as string. If none given, default backend's
1310 :param branch: branch name, as string. If none given, default backend's
1281 branch would be used.
1311 branch would be used.
1282
1312
1283 :raises ``CommitError``: if any error occurs while committing
1313 :raises ``CommitError``: if any error occurs while committing
1284 """
1314 """
1285 raise NotImplementedError
1315 raise NotImplementedError
1286
1316
1287
1317
1288 class BaseInMemoryChangesetClass(type):
1318 class BaseInMemoryChangesetClass(type):
1289
1319
1290 def __instancecheck__(self, instance):
1320 def __instancecheck__(self, instance):
1291 return isinstance(instance, BaseInMemoryCommit)
1321 return isinstance(instance, BaseInMemoryCommit)
1292
1322
1293
1323
1294 class BaseInMemoryChangeset(BaseInMemoryCommit):
1324 class BaseInMemoryChangeset(BaseInMemoryCommit):
1295
1325
1296 __metaclass__ = BaseInMemoryChangesetClass
1326 __metaclass__ = BaseInMemoryChangesetClass
1297
1327
1298 def __new__(cls, *args, **kwargs):
1328 def __new__(cls, *args, **kwargs):
1299 warnings.warn(
1329 warnings.warn(
1300 "Use BaseCommit instead of BaseInMemoryCommit", DeprecationWarning)
1330 "Use BaseCommit instead of BaseInMemoryCommit", DeprecationWarning)
1301 return super(BaseInMemoryChangeset, cls).__new__(cls, *args, **kwargs)
1331 return super(BaseInMemoryChangeset, cls).__new__(cls, *args, **kwargs)
1302
1332
1303
1333
1304 class EmptyCommit(BaseCommit):
1334 class EmptyCommit(BaseCommit):
1305 """
1335 """
1306 An dummy empty commit. It's possible to pass hash when creating
1336 An dummy empty commit. It's possible to pass hash when creating
1307 an EmptyCommit
1337 an EmptyCommit
1308 """
1338 """
1309
1339
1310 def __init__(
1340 def __init__(
1311 self, commit_id='0' * 40, repo=None, alias=None, idx=-1,
1341 self, commit_id='0' * 40, repo=None, alias=None, idx=-1,
1312 message='', author='', date=None):
1342 message='', author='', date=None):
1313 self._empty_commit_id = commit_id
1343 self._empty_commit_id = commit_id
1314 # TODO: johbo: Solve idx parameter, default value does not make
1344 # TODO: johbo: Solve idx parameter, default value does not make
1315 # too much sense
1345 # too much sense
1316 self.idx = idx
1346 self.idx = idx
1317 self.message = message
1347 self.message = message
1318 self.author = author
1348 self.author = author
1319 self.date = date or datetime.datetime.fromtimestamp(0)
1349 self.date = date or datetime.datetime.fromtimestamp(0)
1320 self.repository = repo
1350 self.repository = repo
1321 self.alias = alias
1351 self.alias = alias
1322
1352
1323 @LazyProperty
1353 @LazyProperty
1324 def raw_id(self):
1354 def raw_id(self):
1325 """
1355 """
1326 Returns raw string identifying this commit, useful for web
1356 Returns raw string identifying this commit, useful for web
1327 representation.
1357 representation.
1328 """
1358 """
1329
1359
1330 return self._empty_commit_id
1360 return self._empty_commit_id
1331
1361
1332 @LazyProperty
1362 @LazyProperty
1333 def branch(self):
1363 def branch(self):
1334 if self.alias:
1364 if self.alias:
1335 from rhodecode.lib.vcs.backends import get_backend
1365 from rhodecode.lib.vcs.backends import get_backend
1336 return get_backend(self.alias).DEFAULT_BRANCH_NAME
1366 return get_backend(self.alias).DEFAULT_BRANCH_NAME
1337
1367
1338 @LazyProperty
1368 @LazyProperty
1339 def short_id(self):
1369 def short_id(self):
1340 return self.raw_id[:12]
1370 return self.raw_id[:12]
1341
1371
1342 @LazyProperty
1372 @LazyProperty
1343 def id(self):
1373 def id(self):
1344 return self.raw_id
1374 return self.raw_id
1345
1375
1346 def get_file_commit(self, path):
1376 def get_file_commit(self, path):
1347 return self
1377 return self
1348
1378
1349 def get_file_content(self, path):
1379 def get_file_content(self, path):
1350 return u''
1380 return u''
1351
1381
1352 def get_file_size(self, path):
1382 def get_file_size(self, path):
1353 return 0
1383 return 0
1354
1384
1355
1385
1356 class EmptyChangesetClass(type):
1386 class EmptyChangesetClass(type):
1357
1387
1358 def __instancecheck__(self, instance):
1388 def __instancecheck__(self, instance):
1359 return isinstance(instance, EmptyCommit)
1389 return isinstance(instance, EmptyCommit)
1360
1390
1361
1391
1362 class EmptyChangeset(EmptyCommit):
1392 class EmptyChangeset(EmptyCommit):
1363
1393
1364 __metaclass__ = EmptyChangesetClass
1394 __metaclass__ = EmptyChangesetClass
1365
1395
1366 def __new__(cls, *args, **kwargs):
1396 def __new__(cls, *args, **kwargs):
1367 warnings.warn(
1397 warnings.warn(
1368 "Use EmptyCommit instead of EmptyChangeset", DeprecationWarning)
1398 "Use EmptyCommit instead of EmptyChangeset", DeprecationWarning)
1369 return super(EmptyCommit, cls).__new__(cls, *args, **kwargs)
1399 return super(EmptyCommit, cls).__new__(cls, *args, **kwargs)
1370
1400
1371 def __init__(self, cs='0' * 40, repo=None, requested_revision=None,
1401 def __init__(self, cs='0' * 40, repo=None, requested_revision=None,
1372 alias=None, revision=-1, message='', author='', date=None):
1402 alias=None, revision=-1, message='', author='', date=None):
1373 if requested_revision is not None:
1403 if requested_revision is not None:
1374 warnings.warn(
1404 warnings.warn(
1375 "Parameter requested_revision not supported anymore",
1405 "Parameter requested_revision not supported anymore",
1376 DeprecationWarning)
1406 DeprecationWarning)
1377 super(EmptyChangeset, self).__init__(
1407 super(EmptyChangeset, self).__init__(
1378 commit_id=cs, repo=repo, alias=alias, idx=revision,
1408 commit_id=cs, repo=repo, alias=alias, idx=revision,
1379 message=message, author=author, date=date)
1409 message=message, author=author, date=date)
1380
1410
1381 @property
1411 @property
1382 def revision(self):
1412 def revision(self):
1383 warnings.warn("Use idx instead", DeprecationWarning)
1413 warnings.warn("Use idx instead", DeprecationWarning)
1384 return self.idx
1414 return self.idx
1385
1415
1386 @revision.setter
1416 @revision.setter
1387 def revision(self, value):
1417 def revision(self, value):
1388 warnings.warn("Use idx instead", DeprecationWarning)
1418 warnings.warn("Use idx instead", DeprecationWarning)
1389 self.idx = value
1419 self.idx = value
1390
1420
1391
1421
1392 class CollectionGenerator(object):
1422 class CollectionGenerator(object):
1393
1423
1394 def __init__(self, repo, commit_ids, collection_size=None, pre_load=None):
1424 def __init__(self, repo, commit_ids, collection_size=None, pre_load=None):
1395 self.repo = repo
1425 self.repo = repo
1396 self.commit_ids = commit_ids
1426 self.commit_ids = commit_ids
1397 # TODO: (oliver) this isn't currently hooked up
1427 # TODO: (oliver) this isn't currently hooked up
1398 self.collection_size = None
1428 self.collection_size = None
1399 self.pre_load = pre_load
1429 self.pre_load = pre_load
1400
1430
1401 def __len__(self):
1431 def __len__(self):
1402 if self.collection_size is not None:
1432 if self.collection_size is not None:
1403 return self.collection_size
1433 return self.collection_size
1404 return self.commit_ids.__len__()
1434 return self.commit_ids.__len__()
1405
1435
1406 def __iter__(self):
1436 def __iter__(self):
1407 for commit_id in self.commit_ids:
1437 for commit_id in self.commit_ids:
1408 # TODO: johbo: Mercurial passes in commit indices or commit ids
1438 # TODO: johbo: Mercurial passes in commit indices or commit ids
1409 yield self._commit_factory(commit_id)
1439 yield self._commit_factory(commit_id)
1410
1440
1411 def _commit_factory(self, commit_id):
1441 def _commit_factory(self, commit_id):
1412 """
1442 """
1413 Allows backends to override the way commits are generated.
1443 Allows backends to override the way commits are generated.
1414 """
1444 """
1415 return self.repo.get_commit(commit_id=commit_id,
1445 return self.repo.get_commit(commit_id=commit_id,
1416 pre_load=self.pre_load)
1446 pre_load=self.pre_load)
1417
1447
1418 def __getslice__(self, i, j):
1448 def __getslice__(self, i, j):
1419 """
1449 """
1420 Returns an iterator of sliced repository
1450 Returns an iterator of sliced repository
1421 """
1451 """
1422 commit_ids = self.commit_ids[i:j]
1452 commit_ids = self.commit_ids[i:j]
1423 return self.__class__(
1453 return self.__class__(
1424 self.repo, commit_ids, pre_load=self.pre_load)
1454 self.repo, commit_ids, pre_load=self.pre_load)
1425
1455
1426 def __repr__(self):
1456 def __repr__(self):
1427 return '<CollectionGenerator[len:%s]>' % (self.__len__())
1457 return '<CollectionGenerator[len:%s]>' % (self.__len__())
1428
1458
1429
1459
1430 class Config(object):
1460 class Config(object):
1431 """
1461 """
1432 Represents the configuration for a repository.
1462 Represents the configuration for a repository.
1433
1463
1434 The API is inspired by :class:`ConfigParser.ConfigParser` from the
1464 The API is inspired by :class:`ConfigParser.ConfigParser` from the
1435 standard library. It implements only the needed subset.
1465 standard library. It implements only the needed subset.
1436 """
1466 """
1437
1467
1438 def __init__(self):
1468 def __init__(self):
1439 self._values = {}
1469 self._values = {}
1440
1470
1441 def copy(self):
1471 def copy(self):
1442 clone = Config()
1472 clone = Config()
1443 for section, values in self._values.items():
1473 for section, values in self._values.items():
1444 clone._values[section] = values.copy()
1474 clone._values[section] = values.copy()
1445 return clone
1475 return clone
1446
1476
1447 def __repr__(self):
1477 def __repr__(self):
1448 return '<Config(%s sections) at %s>' % (
1478 return '<Config(%s sections) at %s>' % (
1449 len(self._values), hex(id(self)))
1479 len(self._values), hex(id(self)))
1450
1480
1451 def items(self, section):
1481 def items(self, section):
1452 return self._values.get(section, {}).iteritems()
1482 return self._values.get(section, {}).iteritems()
1453
1483
1454 def get(self, section, option):
1484 def get(self, section, option):
1455 return self._values.get(section, {}).get(option)
1485 return self._values.get(section, {}).get(option)
1456
1486
1457 def set(self, section, option, value):
1487 def set(self, section, option, value):
1458 section_values = self._values.setdefault(section, {})
1488 section_values = self._values.setdefault(section, {})
1459 section_values[option] = value
1489 section_values[option] = value
1460
1490
1461 def clear_section(self, section):
1491 def clear_section(self, section):
1462 self._values[section] = {}
1492 self._values[section] = {}
1463
1493
1464 def serialize(self):
1494 def serialize(self):
1465 """
1495 """
1466 Creates a list of three tuples (section, key, value) representing
1496 Creates a list of three tuples (section, key, value) representing
1467 this config object.
1497 this config object.
1468 """
1498 """
1469 items = []
1499 items = []
1470 for section in self._values:
1500 for section in self._values:
1471 for option, value in self._values[section].items():
1501 for option, value in self._values[section].items():
1472 items.append(
1502 items.append(
1473 (safe_str(section), safe_str(option), safe_str(value)))
1503 (safe_str(section), safe_str(option), safe_str(value)))
1474 return items
1504 return items
1475
1505
1476
1506
1477 class Diff(object):
1507 class Diff(object):
1478 """
1508 """
1479 Represents a diff result from a repository backend.
1509 Represents a diff result from a repository backend.
1480
1510
1481 Subclasses have to provide a backend specific value for :attr:`_header_re`.
1511 Subclasses have to provide a backend specific value for :attr:`_header_re`.
1482 """
1512 """
1483
1513
1484 _header_re = None
1514 _header_re = None
1485
1515
1486 def __init__(self, raw_diff):
1516 def __init__(self, raw_diff):
1487 self.raw = raw_diff
1517 self.raw = raw_diff
1488
1518
1489 def chunks(self):
1519 def chunks(self):
1490 """
1520 """
1491 split the diff in chunks of separate --git a/file b/file chunks
1521 split the diff in chunks of separate --git a/file b/file chunks
1492 to make diffs consistent we must prepend with \n, and make sure
1522 to make diffs consistent we must prepend with \n, and make sure
1493 we can detect last chunk as this was also has special rule
1523 we can detect last chunk as this was also has special rule
1494 """
1524 """
1495 chunks = ('\n' + self.raw).split('\ndiff --git')[1:]
1525 chunks = ('\n' + self.raw).split('\ndiff --git')[1:]
1496 total_chunks = len(chunks)
1526 total_chunks = len(chunks)
1497 return (DiffChunk(chunk, self, cur_chunk == total_chunks)
1527 return (DiffChunk(chunk, self, cur_chunk == total_chunks)
1498 for cur_chunk, chunk in enumerate(chunks, start=1))
1528 for cur_chunk, chunk in enumerate(chunks, start=1))
1499
1529
1500
1530
1501 class DiffChunk(object):
1531 class DiffChunk(object):
1502
1532
1503 def __init__(self, chunk, diff, last_chunk):
1533 def __init__(self, chunk, diff, last_chunk):
1504 self._diff = diff
1534 self._diff = diff
1505
1535
1506 # since we split by \ndiff --git that part is lost from original diff
1536 # since we split by \ndiff --git that part is lost from original diff
1507 # we need to re-apply it at the end, EXCEPT ! if it's last chunk
1537 # we need to re-apply it at the end, EXCEPT ! if it's last chunk
1508 if not last_chunk:
1538 if not last_chunk:
1509 chunk += '\n'
1539 chunk += '\n'
1510
1540
1511 match = self._diff._header_re.match(chunk)
1541 match = self._diff._header_re.match(chunk)
1512 self.header = match.groupdict()
1542 self.header = match.groupdict()
1513 self.diff = chunk[match.end():]
1543 self.diff = chunk[match.end():]
1514 self.raw = chunk
1544 self.raw = chunk
General Comments 0
You need to be logged in to leave comments. Login now