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