##// END OF EJS Templates
vcs: add equality testing for commits against non commits to allow...
dan -
r985:6ce2682d default
parent child Browse files
Show More
@@ -1,1507 +1,1508 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 'dry_run_merge_message'
396 message = message or 'dry_run_merge_message'
397 user_email = user_email or 'dry-run-merge@rhodecode.com'
397 user_email = user_email or 'dry-run-merge@rhodecode.com'
398 user_name = user_name or 'Dry-Run User'
398 user_name = user_name or 'Dry-Run User'
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, use_rebase=False):
424 merger_name, merger_email, dry_run=False, use_rebase=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 same_instance = isinstance(other, self.__class__)
632 return same_instance and self.raw_id == other.raw_id
632
633
633 def __json__(self):
634 def __json__(self):
634 parents = []
635 parents = []
635 try:
636 try:
636 for parent in self.parents:
637 for parent in self.parents:
637 parents.append({'raw_id': parent.raw_id})
638 parents.append({'raw_id': parent.raw_id})
638 except NotImplementedError:
639 except NotImplementedError:
639 # empty commit doesn't have parents implemented
640 # empty commit doesn't have parents implemented
640 pass
641 pass
641
642
642 return {
643 return {
643 'short_id': self.short_id,
644 'short_id': self.short_id,
644 'raw_id': self.raw_id,
645 'raw_id': self.raw_id,
645 'revision': self.idx,
646 'revision': self.idx,
646 'message': self.message,
647 'message': self.message,
647 'date': self.date,
648 'date': self.date,
648 'author': self.author,
649 'author': self.author,
649 'parents': parents,
650 'parents': parents,
650 'branch': self.branch
651 'branch': self.branch
651 }
652 }
652
653
653 @LazyProperty
654 @LazyProperty
654 def last(self):
655 def last(self):
655 """
656 """
656 ``True`` if this is last commit in repository, ``False``
657 ``True`` if this is last commit in repository, ``False``
657 otherwise; trying to access this attribute while there is no
658 otherwise; trying to access this attribute while there is no
658 commits would raise `EmptyRepositoryError`
659 commits would raise `EmptyRepositoryError`
659 """
660 """
660 if self.repository is None:
661 if self.repository is None:
661 raise CommitError("Cannot check if it's most recent commit")
662 raise CommitError("Cannot check if it's most recent commit")
662 return self.raw_id == self.repository.commit_ids[-1]
663 return self.raw_id == self.repository.commit_ids[-1]
663
664
664 @LazyProperty
665 @LazyProperty
665 def parents(self):
666 def parents(self):
666 """
667 """
667 Returns list of parent commits.
668 Returns list of parent commits.
668 """
669 """
669 raise NotImplementedError
670 raise NotImplementedError
670
671
671 @property
672 @property
672 def merge(self):
673 def merge(self):
673 """
674 """
674 Returns boolean if commit is a merge.
675 Returns boolean if commit is a merge.
675 """
676 """
676 return len(self.parents) > 1
677 return len(self.parents) > 1
677
678
678 @LazyProperty
679 @LazyProperty
679 def children(self):
680 def children(self):
680 """
681 """
681 Returns list of child commits.
682 Returns list of child commits.
682 """
683 """
683 raise NotImplementedError
684 raise NotImplementedError
684
685
685 @LazyProperty
686 @LazyProperty
686 def id(self):
687 def id(self):
687 """
688 """
688 Returns string identifying this commit.
689 Returns string identifying this commit.
689 """
690 """
690 raise NotImplementedError
691 raise NotImplementedError
691
692
692 @LazyProperty
693 @LazyProperty
693 def raw_id(self):
694 def raw_id(self):
694 """
695 """
695 Returns raw string identifying this commit.
696 Returns raw string identifying this commit.
696 """
697 """
697 raise NotImplementedError
698 raise NotImplementedError
698
699
699 @LazyProperty
700 @LazyProperty
700 def short_id(self):
701 def short_id(self):
701 """
702 """
702 Returns shortened version of ``raw_id`` attribute, as string,
703 Returns shortened version of ``raw_id`` attribute, as string,
703 identifying this commit, useful for presentation to users.
704 identifying this commit, useful for presentation to users.
704 """
705 """
705 raise NotImplementedError
706 raise NotImplementedError
706
707
707 @LazyProperty
708 @LazyProperty
708 def idx(self):
709 def idx(self):
709 """
710 """
710 Returns integer identifying this commit.
711 Returns integer identifying this commit.
711 """
712 """
712 raise NotImplementedError
713 raise NotImplementedError
713
714
714 @LazyProperty
715 @LazyProperty
715 def committer(self):
716 def committer(self):
716 """
717 """
717 Returns committer for this commit
718 Returns committer for this commit
718 """
719 """
719 raise NotImplementedError
720 raise NotImplementedError
720
721
721 @LazyProperty
722 @LazyProperty
722 def committer_name(self):
723 def committer_name(self):
723 """
724 """
724 Returns committer name for this commit
725 Returns committer name for this commit
725 """
726 """
726
727
727 return author_name(self.committer)
728 return author_name(self.committer)
728
729
729 @LazyProperty
730 @LazyProperty
730 def committer_email(self):
731 def committer_email(self):
731 """
732 """
732 Returns committer email address for this commit
733 Returns committer email address for this commit
733 """
734 """
734
735
735 return author_email(self.committer)
736 return author_email(self.committer)
736
737
737 @LazyProperty
738 @LazyProperty
738 def author(self):
739 def author(self):
739 """
740 """
740 Returns author for this commit
741 Returns author for this commit
741 """
742 """
742
743
743 raise NotImplementedError
744 raise NotImplementedError
744
745
745 @LazyProperty
746 @LazyProperty
746 def author_name(self):
747 def author_name(self):
747 """
748 """
748 Returns author name for this commit
749 Returns author name for this commit
749 """
750 """
750
751
751 return author_name(self.author)
752 return author_name(self.author)
752
753
753 @LazyProperty
754 @LazyProperty
754 def author_email(self):
755 def author_email(self):
755 """
756 """
756 Returns author email address for this commit
757 Returns author email address for this commit
757 """
758 """
758
759
759 return author_email(self.author)
760 return author_email(self.author)
760
761
761 def get_file_mode(self, path):
762 def get_file_mode(self, path):
762 """
763 """
763 Returns stat mode of the file at `path`.
764 Returns stat mode of the file at `path`.
764 """
765 """
765 raise NotImplementedError
766 raise NotImplementedError
766
767
767 def is_link(self, path):
768 def is_link(self, path):
768 """
769 """
769 Returns ``True`` if given `path` is a symlink
770 Returns ``True`` if given `path` is a symlink
770 """
771 """
771 raise NotImplementedError
772 raise NotImplementedError
772
773
773 def get_file_content(self, path):
774 def get_file_content(self, path):
774 """
775 """
775 Returns content of the file at the given `path`.
776 Returns content of the file at the given `path`.
776 """
777 """
777 raise NotImplementedError
778 raise NotImplementedError
778
779
779 def get_file_size(self, path):
780 def get_file_size(self, path):
780 """
781 """
781 Returns size of the file at the given `path`.
782 Returns size of the file at the given `path`.
782 """
783 """
783 raise NotImplementedError
784 raise NotImplementedError
784
785
785 def get_file_commit(self, path, pre_load=None):
786 def get_file_commit(self, path, pre_load=None):
786 """
787 """
787 Returns last commit of the file at the given `path`.
788 Returns last commit of the file at the given `path`.
788
789
789 :param pre_load: Optional. List of commit attributes to load.
790 :param pre_load: Optional. List of commit attributes to load.
790 """
791 """
791 return self.get_file_history(path, limit=1, pre_load=pre_load)[0]
792 return self.get_file_history(path, limit=1, pre_load=pre_load)[0]
792
793
793 def get_file_history(self, path, limit=None, pre_load=None):
794 def get_file_history(self, path, limit=None, pre_load=None):
794 """
795 """
795 Returns history of file as reversed list of :class:`BaseCommit`
796 Returns history of file as reversed list of :class:`BaseCommit`
796 objects for which file at given `path` has been modified.
797 objects for which file at given `path` has been modified.
797
798
798 :param limit: Optional. Allows to limit the size of the returned
799 :param limit: Optional. Allows to limit the size of the returned
799 history. This is intended as a hint to the underlying backend, so
800 history. This is intended as a hint to the underlying backend, so
800 that it can apply optimizations depending on the limit.
801 that it can apply optimizations depending on the limit.
801 :param pre_load: Optional. List of commit attributes to load.
802 :param pre_load: Optional. List of commit attributes to load.
802 """
803 """
803 raise NotImplementedError
804 raise NotImplementedError
804
805
805 def get_file_annotate(self, path, pre_load=None):
806 def get_file_annotate(self, path, pre_load=None):
806 """
807 """
807 Returns a generator of four element tuples with
808 Returns a generator of four element tuples with
808 lineno, sha, commit lazy loader and line
809 lineno, sha, commit lazy loader and line
809
810
810 :param pre_load: Optional. List of commit attributes to load.
811 :param pre_load: Optional. List of commit attributes to load.
811 """
812 """
812 raise NotImplementedError
813 raise NotImplementedError
813
814
814 def get_nodes(self, path):
815 def get_nodes(self, path):
815 """
816 """
816 Returns combined ``DirNode`` and ``FileNode`` objects list representing
817 Returns combined ``DirNode`` and ``FileNode`` objects list representing
817 state of commit at the given ``path``.
818 state of commit at the given ``path``.
818
819
819 :raises ``CommitError``: if node at the given ``path`` is not
820 :raises ``CommitError``: if node at the given ``path`` is not
820 instance of ``DirNode``
821 instance of ``DirNode``
821 """
822 """
822 raise NotImplementedError
823 raise NotImplementedError
823
824
824 def get_node(self, path):
825 def get_node(self, path):
825 """
826 """
826 Returns ``Node`` object from the given ``path``.
827 Returns ``Node`` object from the given ``path``.
827
828
828 :raises ``NodeDoesNotExistError``: if there is no node at the given
829 :raises ``NodeDoesNotExistError``: if there is no node at the given
829 ``path``
830 ``path``
830 """
831 """
831 raise NotImplementedError
832 raise NotImplementedError
832
833
833 def get_largefile_node(self, path):
834 def get_largefile_node(self, path):
834 """
835 """
835 Returns the path to largefile from Mercurial storage.
836 Returns the path to largefile from Mercurial storage.
836 """
837 """
837 raise NotImplementedError
838 raise NotImplementedError
838
839
839 def archive_repo(self, file_path, kind='tgz', subrepos=None,
840 def archive_repo(self, file_path, kind='tgz', subrepos=None,
840 prefix=None, write_metadata=False, mtime=None):
841 prefix=None, write_metadata=False, mtime=None):
841 """
842 """
842 Creates an archive containing the contents of the repository.
843 Creates an archive containing the contents of the repository.
843
844
844 :param file_path: path to the file which to create the archive.
845 :param file_path: path to the file which to create the archive.
845 :param kind: one of following: ``"tbz2"``, ``"tgz"``, ``"zip"``.
846 :param kind: one of following: ``"tbz2"``, ``"tgz"``, ``"zip"``.
846 :param prefix: name of root directory in archive.
847 :param prefix: name of root directory in archive.
847 Default is repository name and commit's short_id joined with dash:
848 Default is repository name and commit's short_id joined with dash:
848 ``"{repo_name}-{short_id}"``.
849 ``"{repo_name}-{short_id}"``.
849 :param write_metadata: write a metadata file into archive.
850 :param write_metadata: write a metadata file into archive.
850 :param mtime: custom modification time for archive creation, defaults
851 :param mtime: custom modification time for archive creation, defaults
851 to time.time() if not given.
852 to time.time() if not given.
852
853
853 :raise VCSError: If prefix has a problem.
854 :raise VCSError: If prefix has a problem.
854 """
855 """
855 allowed_kinds = settings.ARCHIVE_SPECS.keys()
856 allowed_kinds = settings.ARCHIVE_SPECS.keys()
856 if kind not in allowed_kinds:
857 if kind not in allowed_kinds:
857 raise ImproperArchiveTypeError(
858 raise ImproperArchiveTypeError(
858 'Archive kind (%s) not supported use one of %s' %
859 'Archive kind (%s) not supported use one of %s' %
859 (kind, allowed_kinds))
860 (kind, allowed_kinds))
860
861
861 prefix = self._validate_archive_prefix(prefix)
862 prefix = self._validate_archive_prefix(prefix)
862
863
863 mtime = mtime or time.mktime(self.date.timetuple())
864 mtime = mtime or time.mktime(self.date.timetuple())
864
865
865 file_info = []
866 file_info = []
866 cur_rev = self.repository.get_commit(commit_id=self.raw_id)
867 cur_rev = self.repository.get_commit(commit_id=self.raw_id)
867 for _r, _d, files in cur_rev.walk('/'):
868 for _r, _d, files in cur_rev.walk('/'):
868 for f in files:
869 for f in files:
869 f_path = os.path.join(prefix, f.path)
870 f_path = os.path.join(prefix, f.path)
870 file_info.append(
871 file_info.append(
871 (f_path, f.mode, f.is_link(), f.raw_bytes))
872 (f_path, f.mode, f.is_link(), f.raw_bytes))
872
873
873 if write_metadata:
874 if write_metadata:
874 metadata = [
875 metadata = [
875 ('repo_name', self.repository.name),
876 ('repo_name', self.repository.name),
876 ('rev', self.raw_id),
877 ('rev', self.raw_id),
877 ('create_time', mtime),
878 ('create_time', mtime),
878 ('branch', self.branch),
879 ('branch', self.branch),
879 ('tags', ','.join(self.tags)),
880 ('tags', ','.join(self.tags)),
880 ]
881 ]
881 meta = ["%s:%s" % (f_name, value) for f_name, value in metadata]
882 meta = ["%s:%s" % (f_name, value) for f_name, value in metadata]
882 file_info.append(('.archival.txt', 0644, False, '\n'.join(meta)))
883 file_info.append(('.archival.txt', 0644, False, '\n'.join(meta)))
883
884
884 connection.Hg.archive_repo(file_path, mtime, file_info, kind)
885 connection.Hg.archive_repo(file_path, mtime, file_info, kind)
885
886
886 def _validate_archive_prefix(self, prefix):
887 def _validate_archive_prefix(self, prefix):
887 if prefix is None:
888 if prefix is None:
888 prefix = self._ARCHIVE_PREFIX_TEMPLATE.format(
889 prefix = self._ARCHIVE_PREFIX_TEMPLATE.format(
889 repo_name=safe_str(self.repository.name),
890 repo_name=safe_str(self.repository.name),
890 short_id=self.short_id)
891 short_id=self.short_id)
891 elif not isinstance(prefix, str):
892 elif not isinstance(prefix, str):
892 raise ValueError("prefix not a bytes object: %s" % repr(prefix))
893 raise ValueError("prefix not a bytes object: %s" % repr(prefix))
893 elif prefix.startswith('/'):
894 elif prefix.startswith('/'):
894 raise VCSError("Prefix cannot start with leading slash")
895 raise VCSError("Prefix cannot start with leading slash")
895 elif prefix.strip() == '':
896 elif prefix.strip() == '':
896 raise VCSError("Prefix cannot be empty")
897 raise VCSError("Prefix cannot be empty")
897 return prefix
898 return prefix
898
899
899 @LazyProperty
900 @LazyProperty
900 def root(self):
901 def root(self):
901 """
902 """
902 Returns ``RootNode`` object for this commit.
903 Returns ``RootNode`` object for this commit.
903 """
904 """
904 return self.get_node('')
905 return self.get_node('')
905
906
906 def next(self, branch=None):
907 def next(self, branch=None):
907 """
908 """
908 Returns next commit from current, if branch is gives it will return
909 Returns next commit from current, if branch is gives it will return
909 next commit belonging to this branch
910 next commit belonging to this branch
910
911
911 :param branch: show commits within the given named branch
912 :param branch: show commits within the given named branch
912 """
913 """
913 indexes = xrange(self.idx + 1, self.repository.count())
914 indexes = xrange(self.idx + 1, self.repository.count())
914 return self._find_next(indexes, branch)
915 return self._find_next(indexes, branch)
915
916
916 def prev(self, branch=None):
917 def prev(self, branch=None):
917 """
918 """
918 Returns previous commit from current, if branch is gives it will
919 Returns previous commit from current, if branch is gives it will
919 return previous commit belonging to this branch
920 return previous commit belonging to this branch
920
921
921 :param branch: show commit within the given named branch
922 :param branch: show commit within the given named branch
922 """
923 """
923 indexes = xrange(self.idx - 1, -1, -1)
924 indexes = xrange(self.idx - 1, -1, -1)
924 return self._find_next(indexes, branch)
925 return self._find_next(indexes, branch)
925
926
926 def _find_next(self, indexes, branch=None):
927 def _find_next(self, indexes, branch=None):
927 if branch and self.branch != branch:
928 if branch and self.branch != branch:
928 raise VCSError('Branch option used on commit not belonging '
929 raise VCSError('Branch option used on commit not belonging '
929 'to that branch')
930 'to that branch')
930
931
931 for next_idx in indexes:
932 for next_idx in indexes:
932 commit = self.repository.get_commit(commit_idx=next_idx)
933 commit = self.repository.get_commit(commit_idx=next_idx)
933 if branch and branch != commit.branch:
934 if branch and branch != commit.branch:
934 continue
935 continue
935 return commit
936 return commit
936 raise CommitDoesNotExistError
937 raise CommitDoesNotExistError
937
938
938 def diff(self, ignore_whitespace=True, context=3):
939 def diff(self, ignore_whitespace=True, context=3):
939 """
940 """
940 Returns a `Diff` object representing the change made by this commit.
941 Returns a `Diff` object representing the change made by this commit.
941 """
942 """
942 parent = (
943 parent = (
943 self.parents[0] if self.parents else self.repository.EMPTY_COMMIT)
944 self.parents[0] if self.parents else self.repository.EMPTY_COMMIT)
944 diff = self.repository.get_diff(
945 diff = self.repository.get_diff(
945 parent, self,
946 parent, self,
946 ignore_whitespace=ignore_whitespace,
947 ignore_whitespace=ignore_whitespace,
947 context=context)
948 context=context)
948 return diff
949 return diff
949
950
950 @LazyProperty
951 @LazyProperty
951 def added(self):
952 def added(self):
952 """
953 """
953 Returns list of added ``FileNode`` objects.
954 Returns list of added ``FileNode`` objects.
954 """
955 """
955 raise NotImplementedError
956 raise NotImplementedError
956
957
957 @LazyProperty
958 @LazyProperty
958 def changed(self):
959 def changed(self):
959 """
960 """
960 Returns list of modified ``FileNode`` objects.
961 Returns list of modified ``FileNode`` objects.
961 """
962 """
962 raise NotImplementedError
963 raise NotImplementedError
963
964
964 @LazyProperty
965 @LazyProperty
965 def removed(self):
966 def removed(self):
966 """
967 """
967 Returns list of removed ``FileNode`` objects.
968 Returns list of removed ``FileNode`` objects.
968 """
969 """
969 raise NotImplementedError
970 raise NotImplementedError
970
971
971 @LazyProperty
972 @LazyProperty
972 def size(self):
973 def size(self):
973 """
974 """
974 Returns total number of bytes from contents of all filenodes.
975 Returns total number of bytes from contents of all filenodes.
975 """
976 """
976 return sum((node.size for node in self.get_filenodes_generator()))
977 return sum((node.size for node in self.get_filenodes_generator()))
977
978
978 def walk(self, topurl=''):
979 def walk(self, topurl=''):
979 """
980 """
980 Similar to os.walk method. Insted of filesystem it walks through
981 Similar to os.walk method. Insted of filesystem it walks through
981 commit starting at given ``topurl``. Returns generator of tuples
982 commit starting at given ``topurl``. Returns generator of tuples
982 (topnode, dirnodes, filenodes).
983 (topnode, dirnodes, filenodes).
983 """
984 """
984 topnode = self.get_node(topurl)
985 topnode = self.get_node(topurl)
985 if not topnode.is_dir():
986 if not topnode.is_dir():
986 return
987 return
987 yield (topnode, topnode.dirs, topnode.files)
988 yield (topnode, topnode.dirs, topnode.files)
988 for dirnode in topnode.dirs:
989 for dirnode in topnode.dirs:
989 for tup in self.walk(dirnode.path):
990 for tup in self.walk(dirnode.path):
990 yield tup
991 yield tup
991
992
992 def get_filenodes_generator(self):
993 def get_filenodes_generator(self):
993 """
994 """
994 Returns generator that yields *all* file nodes.
995 Returns generator that yields *all* file nodes.
995 """
996 """
996 for topnode, dirs, files in self.walk():
997 for topnode, dirs, files in self.walk():
997 for node in files:
998 for node in files:
998 yield node
999 yield node
999
1000
1000 #
1001 #
1001 # Utilities for sub classes to support consistent behavior
1002 # Utilities for sub classes to support consistent behavior
1002 #
1003 #
1003
1004
1004 def no_node_at_path(self, path):
1005 def no_node_at_path(self, path):
1005 return NodeDoesNotExistError(
1006 return NodeDoesNotExistError(
1006 "There is no file nor directory at the given path: "
1007 "There is no file nor directory at the given path: "
1007 "'%s' at commit %s" % (path, self.short_id))
1008 "'%s' at commit %s" % (path, self.short_id))
1008
1009
1009 def _fix_path(self, path):
1010 def _fix_path(self, path):
1010 """
1011 """
1011 Paths are stored without trailing slash so we need to get rid off it if
1012 Paths are stored without trailing slash so we need to get rid off it if
1012 needed.
1013 needed.
1013 """
1014 """
1014 return path.rstrip('/')
1015 return path.rstrip('/')
1015
1016
1016 #
1017 #
1017 # Deprecated API based on changesets
1018 # Deprecated API based on changesets
1018 #
1019 #
1019
1020
1020 @property
1021 @property
1021 def revision(self):
1022 def revision(self):
1022 warnings.warn("Use idx instead", DeprecationWarning)
1023 warnings.warn("Use idx instead", DeprecationWarning)
1023 return self.idx
1024 return self.idx
1024
1025
1025 @revision.setter
1026 @revision.setter
1026 def revision(self, value):
1027 def revision(self, value):
1027 warnings.warn("Use idx instead", DeprecationWarning)
1028 warnings.warn("Use idx instead", DeprecationWarning)
1028 self.idx = value
1029 self.idx = value
1029
1030
1030 def get_file_changeset(self, path):
1031 def get_file_changeset(self, path):
1031 warnings.warn("Use get_file_commit instead", DeprecationWarning)
1032 warnings.warn("Use get_file_commit instead", DeprecationWarning)
1032 return self.get_file_commit(path)
1033 return self.get_file_commit(path)
1033
1034
1034
1035
1035 class BaseChangesetClass(type):
1036 class BaseChangesetClass(type):
1036
1037
1037 def __instancecheck__(self, instance):
1038 def __instancecheck__(self, instance):
1038 return isinstance(instance, BaseCommit)
1039 return isinstance(instance, BaseCommit)
1039
1040
1040
1041
1041 class BaseChangeset(BaseCommit):
1042 class BaseChangeset(BaseCommit):
1042
1043
1043 __metaclass__ = BaseChangesetClass
1044 __metaclass__ = BaseChangesetClass
1044
1045
1045 def __new__(cls, *args, **kwargs):
1046 def __new__(cls, *args, **kwargs):
1046 warnings.warn(
1047 warnings.warn(
1047 "Use BaseCommit instead of BaseChangeset", DeprecationWarning)
1048 "Use BaseCommit instead of BaseChangeset", DeprecationWarning)
1048 return super(BaseChangeset, cls).__new__(cls, *args, **kwargs)
1049 return super(BaseChangeset, cls).__new__(cls, *args, **kwargs)
1049
1050
1050
1051
1051 class BaseInMemoryCommit(object):
1052 class BaseInMemoryCommit(object):
1052 """
1053 """
1053 Represents differences between repository's state (most recent head) and
1054 Represents differences between repository's state (most recent head) and
1054 changes made *in place*.
1055 changes made *in place*.
1055
1056
1056 **Attributes**
1057 **Attributes**
1057
1058
1058 ``repository``
1059 ``repository``
1059 repository object for this in-memory-commit
1060 repository object for this in-memory-commit
1060
1061
1061 ``added``
1062 ``added``
1062 list of ``FileNode`` objects marked as *added*
1063 list of ``FileNode`` objects marked as *added*
1063
1064
1064 ``changed``
1065 ``changed``
1065 list of ``FileNode`` objects marked as *changed*
1066 list of ``FileNode`` objects marked as *changed*
1066
1067
1067 ``removed``
1068 ``removed``
1068 list of ``FileNode`` or ``RemovedFileNode`` objects marked to be
1069 list of ``FileNode`` or ``RemovedFileNode`` objects marked to be
1069 *removed*
1070 *removed*
1070
1071
1071 ``parents``
1072 ``parents``
1072 list of :class:`BaseCommit` instances representing parents of
1073 list of :class:`BaseCommit` instances representing parents of
1073 in-memory commit. Should always be 2-element sequence.
1074 in-memory commit. Should always be 2-element sequence.
1074
1075
1075 """
1076 """
1076
1077
1077 def __init__(self, repository):
1078 def __init__(self, repository):
1078 self.repository = repository
1079 self.repository = repository
1079 self.added = []
1080 self.added = []
1080 self.changed = []
1081 self.changed = []
1081 self.removed = []
1082 self.removed = []
1082 self.parents = []
1083 self.parents = []
1083
1084
1084 def add(self, *filenodes):
1085 def add(self, *filenodes):
1085 """
1086 """
1086 Marks given ``FileNode`` objects as *to be committed*.
1087 Marks given ``FileNode`` objects as *to be committed*.
1087
1088
1088 :raises ``NodeAlreadyExistsError``: if node with same path exists at
1089 :raises ``NodeAlreadyExistsError``: if node with same path exists at
1089 latest commit
1090 latest commit
1090 :raises ``NodeAlreadyAddedError``: if node with same path is already
1091 :raises ``NodeAlreadyAddedError``: if node with same path is already
1091 marked as *added*
1092 marked as *added*
1092 """
1093 """
1093 # Check if not already marked as *added* first
1094 # Check if not already marked as *added* first
1094 for node in filenodes:
1095 for node in filenodes:
1095 if node.path in (n.path for n in self.added):
1096 if node.path in (n.path for n in self.added):
1096 raise NodeAlreadyAddedError(
1097 raise NodeAlreadyAddedError(
1097 "Such FileNode %s is already marked for addition"
1098 "Such FileNode %s is already marked for addition"
1098 % node.path)
1099 % node.path)
1099 for node in filenodes:
1100 for node in filenodes:
1100 self.added.append(node)
1101 self.added.append(node)
1101
1102
1102 def change(self, *filenodes):
1103 def change(self, *filenodes):
1103 """
1104 """
1104 Marks given ``FileNode`` objects to be *changed* in next commit.
1105 Marks given ``FileNode`` objects to be *changed* in next commit.
1105
1106
1106 :raises ``EmptyRepositoryError``: if there are no commits yet
1107 :raises ``EmptyRepositoryError``: if there are no commits yet
1107 :raises ``NodeAlreadyExistsError``: if node with same path is already
1108 :raises ``NodeAlreadyExistsError``: if node with same path is already
1108 marked to be *changed*
1109 marked to be *changed*
1109 :raises ``NodeAlreadyRemovedError``: if node with same path is already
1110 :raises ``NodeAlreadyRemovedError``: if node with same path is already
1110 marked to be *removed*
1111 marked to be *removed*
1111 :raises ``NodeDoesNotExistError``: if node doesn't exist in latest
1112 :raises ``NodeDoesNotExistError``: if node doesn't exist in latest
1112 commit
1113 commit
1113 :raises ``NodeNotChangedError``: if node hasn't really be changed
1114 :raises ``NodeNotChangedError``: if node hasn't really be changed
1114 """
1115 """
1115 for node in filenodes:
1116 for node in filenodes:
1116 if node.path in (n.path for n in self.removed):
1117 if node.path in (n.path for n in self.removed):
1117 raise NodeAlreadyRemovedError(
1118 raise NodeAlreadyRemovedError(
1118 "Node at %s is already marked as removed" % node.path)
1119 "Node at %s is already marked as removed" % node.path)
1119 try:
1120 try:
1120 self.repository.get_commit()
1121 self.repository.get_commit()
1121 except EmptyRepositoryError:
1122 except EmptyRepositoryError:
1122 raise EmptyRepositoryError(
1123 raise EmptyRepositoryError(
1123 "Nothing to change - try to *add* new nodes rather than "
1124 "Nothing to change - try to *add* new nodes rather than "
1124 "changing them")
1125 "changing them")
1125 for node in filenodes:
1126 for node in filenodes:
1126 if node.path in (n.path for n in self.changed):
1127 if node.path in (n.path for n in self.changed):
1127 raise NodeAlreadyChangedError(
1128 raise NodeAlreadyChangedError(
1128 "Node at '%s' is already marked as changed" % node.path)
1129 "Node at '%s' is already marked as changed" % node.path)
1129 self.changed.append(node)
1130 self.changed.append(node)
1130
1131
1131 def remove(self, *filenodes):
1132 def remove(self, *filenodes):
1132 """
1133 """
1133 Marks given ``FileNode`` (or ``RemovedFileNode``) objects to be
1134 Marks given ``FileNode`` (or ``RemovedFileNode``) objects to be
1134 *removed* in next commit.
1135 *removed* in next commit.
1135
1136
1136 :raises ``NodeAlreadyRemovedError``: if node has been already marked to
1137 :raises ``NodeAlreadyRemovedError``: if node has been already marked to
1137 be *removed*
1138 be *removed*
1138 :raises ``NodeAlreadyChangedError``: if node has been already marked to
1139 :raises ``NodeAlreadyChangedError``: if node has been already marked to
1139 be *changed*
1140 be *changed*
1140 """
1141 """
1141 for node in filenodes:
1142 for node in filenodes:
1142 if node.path in (n.path for n in self.removed):
1143 if node.path in (n.path for n in self.removed):
1143 raise NodeAlreadyRemovedError(
1144 raise NodeAlreadyRemovedError(
1144 "Node is already marked to for removal at %s" % node.path)
1145 "Node is already marked to for removal at %s" % node.path)
1145 if node.path in (n.path for n in self.changed):
1146 if node.path in (n.path for n in self.changed):
1146 raise NodeAlreadyChangedError(
1147 raise NodeAlreadyChangedError(
1147 "Node is already marked to be changed at %s" % node.path)
1148 "Node is already marked to be changed at %s" % node.path)
1148 # We only mark node as *removed* - real removal is done by
1149 # We only mark node as *removed* - real removal is done by
1149 # commit method
1150 # commit method
1150 self.removed.append(node)
1151 self.removed.append(node)
1151
1152
1152 def reset(self):
1153 def reset(self):
1153 """
1154 """
1154 Resets this instance to initial state (cleans ``added``, ``changed``
1155 Resets this instance to initial state (cleans ``added``, ``changed``
1155 and ``removed`` lists).
1156 and ``removed`` lists).
1156 """
1157 """
1157 self.added = []
1158 self.added = []
1158 self.changed = []
1159 self.changed = []
1159 self.removed = []
1160 self.removed = []
1160 self.parents = []
1161 self.parents = []
1161
1162
1162 def get_ipaths(self):
1163 def get_ipaths(self):
1163 """
1164 """
1164 Returns generator of paths from nodes marked as added, changed or
1165 Returns generator of paths from nodes marked as added, changed or
1165 removed.
1166 removed.
1166 """
1167 """
1167 for node in itertools.chain(self.added, self.changed, self.removed):
1168 for node in itertools.chain(self.added, self.changed, self.removed):
1168 yield node.path
1169 yield node.path
1169
1170
1170 def get_paths(self):
1171 def get_paths(self):
1171 """
1172 """
1172 Returns list of paths from nodes marked as added, changed or removed.
1173 Returns list of paths from nodes marked as added, changed or removed.
1173 """
1174 """
1174 return list(self.get_ipaths())
1175 return list(self.get_ipaths())
1175
1176
1176 def check_integrity(self, parents=None):
1177 def check_integrity(self, parents=None):
1177 """
1178 """
1178 Checks in-memory commit's integrity. Also, sets parents if not
1179 Checks in-memory commit's integrity. Also, sets parents if not
1179 already set.
1180 already set.
1180
1181
1181 :raises CommitError: if any error occurs (i.e.
1182 :raises CommitError: if any error occurs (i.e.
1182 ``NodeDoesNotExistError``).
1183 ``NodeDoesNotExistError``).
1183 """
1184 """
1184 if not self.parents:
1185 if not self.parents:
1185 parents = parents or []
1186 parents = parents or []
1186 if len(parents) == 0:
1187 if len(parents) == 0:
1187 try:
1188 try:
1188 parents = [self.repository.get_commit(), None]
1189 parents = [self.repository.get_commit(), None]
1189 except EmptyRepositoryError:
1190 except EmptyRepositoryError:
1190 parents = [None, None]
1191 parents = [None, None]
1191 elif len(parents) == 1:
1192 elif len(parents) == 1:
1192 parents += [None]
1193 parents += [None]
1193 self.parents = parents
1194 self.parents = parents
1194
1195
1195 # Local parents, only if not None
1196 # Local parents, only if not None
1196 parents = [p for p in self.parents if p]
1197 parents = [p for p in self.parents if p]
1197
1198
1198 # Check nodes marked as added
1199 # Check nodes marked as added
1199 for p in parents:
1200 for p in parents:
1200 for node in self.added:
1201 for node in self.added:
1201 try:
1202 try:
1202 p.get_node(node.path)
1203 p.get_node(node.path)
1203 except NodeDoesNotExistError:
1204 except NodeDoesNotExistError:
1204 pass
1205 pass
1205 else:
1206 else:
1206 raise NodeAlreadyExistsError(
1207 raise NodeAlreadyExistsError(
1207 "Node `%s` already exists at %s" % (node.path, p))
1208 "Node `%s` already exists at %s" % (node.path, p))
1208
1209
1209 # Check nodes marked as changed
1210 # Check nodes marked as changed
1210 missing = set(self.changed)
1211 missing = set(self.changed)
1211 not_changed = set(self.changed)
1212 not_changed = set(self.changed)
1212 if self.changed and not parents:
1213 if self.changed and not parents:
1213 raise NodeDoesNotExistError(str(self.changed[0].path))
1214 raise NodeDoesNotExistError(str(self.changed[0].path))
1214 for p in parents:
1215 for p in parents:
1215 for node in self.changed:
1216 for node in self.changed:
1216 try:
1217 try:
1217 old = p.get_node(node.path)
1218 old = p.get_node(node.path)
1218 missing.remove(node)
1219 missing.remove(node)
1219 # if content actually changed, remove node from not_changed
1220 # if content actually changed, remove node from not_changed
1220 if old.content != node.content:
1221 if old.content != node.content:
1221 not_changed.remove(node)
1222 not_changed.remove(node)
1222 except NodeDoesNotExistError:
1223 except NodeDoesNotExistError:
1223 pass
1224 pass
1224 if self.changed and missing:
1225 if self.changed and missing:
1225 raise NodeDoesNotExistError(
1226 raise NodeDoesNotExistError(
1226 "Node `%s` marked as modified but missing in parents: %s"
1227 "Node `%s` marked as modified but missing in parents: %s"
1227 % (node.path, parents))
1228 % (node.path, parents))
1228
1229
1229 if self.changed and not_changed:
1230 if self.changed and not_changed:
1230 raise NodeNotChangedError(
1231 raise NodeNotChangedError(
1231 "Node `%s` wasn't actually changed (parents: %s)"
1232 "Node `%s` wasn't actually changed (parents: %s)"
1232 % (not_changed.pop().path, parents))
1233 % (not_changed.pop().path, parents))
1233
1234
1234 # Check nodes marked as removed
1235 # Check nodes marked as removed
1235 if self.removed and not parents:
1236 if self.removed and not parents:
1236 raise NodeDoesNotExistError(
1237 raise NodeDoesNotExistError(
1237 "Cannot remove node at %s as there "
1238 "Cannot remove node at %s as there "
1238 "were no parents specified" % self.removed[0].path)
1239 "were no parents specified" % self.removed[0].path)
1239 really_removed = set()
1240 really_removed = set()
1240 for p in parents:
1241 for p in parents:
1241 for node in self.removed:
1242 for node in self.removed:
1242 try:
1243 try:
1243 p.get_node(node.path)
1244 p.get_node(node.path)
1244 really_removed.add(node)
1245 really_removed.add(node)
1245 except CommitError:
1246 except CommitError:
1246 pass
1247 pass
1247 not_removed = set(self.removed) - really_removed
1248 not_removed = set(self.removed) - really_removed
1248 if not_removed:
1249 if not_removed:
1249 # TODO: johbo: This code branch does not seem to be covered
1250 # TODO: johbo: This code branch does not seem to be covered
1250 raise NodeDoesNotExistError(
1251 raise NodeDoesNotExistError(
1251 "Cannot remove node at %s from "
1252 "Cannot remove node at %s from "
1252 "following parents: %s" % (not_removed, parents))
1253 "following parents: %s" % (not_removed, parents))
1253
1254
1254 def commit(
1255 def commit(
1255 self, message, author, parents=None, branch=None, date=None,
1256 self, message, author, parents=None, branch=None, date=None,
1256 **kwargs):
1257 **kwargs):
1257 """
1258 """
1258 Performs in-memory commit (doesn't check workdir in any way) and
1259 Performs in-memory commit (doesn't check workdir in any way) and
1259 returns newly created :class:`BaseCommit`. Updates repository's
1260 returns newly created :class:`BaseCommit`. Updates repository's
1260 attribute `commits`.
1261 attribute `commits`.
1261
1262
1262 .. note::
1263 .. note::
1263
1264
1264 While overriding this method each backend's should call
1265 While overriding this method each backend's should call
1265 ``self.check_integrity(parents)`` in the first place.
1266 ``self.check_integrity(parents)`` in the first place.
1266
1267
1267 :param message: message of the commit
1268 :param message: message of the commit
1268 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
1269 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
1269 :param parents: single parent or sequence of parents from which commit
1270 :param parents: single parent or sequence of parents from which commit
1270 would be derived
1271 would be derived
1271 :param date: ``datetime.datetime`` instance. Defaults to
1272 :param date: ``datetime.datetime`` instance. Defaults to
1272 ``datetime.datetime.now()``.
1273 ``datetime.datetime.now()``.
1273 :param branch: branch name, as string. If none given, default backend's
1274 :param branch: branch name, as string. If none given, default backend's
1274 branch would be used.
1275 branch would be used.
1275
1276
1276 :raises ``CommitError``: if any error occurs while committing
1277 :raises ``CommitError``: if any error occurs while committing
1277 """
1278 """
1278 raise NotImplementedError
1279 raise NotImplementedError
1279
1280
1280
1281
1281 class BaseInMemoryChangesetClass(type):
1282 class BaseInMemoryChangesetClass(type):
1282
1283
1283 def __instancecheck__(self, instance):
1284 def __instancecheck__(self, instance):
1284 return isinstance(instance, BaseInMemoryCommit)
1285 return isinstance(instance, BaseInMemoryCommit)
1285
1286
1286
1287
1287 class BaseInMemoryChangeset(BaseInMemoryCommit):
1288 class BaseInMemoryChangeset(BaseInMemoryCommit):
1288
1289
1289 __metaclass__ = BaseInMemoryChangesetClass
1290 __metaclass__ = BaseInMemoryChangesetClass
1290
1291
1291 def __new__(cls, *args, **kwargs):
1292 def __new__(cls, *args, **kwargs):
1292 warnings.warn(
1293 warnings.warn(
1293 "Use BaseCommit instead of BaseInMemoryCommit", DeprecationWarning)
1294 "Use BaseCommit instead of BaseInMemoryCommit", DeprecationWarning)
1294 return super(BaseInMemoryChangeset, cls).__new__(cls, *args, **kwargs)
1295 return super(BaseInMemoryChangeset, cls).__new__(cls, *args, **kwargs)
1295
1296
1296
1297
1297 class EmptyCommit(BaseCommit):
1298 class EmptyCommit(BaseCommit):
1298 """
1299 """
1299 An dummy empty commit. It's possible to pass hash when creating
1300 An dummy empty commit. It's possible to pass hash when creating
1300 an EmptyCommit
1301 an EmptyCommit
1301 """
1302 """
1302
1303
1303 def __init__(
1304 def __init__(
1304 self, commit_id='0' * 40, repo=None, alias=None, idx=-1,
1305 self, commit_id='0' * 40, repo=None, alias=None, idx=-1,
1305 message='', author='', date=None):
1306 message='', author='', date=None):
1306 self._empty_commit_id = commit_id
1307 self._empty_commit_id = commit_id
1307 # TODO: johbo: Solve idx parameter, default value does not make
1308 # TODO: johbo: Solve idx parameter, default value does not make
1308 # too much sense
1309 # too much sense
1309 self.idx = idx
1310 self.idx = idx
1310 self.message = message
1311 self.message = message
1311 self.author = author
1312 self.author = author
1312 self.date = date or datetime.datetime.fromtimestamp(0)
1313 self.date = date or datetime.datetime.fromtimestamp(0)
1313 self.repository = repo
1314 self.repository = repo
1314 self.alias = alias
1315 self.alias = alias
1315
1316
1316 @LazyProperty
1317 @LazyProperty
1317 def raw_id(self):
1318 def raw_id(self):
1318 """
1319 """
1319 Returns raw string identifying this commit, useful for web
1320 Returns raw string identifying this commit, useful for web
1320 representation.
1321 representation.
1321 """
1322 """
1322
1323
1323 return self._empty_commit_id
1324 return self._empty_commit_id
1324
1325
1325 @LazyProperty
1326 @LazyProperty
1326 def branch(self):
1327 def branch(self):
1327 if self.alias:
1328 if self.alias:
1328 from rhodecode.lib.vcs.backends import get_backend
1329 from rhodecode.lib.vcs.backends import get_backend
1329 return get_backend(self.alias).DEFAULT_BRANCH_NAME
1330 return get_backend(self.alias).DEFAULT_BRANCH_NAME
1330
1331
1331 @LazyProperty
1332 @LazyProperty
1332 def short_id(self):
1333 def short_id(self):
1333 return self.raw_id[:12]
1334 return self.raw_id[:12]
1334
1335
1335 @LazyProperty
1336 @LazyProperty
1336 def id(self):
1337 def id(self):
1337 return self.raw_id
1338 return self.raw_id
1338
1339
1339 def get_file_commit(self, path):
1340 def get_file_commit(self, path):
1340 return self
1341 return self
1341
1342
1342 def get_file_content(self, path):
1343 def get_file_content(self, path):
1343 return u''
1344 return u''
1344
1345
1345 def get_file_size(self, path):
1346 def get_file_size(self, path):
1346 return 0
1347 return 0
1347
1348
1348
1349
1349 class EmptyChangesetClass(type):
1350 class EmptyChangesetClass(type):
1350
1351
1351 def __instancecheck__(self, instance):
1352 def __instancecheck__(self, instance):
1352 return isinstance(instance, EmptyCommit)
1353 return isinstance(instance, EmptyCommit)
1353
1354
1354
1355
1355 class EmptyChangeset(EmptyCommit):
1356 class EmptyChangeset(EmptyCommit):
1356
1357
1357 __metaclass__ = EmptyChangesetClass
1358 __metaclass__ = EmptyChangesetClass
1358
1359
1359 def __new__(cls, *args, **kwargs):
1360 def __new__(cls, *args, **kwargs):
1360 warnings.warn(
1361 warnings.warn(
1361 "Use EmptyCommit instead of EmptyChangeset", DeprecationWarning)
1362 "Use EmptyCommit instead of EmptyChangeset", DeprecationWarning)
1362 return super(EmptyCommit, cls).__new__(cls, *args, **kwargs)
1363 return super(EmptyCommit, cls).__new__(cls, *args, **kwargs)
1363
1364
1364 def __init__(self, cs='0' * 40, repo=None, requested_revision=None,
1365 def __init__(self, cs='0' * 40, repo=None, requested_revision=None,
1365 alias=None, revision=-1, message='', author='', date=None):
1366 alias=None, revision=-1, message='', author='', date=None):
1366 if requested_revision is not None:
1367 if requested_revision is not None:
1367 warnings.warn(
1368 warnings.warn(
1368 "Parameter requested_revision not supported anymore",
1369 "Parameter requested_revision not supported anymore",
1369 DeprecationWarning)
1370 DeprecationWarning)
1370 super(EmptyChangeset, self).__init__(
1371 super(EmptyChangeset, self).__init__(
1371 commit_id=cs, repo=repo, alias=alias, idx=revision,
1372 commit_id=cs, repo=repo, alias=alias, idx=revision,
1372 message=message, author=author, date=date)
1373 message=message, author=author, date=date)
1373
1374
1374 @property
1375 @property
1375 def revision(self):
1376 def revision(self):
1376 warnings.warn("Use idx instead", DeprecationWarning)
1377 warnings.warn("Use idx instead", DeprecationWarning)
1377 return self.idx
1378 return self.idx
1378
1379
1379 @revision.setter
1380 @revision.setter
1380 def revision(self, value):
1381 def revision(self, value):
1381 warnings.warn("Use idx instead", DeprecationWarning)
1382 warnings.warn("Use idx instead", DeprecationWarning)
1382 self.idx = value
1383 self.idx = value
1383
1384
1384
1385
1385 class CollectionGenerator(object):
1386 class CollectionGenerator(object):
1386
1387
1387 def __init__(self, repo, commit_ids, collection_size=None, pre_load=None):
1388 def __init__(self, repo, commit_ids, collection_size=None, pre_load=None):
1388 self.repo = repo
1389 self.repo = repo
1389 self.commit_ids = commit_ids
1390 self.commit_ids = commit_ids
1390 # TODO: (oliver) this isn't currently hooked up
1391 # TODO: (oliver) this isn't currently hooked up
1391 self.collection_size = None
1392 self.collection_size = None
1392 self.pre_load = pre_load
1393 self.pre_load = pre_load
1393
1394
1394 def __len__(self):
1395 def __len__(self):
1395 if self.collection_size is not None:
1396 if self.collection_size is not None:
1396 return self.collection_size
1397 return self.collection_size
1397 return self.commit_ids.__len__()
1398 return self.commit_ids.__len__()
1398
1399
1399 def __iter__(self):
1400 def __iter__(self):
1400 for commit_id in self.commit_ids:
1401 for commit_id in self.commit_ids:
1401 # TODO: johbo: Mercurial passes in commit indices or commit ids
1402 # TODO: johbo: Mercurial passes in commit indices or commit ids
1402 yield self._commit_factory(commit_id)
1403 yield self._commit_factory(commit_id)
1403
1404
1404 def _commit_factory(self, commit_id):
1405 def _commit_factory(self, commit_id):
1405 """
1406 """
1406 Allows backends to override the way commits are generated.
1407 Allows backends to override the way commits are generated.
1407 """
1408 """
1408 return self.repo.get_commit(commit_id=commit_id,
1409 return self.repo.get_commit(commit_id=commit_id,
1409 pre_load=self.pre_load)
1410 pre_load=self.pre_load)
1410
1411
1411 def __getslice__(self, i, j):
1412 def __getslice__(self, i, j):
1412 """
1413 """
1413 Returns an iterator of sliced repository
1414 Returns an iterator of sliced repository
1414 """
1415 """
1415 commit_ids = self.commit_ids[i:j]
1416 commit_ids = self.commit_ids[i:j]
1416 return self.__class__(
1417 return self.__class__(
1417 self.repo, commit_ids, pre_load=self.pre_load)
1418 self.repo, commit_ids, pre_load=self.pre_load)
1418
1419
1419 def __repr__(self):
1420 def __repr__(self):
1420 return '<CollectionGenerator[len:%s]>' % (self.__len__())
1421 return '<CollectionGenerator[len:%s]>' % (self.__len__())
1421
1422
1422
1423
1423 class Config(object):
1424 class Config(object):
1424 """
1425 """
1425 Represents the configuration for a repository.
1426 Represents the configuration for a repository.
1426
1427
1427 The API is inspired by :class:`ConfigParser.ConfigParser` from the
1428 The API is inspired by :class:`ConfigParser.ConfigParser` from the
1428 standard library. It implements only the needed subset.
1429 standard library. It implements only the needed subset.
1429 """
1430 """
1430
1431
1431 def __init__(self):
1432 def __init__(self):
1432 self._values = {}
1433 self._values = {}
1433
1434
1434 def copy(self):
1435 def copy(self):
1435 clone = Config()
1436 clone = Config()
1436 for section, values in self._values.items():
1437 for section, values in self._values.items():
1437 clone._values[section] = values.copy()
1438 clone._values[section] = values.copy()
1438 return clone
1439 return clone
1439
1440
1440 def __repr__(self):
1441 def __repr__(self):
1441 return '<Config(%s sections) at %s>' % (
1442 return '<Config(%s sections) at %s>' % (
1442 len(self._values), hex(id(self)))
1443 len(self._values), hex(id(self)))
1443
1444
1444 def items(self, section):
1445 def items(self, section):
1445 return self._values.get(section, {}).iteritems()
1446 return self._values.get(section, {}).iteritems()
1446
1447
1447 def get(self, section, option):
1448 def get(self, section, option):
1448 return self._values.get(section, {}).get(option)
1449 return self._values.get(section, {}).get(option)
1449
1450
1450 def set(self, section, option, value):
1451 def set(self, section, option, value):
1451 section_values = self._values.setdefault(section, {})
1452 section_values = self._values.setdefault(section, {})
1452 section_values[option] = value
1453 section_values[option] = value
1453
1454
1454 def clear_section(self, section):
1455 def clear_section(self, section):
1455 self._values[section] = {}
1456 self._values[section] = {}
1456
1457
1457 def serialize(self):
1458 def serialize(self):
1458 """
1459 """
1459 Creates a list of three tuples (section, key, value) representing
1460 Creates a list of three tuples (section, key, value) representing
1460 this config object.
1461 this config object.
1461 """
1462 """
1462 items = []
1463 items = []
1463 for section in self._values:
1464 for section in self._values:
1464 for option, value in self._values[section].items():
1465 for option, value in self._values[section].items():
1465 items.append(
1466 items.append(
1466 (safe_str(section), safe_str(option), safe_str(value)))
1467 (safe_str(section), safe_str(option), safe_str(value)))
1467 return items
1468 return items
1468
1469
1469
1470
1470 class Diff(object):
1471 class Diff(object):
1471 """
1472 """
1472 Represents a diff result from a repository backend.
1473 Represents a diff result from a repository backend.
1473
1474
1474 Subclasses have to provide a backend specific value for :attr:`_header_re`.
1475 Subclasses have to provide a backend specific value for :attr:`_header_re`.
1475 """
1476 """
1476
1477
1477 _header_re = None
1478 _header_re = None
1478
1479
1479 def __init__(self, raw_diff):
1480 def __init__(self, raw_diff):
1480 self.raw = raw_diff
1481 self.raw = raw_diff
1481
1482
1482 def chunks(self):
1483 def chunks(self):
1483 """
1484 """
1484 split the diff in chunks of separate --git a/file b/file chunks
1485 split the diff in chunks of separate --git a/file b/file chunks
1485 to make diffs consistent we must prepend with \n, and make sure
1486 to make diffs consistent we must prepend with \n, and make sure
1486 we can detect last chunk as this was also has special rule
1487 we can detect last chunk as this was also has special rule
1487 """
1488 """
1488 chunks = ('\n' + self.raw).split('\ndiff --git')[1:]
1489 chunks = ('\n' + self.raw).split('\ndiff --git')[1:]
1489 total_chunks = len(chunks)
1490 total_chunks = len(chunks)
1490 return (DiffChunk(chunk, self, cur_chunk == total_chunks)
1491 return (DiffChunk(chunk, self, cur_chunk == total_chunks)
1491 for cur_chunk, chunk in enumerate(chunks, start=1))
1492 for cur_chunk, chunk in enumerate(chunks, start=1))
1492
1493
1493
1494
1494 class DiffChunk(object):
1495 class DiffChunk(object):
1495
1496
1496 def __init__(self, chunk, diff, last_chunk):
1497 def __init__(self, chunk, diff, last_chunk):
1497 self._diff = diff
1498 self._diff = diff
1498
1499
1499 # since we split by \ndiff --git that part is lost from original diff
1500 # since we split by \ndiff --git that part is lost from original diff
1500 # we need to re-apply it at the end, EXCEPT ! if it's last chunk
1501 # we need to re-apply it at the end, EXCEPT ! if it's last chunk
1501 if not last_chunk:
1502 if not last_chunk:
1502 chunk += '\n'
1503 chunk += '\n'
1503
1504
1504 match = self._diff._header_re.match(chunk)
1505 match = self._diff._header_re.match(chunk)
1505 self.header = match.groupdict()
1506 self.header = match.groupdict()
1506 self.diff = chunk[match.end():]
1507 self.diff = chunk[match.end():]
1507 self.raw = chunk
1508 self.raw = chunk
@@ -1,564 +1,577 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2016 RhodeCode GmbH
3 # Copyright (C) 2010-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 import datetime
21 import datetime
22 import time
22 import time
23
23
24 import pytest
24 import pytest
25
25
26 from rhodecode.lib.vcs.backends.base import (
26 from rhodecode.lib.vcs.backends.base import (
27 CollectionGenerator, FILEMODE_DEFAULT, EmptyCommit)
27 CollectionGenerator, FILEMODE_DEFAULT, EmptyCommit)
28 from rhodecode.lib.vcs.exceptions import (
28 from rhodecode.lib.vcs.exceptions import (
29 BranchDoesNotExistError, CommitDoesNotExistError,
29 BranchDoesNotExistError, CommitDoesNotExistError,
30 RepositoryError, EmptyRepositoryError)
30 RepositoryError, EmptyRepositoryError)
31 from rhodecode.lib.vcs.nodes import (
31 from rhodecode.lib.vcs.nodes import (
32 FileNode, AddedFileNodesGenerator,
32 FileNode, AddedFileNodesGenerator,
33 ChangedFileNodesGenerator, RemovedFileNodesGenerator)
33 ChangedFileNodesGenerator, RemovedFileNodesGenerator)
34 from rhodecode.tests import get_new_dir
34 from rhodecode.tests import get_new_dir
35 from rhodecode.tests.vcs.base import BackendTestMixin
35 from rhodecode.tests.vcs.base import BackendTestMixin
36
36
37
37
38 class TestBaseChangeset:
38 class TestBaseChangeset:
39
39
40 def test_is_deprecated(self):
40 def test_is_deprecated(self):
41 from rhodecode.lib.vcs.backends.base import BaseChangeset
41 from rhodecode.lib.vcs.backends.base import BaseChangeset
42 pytest.deprecated_call(BaseChangeset)
42 pytest.deprecated_call(BaseChangeset)
43
43
44
44
45 class TestEmptyCommit:
45 class TestEmptyCommit:
46
46
47 def test_branch_without_alias_returns_none(self):
47 def test_branch_without_alias_returns_none(self):
48 commit = EmptyCommit()
48 commit = EmptyCommit()
49 assert commit.branch is None
49 assert commit.branch is None
50
50
51
51
52 class TestCommitsInNonEmptyRepo(BackendTestMixin):
52 class TestCommitsInNonEmptyRepo(BackendTestMixin):
53 recreate_repo_per_test = True
53 recreate_repo_per_test = True
54
54
55 @classmethod
55 @classmethod
56 def _get_commits(cls):
56 def _get_commits(cls):
57 start_date = datetime.datetime(2010, 1, 1, 20)
57 start_date = datetime.datetime(2010, 1, 1, 20)
58 for x in xrange(5):
58 for x in xrange(5):
59 yield {
59 yield {
60 'message': 'Commit %d' % x,
60 'message': 'Commit %d' % x,
61 'author': 'Joe Doe <joe.doe@example.com>',
61 'author': 'Joe Doe <joe.doe@example.com>',
62 'date': start_date + datetime.timedelta(hours=12 * x),
62 'date': start_date + datetime.timedelta(hours=12 * x),
63 'added': [
63 'added': [
64 FileNode('file_%d.txt' % x, content='Foobar %d' % x),
64 FileNode('file_%d.txt' % x, content='Foobar %d' % x),
65 ],
65 ],
66 }
66 }
67
67
68 def test_walk_returns_empty_list_in_case_of_file(self):
68 def test_walk_returns_empty_list_in_case_of_file(self):
69 result = list(self.tip.walk('file_0.txt'))
69 result = list(self.tip.walk('file_0.txt'))
70 assert result == []
70 assert result == []
71
71
72 @pytest.mark.backends("git", "hg")
72 @pytest.mark.backends("git", "hg")
73 def test_new_branch(self):
73 def test_new_branch(self):
74 self.imc.add(FileNode('docs/index.txt',
74 self.imc.add(FileNode('docs/index.txt',
75 content='Documentation\n'))
75 content='Documentation\n'))
76 foobar_tip = self.imc.commit(
76 foobar_tip = self.imc.commit(
77 message=u'New branch: foobar',
77 message=u'New branch: foobar',
78 author=u'joe',
78 author=u'joe',
79 branch='foobar',
79 branch='foobar',
80 )
80 )
81 assert 'foobar' in self.repo.branches
81 assert 'foobar' in self.repo.branches
82 assert foobar_tip.branch == 'foobar'
82 assert foobar_tip.branch == 'foobar'
83 # 'foobar' should be the only branch that contains the new commit
83 # 'foobar' should be the only branch that contains the new commit
84 branch = self.repo.branches.values()
84 branch = self.repo.branches.values()
85 assert branch[0] != branch[1]
85 assert branch[0] != branch[1]
86
86
87 @pytest.mark.backends("git", "hg")
87 @pytest.mark.backends("git", "hg")
88 def test_new_head_in_default_branch(self):
88 def test_new_head_in_default_branch(self):
89 tip = self.repo.get_commit()
89 tip = self.repo.get_commit()
90 self.imc.add(FileNode('docs/index.txt',
90 self.imc.add(FileNode('docs/index.txt',
91 content='Documentation\n'))
91 content='Documentation\n'))
92 foobar_tip = self.imc.commit(
92 foobar_tip = self.imc.commit(
93 message=u'New branch: foobar',
93 message=u'New branch: foobar',
94 author=u'joe',
94 author=u'joe',
95 branch='foobar',
95 branch='foobar',
96 parents=[tip],
96 parents=[tip],
97 )
97 )
98 self.imc.change(FileNode('docs/index.txt',
98 self.imc.change(FileNode('docs/index.txt',
99 content='Documentation\nand more...\n'))
99 content='Documentation\nand more...\n'))
100 newtip = self.imc.commit(
100 newtip = self.imc.commit(
101 message=u'At default branch',
101 message=u'At default branch',
102 author=u'joe',
102 author=u'joe',
103 branch=foobar_tip.branch,
103 branch=foobar_tip.branch,
104 parents=[foobar_tip],
104 parents=[foobar_tip],
105 )
105 )
106
106
107 newest_tip = self.imc.commit(
107 newest_tip = self.imc.commit(
108 message=u'Merged with %s' % foobar_tip.raw_id,
108 message=u'Merged with %s' % foobar_tip.raw_id,
109 author=u'joe',
109 author=u'joe',
110 branch=self.backend_class.DEFAULT_BRANCH_NAME,
110 branch=self.backend_class.DEFAULT_BRANCH_NAME,
111 parents=[newtip, foobar_tip],
111 parents=[newtip, foobar_tip],
112 )
112 )
113
113
114 assert newest_tip.branch == self.backend_class.DEFAULT_BRANCH_NAME
114 assert newest_tip.branch == self.backend_class.DEFAULT_BRANCH_NAME
115
115
116 @pytest.mark.backends("git", "hg")
116 @pytest.mark.backends("git", "hg")
117 def test_get_commits_respects_branch_name(self):
117 def test_get_commits_respects_branch_name(self):
118 """
118 """
119 * e1930d0 (HEAD, master) Back in default branch
119 * e1930d0 (HEAD, master) Back in default branch
120 | * e1930d0 (docs) New Branch: docs2
120 | * e1930d0 (docs) New Branch: docs2
121 | * dcc14fa New branch: docs
121 | * dcc14fa New branch: docs
122 |/
122 |/
123 * e63c41a Initial commit
123 * e63c41a Initial commit
124 ...
124 ...
125 * 624d3db Commit 0
125 * 624d3db Commit 0
126
126
127 :return:
127 :return:
128 """
128 """
129 DEFAULT_BRANCH = self.repo.DEFAULT_BRANCH_NAME
129 DEFAULT_BRANCH = self.repo.DEFAULT_BRANCH_NAME
130 TEST_BRANCH = 'docs'
130 TEST_BRANCH = 'docs'
131 org_tip = self.repo.get_commit()
131 org_tip = self.repo.get_commit()
132
132
133 self.imc.add(FileNode('readme.txt', content='Document\n'))
133 self.imc.add(FileNode('readme.txt', content='Document\n'))
134 initial = self.imc.commit(
134 initial = self.imc.commit(
135 message=u'Initial commit',
135 message=u'Initial commit',
136 author=u'joe',
136 author=u'joe',
137 parents=[org_tip],
137 parents=[org_tip],
138 branch=DEFAULT_BRANCH,)
138 branch=DEFAULT_BRANCH,)
139
139
140 self.imc.add(FileNode('newdoc.txt', content='foobar\n'))
140 self.imc.add(FileNode('newdoc.txt', content='foobar\n'))
141 docs_branch_commit1 = self.imc.commit(
141 docs_branch_commit1 = self.imc.commit(
142 message=u'New branch: docs',
142 message=u'New branch: docs',
143 author=u'joe',
143 author=u'joe',
144 parents=[initial],
144 parents=[initial],
145 branch=TEST_BRANCH,)
145 branch=TEST_BRANCH,)
146
146
147 self.imc.add(FileNode('newdoc2.txt', content='foobar2\n'))
147 self.imc.add(FileNode('newdoc2.txt', content='foobar2\n'))
148 docs_branch_commit2 = self.imc.commit(
148 docs_branch_commit2 = self.imc.commit(
149 message=u'New branch: docs2',
149 message=u'New branch: docs2',
150 author=u'joe',
150 author=u'joe',
151 parents=[docs_branch_commit1],
151 parents=[docs_branch_commit1],
152 branch=TEST_BRANCH,)
152 branch=TEST_BRANCH,)
153
153
154 self.imc.add(FileNode('newfile', content='hello world\n'))
154 self.imc.add(FileNode('newfile', content='hello world\n'))
155 self.imc.commit(
155 self.imc.commit(
156 message=u'Back in default branch',
156 message=u'Back in default branch',
157 author=u'joe',
157 author=u'joe',
158 parents=[initial],
158 parents=[initial],
159 branch=DEFAULT_BRANCH,)
159 branch=DEFAULT_BRANCH,)
160
160
161 default_branch_commits = self.repo.get_commits(
161 default_branch_commits = self.repo.get_commits(
162 branch_name=DEFAULT_BRANCH)
162 branch_name=DEFAULT_BRANCH)
163 assert docs_branch_commit1 not in list(default_branch_commits)
163 assert docs_branch_commit1 not in list(default_branch_commits)
164 assert docs_branch_commit2 not in list(default_branch_commits)
164 assert docs_branch_commit2 not in list(default_branch_commits)
165
165
166 docs_branch_commits = self.repo.get_commits(
166 docs_branch_commits = self.repo.get_commits(
167 start_id=self.repo.commit_ids[0], end_id=self.repo.commit_ids[-1],
167 start_id=self.repo.commit_ids[0], end_id=self.repo.commit_ids[-1],
168 branch_name=TEST_BRANCH)
168 branch_name=TEST_BRANCH)
169 assert docs_branch_commit1 in list(docs_branch_commits)
169 assert docs_branch_commit1 in list(docs_branch_commits)
170 assert docs_branch_commit2 in list(docs_branch_commits)
170 assert docs_branch_commit2 in list(docs_branch_commits)
171
171
172 @pytest.mark.backends("svn")
172 @pytest.mark.backends("svn")
173 def test_get_commits_respects_branch_name_svn(self, vcsbackend_svn):
173 def test_get_commits_respects_branch_name_svn(self, vcsbackend_svn):
174 repo = vcsbackend_svn['svn-simple-layout']
174 repo = vcsbackend_svn['svn-simple-layout']
175 commits = repo.get_commits(branch_name='trunk')
175 commits = repo.get_commits(branch_name='trunk')
176 commit_indexes = [c.idx for c in commits]
176 commit_indexes = [c.idx for c in commits]
177 assert commit_indexes == [1, 2, 3, 7, 12, 15]
177 assert commit_indexes == [1, 2, 3, 7, 12, 15]
178
178
179 def test_get_commit_by_branch(self):
179 def test_get_commit_by_branch(self):
180 for branch, commit_id in self.repo.branches.iteritems():
180 for branch, commit_id in self.repo.branches.iteritems():
181 assert commit_id == self.repo.get_commit(branch).raw_id
181 assert commit_id == self.repo.get_commit(branch).raw_id
182
182
183 def test_get_commit_by_tag(self):
183 def test_get_commit_by_tag(self):
184 for tag, commit_id in self.repo.tags.iteritems():
184 for tag, commit_id in self.repo.tags.iteritems():
185 assert commit_id == self.repo.get_commit(tag).raw_id
185 assert commit_id == self.repo.get_commit(tag).raw_id
186
186
187 def test_get_commit_parents(self):
187 def test_get_commit_parents(self):
188 repo = self.repo
188 repo = self.repo
189 for test_idx in [1, 2, 3]:
189 for test_idx in [1, 2, 3]:
190 commit = repo.get_commit(commit_idx=test_idx - 1)
190 commit = repo.get_commit(commit_idx=test_idx - 1)
191 assert [commit] == repo.get_commit(commit_idx=test_idx).parents
191 assert [commit] == repo.get_commit(commit_idx=test_idx).parents
192
192
193 def test_get_commit_children(self):
193 def test_get_commit_children(self):
194 repo = self.repo
194 repo = self.repo
195 for test_idx in [1, 2, 3]:
195 for test_idx in [1, 2, 3]:
196 commit = repo.get_commit(commit_idx=test_idx + 1)
196 commit = repo.get_commit(commit_idx=test_idx + 1)
197 assert [commit] == repo.get_commit(commit_idx=test_idx).children
197 assert [commit] == repo.get_commit(commit_idx=test_idx).children
198
198
199
199
200 class TestCommits(BackendTestMixin):
200 class TestCommits(BackendTestMixin):
201 recreate_repo_per_test = False
201 recreate_repo_per_test = False
202
202
203 @classmethod
203 @classmethod
204 def _get_commits(cls):
204 def _get_commits(cls):
205 start_date = datetime.datetime(2010, 1, 1, 20)
205 start_date = datetime.datetime(2010, 1, 1, 20)
206 for x in xrange(5):
206 for x in xrange(5):
207 yield {
207 yield {
208 'message': u'Commit %d' % x,
208 'message': u'Commit %d' % x,
209 'author': u'Joe Doe <joe.doe@example.com>',
209 'author': u'Joe Doe <joe.doe@example.com>',
210 'date': start_date + datetime.timedelta(hours=12 * x),
210 'date': start_date + datetime.timedelta(hours=12 * x),
211 'added': [
211 'added': [
212 FileNode('file_%d.txt' % x, content='Foobar %d' % x),
212 FileNode('file_%d.txt' % x, content='Foobar %d' % x),
213 ],
213 ],
214 }
214 }
215
215
216 def test_simple(self):
216 def test_simple(self):
217 tip = self.repo.get_commit()
217 tip = self.repo.get_commit()
218 assert tip.date, datetime.datetime(2010, 1, 3 == 20)
218 assert tip.date, datetime.datetime(2010, 1, 3 == 20)
219
219
220 def test_simple_serialized_commit(self):
220 def test_simple_serialized_commit(self):
221 tip = self.repo.get_commit()
221 tip = self.repo.get_commit()
222 # json.dumps(tip) uses .__json__() method
222 # json.dumps(tip) uses .__json__() method
223 data = tip.__json__()
223 data = tip.__json__()
224 assert 'branch' in data
224 assert 'branch' in data
225 assert data['revision']
225 assert data['revision']
226
226
227 def test_retrieve_tip(self):
227 def test_retrieve_tip(self):
228 tip = self.repo.get_commit('tip')
228 tip = self.repo.get_commit('tip')
229 assert tip == self.repo.get_commit()
229 assert tip == self.repo.get_commit()
230
230
231 def test_invalid(self):
231 def test_invalid(self):
232 with pytest.raises(CommitDoesNotExistError):
232 with pytest.raises(CommitDoesNotExistError):
233 self.repo.get_commit(commit_idx=123456789)
233 self.repo.get_commit(commit_idx=123456789)
234
234
235 def test_idx(self):
235 def test_idx(self):
236 commit = self.repo[0]
236 commit = self.repo[0]
237 assert commit.idx == 0
237 assert commit.idx == 0
238
238
239 def test_negative_idx(self):
239 def test_negative_idx(self):
240 commit = self.repo.get_commit(commit_idx=-1)
240 commit = self.repo.get_commit(commit_idx=-1)
241 assert commit.idx >= 0
241 assert commit.idx >= 0
242
242
243 def test_revision_is_deprecated(self):
243 def test_revision_is_deprecated(self):
244 def get_revision(commit):
244 def get_revision(commit):
245 return commit.revision
245 return commit.revision
246
246
247 commit = self.repo[0]
247 commit = self.repo[0]
248 pytest.deprecated_call(get_revision, commit)
248 pytest.deprecated_call(get_revision, commit)
249
249
250 def test_size(self):
250 def test_size(self):
251 tip = self.repo.get_commit()
251 tip = self.repo.get_commit()
252 size = 5 * len('Foobar N') # Size of 5 files
252 size = 5 * len('Foobar N') # Size of 5 files
253 assert tip.size == size
253 assert tip.size == size
254
254
255 def test_size_at_commit(self):
255 def test_size_at_commit(self):
256 tip = self.repo.get_commit()
256 tip = self.repo.get_commit()
257 size = 5 * len('Foobar N') # Size of 5 files
257 size = 5 * len('Foobar N') # Size of 5 files
258 assert self.repo.size_at_commit(tip.raw_id) == size
258 assert self.repo.size_at_commit(tip.raw_id) == size
259
259
260 def test_size_at_first_commit(self):
260 def test_size_at_first_commit(self):
261 commit = self.repo[0]
261 commit = self.repo[0]
262 size = len('Foobar N') # Size of 1 file
262 size = len('Foobar N') # Size of 1 file
263 assert self.repo.size_at_commit(commit.raw_id) == size
263 assert self.repo.size_at_commit(commit.raw_id) == size
264
264
265 def test_author(self):
265 def test_author(self):
266 tip = self.repo.get_commit()
266 tip = self.repo.get_commit()
267 assert_text_equal(tip.author, u'Joe Doe <joe.doe@example.com>')
267 assert_text_equal(tip.author, u'Joe Doe <joe.doe@example.com>')
268
268
269 def test_author_name(self):
269 def test_author_name(self):
270 tip = self.repo.get_commit()
270 tip = self.repo.get_commit()
271 assert_text_equal(tip.author_name, u'Joe Doe')
271 assert_text_equal(tip.author_name, u'Joe Doe')
272
272
273 def test_author_email(self):
273 def test_author_email(self):
274 tip = self.repo.get_commit()
274 tip = self.repo.get_commit()
275 assert_text_equal(tip.author_email, u'joe.doe@example.com')
275 assert_text_equal(tip.author_email, u'joe.doe@example.com')
276
276
277 def test_message(self):
277 def test_message(self):
278 tip = self.repo.get_commit()
278 tip = self.repo.get_commit()
279 assert_text_equal(tip.message, u'Commit 4')
279 assert_text_equal(tip.message, u'Commit 4')
280
280
281 def test_diff(self):
281 def test_diff(self):
282 tip = self.repo.get_commit()
282 tip = self.repo.get_commit()
283 diff = tip.diff()
283 diff = tip.diff()
284 assert "+Foobar 4" in diff.raw
284 assert "+Foobar 4" in diff.raw
285
285
286 def test_prev(self):
286 def test_prev(self):
287 tip = self.repo.get_commit()
287 tip = self.repo.get_commit()
288 prev_commit = tip.prev()
288 prev_commit = tip.prev()
289 assert prev_commit.message == 'Commit 3'
289 assert prev_commit.message == 'Commit 3'
290
290
291 def test_prev_raises_on_first_commit(self):
291 def test_prev_raises_on_first_commit(self):
292 commit = self.repo.get_commit(commit_idx=0)
292 commit = self.repo.get_commit(commit_idx=0)
293 with pytest.raises(CommitDoesNotExistError):
293 with pytest.raises(CommitDoesNotExistError):
294 commit.prev()
294 commit.prev()
295
295
296 def test_prev_works_on_second_commit_issue_183(self):
296 def test_prev_works_on_second_commit_issue_183(self):
297 commit = self.repo.get_commit(commit_idx=1)
297 commit = self.repo.get_commit(commit_idx=1)
298 prev_commit = commit.prev()
298 prev_commit = commit.prev()
299 assert prev_commit.idx == 0
299 assert prev_commit.idx == 0
300
300
301 def test_next(self):
301 def test_next(self):
302 commit = self.repo.get_commit(commit_idx=2)
302 commit = self.repo.get_commit(commit_idx=2)
303 next_commit = commit.next()
303 next_commit = commit.next()
304 assert next_commit.message == 'Commit 3'
304 assert next_commit.message == 'Commit 3'
305
305
306 def test_next_raises_on_tip(self):
306 def test_next_raises_on_tip(self):
307 commit = self.repo.get_commit()
307 commit = self.repo.get_commit()
308 with pytest.raises(CommitDoesNotExistError):
308 with pytest.raises(CommitDoesNotExistError):
309 commit.next()
309 commit.next()
310
310
311 def test_get_file_commit(self):
311 def test_get_file_commit(self):
312 commit = self.repo.get_commit()
312 commit = self.repo.get_commit()
313 commit.get_file_commit('file_4.txt')
313 commit.get_file_commit('file_4.txt')
314 assert commit.message == 'Commit 4'
314 assert commit.message == 'Commit 4'
315
315
316 def test_get_filenodes_generator(self):
316 def test_get_filenodes_generator(self):
317 tip = self.repo.get_commit()
317 tip = self.repo.get_commit()
318 filepaths = [node.path for node in tip.get_filenodes_generator()]
318 filepaths = [node.path for node in tip.get_filenodes_generator()]
319 assert filepaths == ['file_%d.txt' % x for x in xrange(5)]
319 assert filepaths == ['file_%d.txt' % x for x in xrange(5)]
320
320
321 def test_get_file_annotate(self):
321 def test_get_file_annotate(self):
322 file_added_commit = self.repo.get_commit(commit_idx=3)
322 file_added_commit = self.repo.get_commit(commit_idx=3)
323 annotations = list(file_added_commit.get_file_annotate('file_3.txt'))
323 annotations = list(file_added_commit.get_file_annotate('file_3.txt'))
324 line_no, commit_id, commit_loader, line = annotations[0]
324 line_no, commit_id, commit_loader, line = annotations[0]
325 assert line_no == 1
325 assert line_no == 1
326 assert commit_id == file_added_commit.raw_id
326 assert commit_id == file_added_commit.raw_id
327 assert commit_loader() == file_added_commit
327 assert commit_loader() == file_added_commit
328
328
329 # git annotation is generated differently thus different results
329 # git annotation is generated differently thus different results
330 if self.repo.alias == 'git':
330 if self.repo.alias == 'git':
331 assert line == '(Joe Doe 2010-01-03 08:00:00 +0000 1) Foobar 3'
331 assert line == '(Joe Doe 2010-01-03 08:00:00 +0000 1) Foobar 3'
332 else:
332 else:
333 assert line == 'Foobar 3'
333 assert line == 'Foobar 3'
334
334
335 def test_get_file_annotate_does_not_exist(self):
335 def test_get_file_annotate_does_not_exist(self):
336 file_added_commit = self.repo.get_commit(commit_idx=2)
336 file_added_commit = self.repo.get_commit(commit_idx=2)
337 # TODO: Should use a specific exception class here?
337 # TODO: Should use a specific exception class here?
338 with pytest.raises(Exception):
338 with pytest.raises(Exception):
339 list(file_added_commit.get_file_annotate('file_3.txt'))
339 list(file_added_commit.get_file_annotate('file_3.txt'))
340
340
341 def test_get_file_annotate_tip(self):
341 def test_get_file_annotate_tip(self):
342 tip = self.repo.get_commit()
342 tip = self.repo.get_commit()
343 commit = self.repo.get_commit(commit_idx=3)
343 commit = self.repo.get_commit(commit_idx=3)
344 expected_values = list(commit.get_file_annotate('file_3.txt'))
344 expected_values = list(commit.get_file_annotate('file_3.txt'))
345 annotations = list(tip.get_file_annotate('file_3.txt'))
345 annotations = list(tip.get_file_annotate('file_3.txt'))
346
346
347 # Note: Skip index 2 because the loader function is not the same
347 # Note: Skip index 2 because the loader function is not the same
348 for idx in (0, 1, 3):
348 for idx in (0, 1, 3):
349 assert annotations[0][idx] == expected_values[0][idx]
349 assert annotations[0][idx] == expected_values[0][idx]
350
350
351 def test_get_commits_is_ordered_by_date(self):
351 def test_get_commits_is_ordered_by_date(self):
352 commits = self.repo.get_commits()
352 commits = self.repo.get_commits()
353 assert isinstance(commits, CollectionGenerator)
353 assert isinstance(commits, CollectionGenerator)
354 assert len(commits) == 0 or len(commits) != 0
354 assert len(commits) == 0 or len(commits) != 0
355 commits = list(commits)
355 commits = list(commits)
356 ordered_by_date = sorted(commits, key=lambda commit: commit.date)
356 ordered_by_date = sorted(commits, key=lambda commit: commit.date)
357 assert commits == ordered_by_date
357 assert commits == ordered_by_date
358
358
359 def test_get_commits_respects_start(self):
359 def test_get_commits_respects_start(self):
360 second_id = self.repo.commit_ids[1]
360 second_id = self.repo.commit_ids[1]
361 commits = self.repo.get_commits(start_id=second_id)
361 commits = self.repo.get_commits(start_id=second_id)
362 assert isinstance(commits, CollectionGenerator)
362 assert isinstance(commits, CollectionGenerator)
363 commits = list(commits)
363 commits = list(commits)
364 assert len(commits) == 4
364 assert len(commits) == 4
365
365
366 def test_get_commits_includes_start_commit(self):
366 def test_get_commits_includes_start_commit(self):
367 second_id = self.repo.commit_ids[1]
367 second_id = self.repo.commit_ids[1]
368 commits = self.repo.get_commits(start_id=second_id)
368 commits = self.repo.get_commits(start_id=second_id)
369 assert isinstance(commits, CollectionGenerator)
369 assert isinstance(commits, CollectionGenerator)
370 commits = list(commits)
370 commits = list(commits)
371 assert commits[0].raw_id == second_id
371 assert commits[0].raw_id == second_id
372
372
373 def test_get_commits_respects_end(self):
373 def test_get_commits_respects_end(self):
374 second_id = self.repo.commit_ids[1]
374 second_id = self.repo.commit_ids[1]
375 commits = self.repo.get_commits(end_id=second_id)
375 commits = self.repo.get_commits(end_id=second_id)
376 assert isinstance(commits, CollectionGenerator)
376 assert isinstance(commits, CollectionGenerator)
377 commits = list(commits)
377 commits = list(commits)
378 assert commits[-1].raw_id == second_id
378 assert commits[-1].raw_id == second_id
379 assert len(commits) == 2
379 assert len(commits) == 2
380
380
381 def test_get_commits_respects_both_start_and_end(self):
381 def test_get_commits_respects_both_start_and_end(self):
382 second_id = self.repo.commit_ids[1]
382 second_id = self.repo.commit_ids[1]
383 third_id = self.repo.commit_ids[2]
383 third_id = self.repo.commit_ids[2]
384 commits = self.repo.get_commits(start_id=second_id, end_id=third_id)
384 commits = self.repo.get_commits(start_id=second_id, end_id=third_id)
385 assert isinstance(commits, CollectionGenerator)
385 assert isinstance(commits, CollectionGenerator)
386 commits = list(commits)
386 commits = list(commits)
387 assert len(commits) == 2
387 assert len(commits) == 2
388
388
389 def test_get_commits_on_empty_repo_raises_EmptyRepository_error(self):
389 def test_get_commits_on_empty_repo_raises_EmptyRepository_error(self):
390 repo_path = get_new_dir(str(time.time()))
390 repo_path = get_new_dir(str(time.time()))
391 repo = self.Backend(repo_path, create=True)
391 repo = self.Backend(repo_path, create=True)
392
392
393 with pytest.raises(EmptyRepositoryError):
393 with pytest.raises(EmptyRepositoryError):
394 list(repo.get_commits(start_id='foobar'))
394 list(repo.get_commits(start_id='foobar'))
395
395
396 def test_get_commits_includes_end_commit(self):
396 def test_get_commits_includes_end_commit(self):
397 second_id = self.repo.commit_ids[1]
397 second_id = self.repo.commit_ids[1]
398 commits = self.repo.get_commits(end_id=second_id)
398 commits = self.repo.get_commits(end_id=second_id)
399 assert isinstance(commits, CollectionGenerator)
399 assert isinstance(commits, CollectionGenerator)
400 assert len(commits) == 2
400 assert len(commits) == 2
401 commits = list(commits)
401 commits = list(commits)
402 assert commits[-1].raw_id == second_id
402 assert commits[-1].raw_id == second_id
403
403
404 def test_get_commits_respects_start_date(self):
404 def test_get_commits_respects_start_date(self):
405 start_date = datetime.datetime(2010, 1, 2)
405 start_date = datetime.datetime(2010, 1, 2)
406 commits = self.repo.get_commits(start_date=start_date)
406 commits = self.repo.get_commits(start_date=start_date)
407 assert isinstance(commits, CollectionGenerator)
407 assert isinstance(commits, CollectionGenerator)
408 # Should be 4 commits after 2010-01-02 00:00:00
408 # Should be 4 commits after 2010-01-02 00:00:00
409 assert len(commits) == 4
409 assert len(commits) == 4
410 for c in commits:
410 for c in commits:
411 assert c.date >= start_date
411 assert c.date >= start_date
412
412
413 def test_get_commits_respects_start_date_and_end_date(self):
413 def test_get_commits_respects_start_date_and_end_date(self):
414 start_date = datetime.datetime(2010, 1, 2)
414 start_date = datetime.datetime(2010, 1, 2)
415 end_date = datetime.datetime(2010, 1, 3)
415 end_date = datetime.datetime(2010, 1, 3)
416 commits = self.repo.get_commits(start_date=start_date,
416 commits = self.repo.get_commits(start_date=start_date,
417 end_date=end_date)
417 end_date=end_date)
418 assert isinstance(commits, CollectionGenerator)
418 assert isinstance(commits, CollectionGenerator)
419 assert len(commits) == 2
419 assert len(commits) == 2
420 for c in commits:
420 for c in commits:
421 assert c.date >= start_date
421 assert c.date >= start_date
422 assert c.date <= end_date
422 assert c.date <= end_date
423
423
424 def test_get_commits_respects_end_date(self):
424 def test_get_commits_respects_end_date(self):
425 end_date = datetime.datetime(2010, 1, 2)
425 end_date = datetime.datetime(2010, 1, 2)
426 commits = self.repo.get_commits(end_date=end_date)
426 commits = self.repo.get_commits(end_date=end_date)
427 assert isinstance(commits, CollectionGenerator)
427 assert isinstance(commits, CollectionGenerator)
428 assert len(commits) == 1
428 assert len(commits) == 1
429 for c in commits:
429 for c in commits:
430 assert c.date <= end_date
430 assert c.date <= end_date
431
431
432 def test_get_commits_respects_reverse(self):
432 def test_get_commits_respects_reverse(self):
433 commits = self.repo.get_commits() # no longer reverse support
433 commits = self.repo.get_commits() # no longer reverse support
434 assert isinstance(commits, CollectionGenerator)
434 assert isinstance(commits, CollectionGenerator)
435 assert len(commits) == 5
435 assert len(commits) == 5
436 commit_ids = reversed([c.raw_id for c in commits])
436 commit_ids = reversed([c.raw_id for c in commits])
437 assert list(commit_ids) == list(reversed(self.repo.commit_ids))
437 assert list(commit_ids) == list(reversed(self.repo.commit_ids))
438
438
439 def test_get_commits_slice_generator(self):
439 def test_get_commits_slice_generator(self):
440 commits = self.repo.get_commits(
440 commits = self.repo.get_commits(
441 branch_name=self.repo.DEFAULT_BRANCH_NAME)
441 branch_name=self.repo.DEFAULT_BRANCH_NAME)
442 assert isinstance(commits, CollectionGenerator)
442 assert isinstance(commits, CollectionGenerator)
443 commit_slice = list(commits[1:3])
443 commit_slice = list(commits[1:3])
444 assert len(commit_slice) == 2
444 assert len(commit_slice) == 2
445
445
446 def test_get_commits_raise_commitdoesnotexist_for_wrong_start(self):
446 def test_get_commits_raise_commitdoesnotexist_for_wrong_start(self):
447 with pytest.raises(CommitDoesNotExistError):
447 with pytest.raises(CommitDoesNotExistError):
448 list(self.repo.get_commits(start_id='foobar'))
448 list(self.repo.get_commits(start_id='foobar'))
449
449
450 def test_get_commits_raise_commitdoesnotexist_for_wrong_end(self):
450 def test_get_commits_raise_commitdoesnotexist_for_wrong_end(self):
451 with pytest.raises(CommitDoesNotExistError):
451 with pytest.raises(CommitDoesNotExistError):
452 list(self.repo.get_commits(end_id='foobar'))
452 list(self.repo.get_commits(end_id='foobar'))
453
453
454 def test_get_commits_raise_branchdoesnotexist_for_wrong_branch_name(self):
454 def test_get_commits_raise_branchdoesnotexist_for_wrong_branch_name(self):
455 with pytest.raises(BranchDoesNotExistError):
455 with pytest.raises(BranchDoesNotExistError):
456 list(self.repo.get_commits(branch_name='foobar'))
456 list(self.repo.get_commits(branch_name='foobar'))
457
457
458 def test_get_commits_raise_repositoryerror_for_wrong_start_end(self):
458 def test_get_commits_raise_repositoryerror_for_wrong_start_end(self):
459 start_id = self.repo.commit_ids[-1]
459 start_id = self.repo.commit_ids[-1]
460 end_id = self.repo.commit_ids[0]
460 end_id = self.repo.commit_ids[0]
461 with pytest.raises(RepositoryError):
461 with pytest.raises(RepositoryError):
462 list(self.repo.get_commits(start_id=start_id, end_id=end_id))
462 list(self.repo.get_commits(start_id=start_id, end_id=end_id))
463
463
464 def test_get_commits_raises_for_numerical_ids(self):
464 def test_get_commits_raises_for_numerical_ids(self):
465 with pytest.raises(TypeError):
465 with pytest.raises(TypeError):
466 self.repo.get_commits(start_id=1, end_id=2)
466 self.repo.get_commits(start_id=1, end_id=2)
467
467
468 def test_commit_equality(self):
469 commit1 = self.repo.get_commit(self.repo.commit_ids[0])
470 commit2 = self.repo.get_commit(self.repo.commit_ids[1])
471
472 assert commit1 == commit1
473 assert commit2 == commit2
474 assert commit1 != commit2
475 assert commit2 != commit1
476 assert commit1 != None
477 assert None != commit1
478 assert 1 != commit1
479 assert 'string' != commit1
480
468
481
469 @pytest.mark.parametrize("filename, expected", [
482 @pytest.mark.parametrize("filename, expected", [
470 ("README.rst", False),
483 ("README.rst", False),
471 ("README", True),
484 ("README", True),
472 ])
485 ])
473 def test_commit_is_link(vcsbackend, filename, expected):
486 def test_commit_is_link(vcsbackend, filename, expected):
474 commit = vcsbackend.repo.get_commit()
487 commit = vcsbackend.repo.get_commit()
475 link_status = commit.is_link(filename)
488 link_status = commit.is_link(filename)
476 assert link_status is expected
489 assert link_status is expected
477
490
478
491
479 class TestCommitsChanges(BackendTestMixin):
492 class TestCommitsChanges(BackendTestMixin):
480 recreate_repo_per_test = False
493 recreate_repo_per_test = False
481
494
482 @classmethod
495 @classmethod
483 def _get_commits(cls):
496 def _get_commits(cls):
484 return [
497 return [
485 {
498 {
486 'message': u'Initial',
499 'message': u'Initial',
487 'author': u'Joe Doe <joe.doe@example.com>',
500 'author': u'Joe Doe <joe.doe@example.com>',
488 'date': datetime.datetime(2010, 1, 1, 20),
501 'date': datetime.datetime(2010, 1, 1, 20),
489 'added': [
502 'added': [
490 FileNode('foo/bar', content='foo'),
503 FileNode('foo/bar', content='foo'),
491 FileNode('foo/bał', content='foo'),
504 FileNode('foo/bał', content='foo'),
492 FileNode('foobar', content='foo'),
505 FileNode('foobar', content='foo'),
493 FileNode('qwe', content='foo'),
506 FileNode('qwe', content='foo'),
494 ],
507 ],
495 },
508 },
496 {
509 {
497 'message': u'Massive changes',
510 'message': u'Massive changes',
498 'author': u'Joe Doe <joe.doe@example.com>',
511 'author': u'Joe Doe <joe.doe@example.com>',
499 'date': datetime.datetime(2010, 1, 1, 22),
512 'date': datetime.datetime(2010, 1, 1, 22),
500 'added': [FileNode('fallout', content='War never changes')],
513 'added': [FileNode('fallout', content='War never changes')],
501 'changed': [
514 'changed': [
502 FileNode('foo/bar', content='baz'),
515 FileNode('foo/bar', content='baz'),
503 FileNode('foobar', content='baz'),
516 FileNode('foobar', content='baz'),
504 ],
517 ],
505 'removed': [FileNode('qwe')],
518 'removed': [FileNode('qwe')],
506 },
519 },
507 ]
520 ]
508
521
509 def test_initial_commit(self):
522 def test_initial_commit(self):
510 commit = self.repo.get_commit(commit_idx=0)
523 commit = self.repo.get_commit(commit_idx=0)
511 assert set(commit.added) == set([
524 assert set(commit.added) == set([
512 commit.get_node('foo/bar'),
525 commit.get_node('foo/bar'),
513 commit.get_node('foo/bał'),
526 commit.get_node('foo/bał'),
514 commit.get_node('foobar'),
527 commit.get_node('foobar'),
515 commit.get_node('qwe'),
528 commit.get_node('qwe'),
516 ])
529 ])
517 assert set(commit.changed) == set()
530 assert set(commit.changed) == set()
518 assert set(commit.removed) == set()
531 assert set(commit.removed) == set()
519 assert set(commit.affected_files) == set(
532 assert set(commit.affected_files) == set(
520 ['foo/bar', 'foo/bał', 'foobar', 'qwe'])
533 ['foo/bar', 'foo/bał', 'foobar', 'qwe'])
521 assert commit.date == datetime.datetime(2010, 1, 1, 20, 0)
534 assert commit.date == datetime.datetime(2010, 1, 1, 20, 0)
522
535
523 def test_head_added(self):
536 def test_head_added(self):
524 commit = self.repo.get_commit()
537 commit = self.repo.get_commit()
525 assert isinstance(commit.added, AddedFileNodesGenerator)
538 assert isinstance(commit.added, AddedFileNodesGenerator)
526 assert set(commit.added) == set([commit.get_node('fallout')])
539 assert set(commit.added) == set([commit.get_node('fallout')])
527 assert isinstance(commit.changed, ChangedFileNodesGenerator)
540 assert isinstance(commit.changed, ChangedFileNodesGenerator)
528 assert set(commit.changed) == set([
541 assert set(commit.changed) == set([
529 commit.get_node('foo/bar'),
542 commit.get_node('foo/bar'),
530 commit.get_node('foobar'),
543 commit.get_node('foobar'),
531 ])
544 ])
532 assert isinstance(commit.removed, RemovedFileNodesGenerator)
545 assert isinstance(commit.removed, RemovedFileNodesGenerator)
533 assert len(commit.removed) == 1
546 assert len(commit.removed) == 1
534 assert list(commit.removed)[0].path == 'qwe'
547 assert list(commit.removed)[0].path == 'qwe'
535
548
536 def test_get_filemode(self):
549 def test_get_filemode(self):
537 commit = self.repo.get_commit()
550 commit = self.repo.get_commit()
538 assert FILEMODE_DEFAULT == commit.get_file_mode('foo/bar')
551 assert FILEMODE_DEFAULT == commit.get_file_mode('foo/bar')
539
552
540 def test_get_filemode_non_ascii(self):
553 def test_get_filemode_non_ascii(self):
541 commit = self.repo.get_commit()
554 commit = self.repo.get_commit()
542 assert FILEMODE_DEFAULT == commit.get_file_mode('foo/bał')
555 assert FILEMODE_DEFAULT == commit.get_file_mode('foo/bał')
543 assert FILEMODE_DEFAULT == commit.get_file_mode(u'foo/bał')
556 assert FILEMODE_DEFAULT == commit.get_file_mode(u'foo/bał')
544
557
545 def test_get_file_history(self):
558 def test_get_file_history(self):
546 commit = self.repo.get_commit()
559 commit = self.repo.get_commit()
547 history = commit.get_file_history('foo/bar')
560 history = commit.get_file_history('foo/bar')
548 assert len(history) == 2
561 assert len(history) == 2
549
562
550 def test_get_file_history_with_limit(self):
563 def test_get_file_history_with_limit(self):
551 commit = self.repo.get_commit()
564 commit = self.repo.get_commit()
552 history = commit.get_file_history('foo/bar', limit=1)
565 history = commit.get_file_history('foo/bar', limit=1)
553 assert len(history) == 1
566 assert len(history) == 1
554
567
555 def test_get_file_history_first_commit(self):
568 def test_get_file_history_first_commit(self):
556 commit = self.repo[0]
569 commit = self.repo[0]
557 history = commit.get_file_history('foo/bar')
570 history = commit.get_file_history('foo/bar')
558 assert len(history) == 1
571 assert len(history) == 1
559
572
560
573
561 def assert_text_equal(expected, given):
574 def assert_text_equal(expected, given):
562 assert expected == given
575 assert expected == given
563 assert isinstance(expected, unicode)
576 assert isinstance(expected, unicode)
564 assert isinstance(given, unicode)
577 assert isinstance(given, unicode)
General Comments 0
You need to be logged in to leave comments. Login now