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