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