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