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