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