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