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