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