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