##// END OF EJS Templates
subrepo: Handle subrepo merge errors.
Martin Bornhold -
r1108:ebe0247c default
parent child Browse files
Show More
@@ -1,803 +1,808 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, Reference)
41 MergeFailureReason, Reference)
42 from rhodecode.lib.vcs.backends.hg.commit import MercurialCommit
42 from rhodecode.lib.vcs.backends.hg.commit import MercurialCommit
43 from rhodecode.lib.vcs.backends.hg.diff import MercurialDiff
43 from rhodecode.lib.vcs.backends.hg.diff import MercurialDiff
44 from rhodecode.lib.vcs.backends.hg.inmemory import MercurialInMemoryCommit
44 from rhodecode.lib.vcs.backends.hg.inmemory import MercurialInMemoryCommit
45 from rhodecode.lib.vcs.exceptions import (
45 from rhodecode.lib.vcs.exceptions import (
46 EmptyRepositoryError, RepositoryError, TagAlreadyExistError,
46 EmptyRepositoryError, RepositoryError, TagAlreadyExistError,
47 TagDoesNotExistError, CommitDoesNotExistError)
47 TagDoesNotExistError, CommitDoesNotExistError, SubrepoMergeError)
48
48
49 hexlify = binascii.hexlify
49 hexlify = binascii.hexlify
50 nullid = "\0" * 20
50 nullid = "\0" * 20
51
51
52 log = logging.getLogger(__name__)
52 log = logging.getLogger(__name__)
53
53
54
54
55 class MercurialRepository(BaseRepository):
55 class MercurialRepository(BaseRepository):
56 """
56 """
57 Mercurial repository backend
57 Mercurial repository backend
58 """
58 """
59 DEFAULT_BRANCH_NAME = 'default'
59 DEFAULT_BRANCH_NAME = 'default'
60
60
61 def __init__(self, repo_path, config=None, create=False, src_url=None,
61 def __init__(self, repo_path, config=None, create=False, src_url=None,
62 update_after_clone=False, with_wire=None):
62 update_after_clone=False, with_wire=None):
63 """
63 """
64 Raises RepositoryError if repository could not be find at the given
64 Raises RepositoryError if repository could not be find at the given
65 ``repo_path``.
65 ``repo_path``.
66
66
67 :param repo_path: local path of the repository
67 :param repo_path: local path of the repository
68 :param config: config object containing the repo configuration
68 :param config: config object containing the repo configuration
69 :param create=False: if set to True, would try to create repository if
69 :param create=False: if set to True, would try to create repository if
70 it does not exist rather than raising exception
70 it does not exist rather than raising exception
71 :param src_url=None: would try to clone repository from given location
71 :param src_url=None: would try to clone repository from given location
72 :param update_after_clone=False: sets update of working copy after
72 :param update_after_clone=False: sets update of working copy after
73 making a clone
73 making a clone
74 """
74 """
75 self.path = safe_str(os.path.abspath(repo_path))
75 self.path = safe_str(os.path.abspath(repo_path))
76 self.config = config if config else Config()
76 self.config = config if config else Config()
77 self._remote = connection.Hg(
77 self._remote = connection.Hg(
78 self.path, self.config, with_wire=with_wire)
78 self.path, self.config, with_wire=with_wire)
79
79
80 self._init_repo(create, src_url, update_after_clone)
80 self._init_repo(create, src_url, update_after_clone)
81
81
82 # caches
82 # caches
83 self._commit_ids = {}
83 self._commit_ids = {}
84
84
85 @LazyProperty
85 @LazyProperty
86 def commit_ids(self):
86 def commit_ids(self):
87 """
87 """
88 Returns list of commit ids, in ascending order. Being lazy
88 Returns list of commit ids, in ascending order. Being lazy
89 attribute allows external tools to inject shas from cache.
89 attribute allows external tools to inject shas from cache.
90 """
90 """
91 commit_ids = self._get_all_commit_ids()
91 commit_ids = self._get_all_commit_ids()
92 self._rebuild_cache(commit_ids)
92 self._rebuild_cache(commit_ids)
93 return commit_ids
93 return commit_ids
94
94
95 def _rebuild_cache(self, commit_ids):
95 def _rebuild_cache(self, commit_ids):
96 self._commit_ids = dict((commit_id, index)
96 self._commit_ids = dict((commit_id, index)
97 for index, commit_id in enumerate(commit_ids))
97 for index, commit_id in enumerate(commit_ids))
98
98
99 @LazyProperty
99 @LazyProperty
100 def branches(self):
100 def branches(self):
101 return self._get_branches()
101 return self._get_branches()
102
102
103 @LazyProperty
103 @LazyProperty
104 def branches_closed(self):
104 def branches_closed(self):
105 return self._get_branches(active=False, closed=True)
105 return self._get_branches(active=False, closed=True)
106
106
107 @LazyProperty
107 @LazyProperty
108 def branches_all(self):
108 def branches_all(self):
109 all_branches = {}
109 all_branches = {}
110 all_branches.update(self.branches)
110 all_branches.update(self.branches)
111 all_branches.update(self.branches_closed)
111 all_branches.update(self.branches_closed)
112 return all_branches
112 return all_branches
113
113
114 def _get_branches(self, active=True, closed=False):
114 def _get_branches(self, active=True, closed=False):
115 """
115 """
116 Gets branches for this repository
116 Gets branches for this repository
117 Returns only not closed active branches by default
117 Returns only not closed active branches by default
118
118
119 :param active: return also active branches
119 :param active: return also active branches
120 :param closed: return also closed branches
120 :param closed: return also closed branches
121
121
122 """
122 """
123 if self.is_empty():
123 if self.is_empty():
124 return {}
124 return {}
125
125
126 def get_name(ctx):
126 def get_name(ctx):
127 return ctx[0]
127 return ctx[0]
128
128
129 _branches = [(safe_unicode(n), hexlify(h),) for n, h in
129 _branches = [(safe_unicode(n), hexlify(h),) for n, h in
130 self._remote.branches(active, closed).items()]
130 self._remote.branches(active, closed).items()]
131
131
132 return OrderedDict(sorted(_branches, key=get_name, reverse=False))
132 return OrderedDict(sorted(_branches, key=get_name, reverse=False))
133
133
134 @LazyProperty
134 @LazyProperty
135 def tags(self):
135 def tags(self):
136 """
136 """
137 Gets tags for this repository
137 Gets tags for this repository
138 """
138 """
139 return self._get_tags()
139 return self._get_tags()
140
140
141 def _get_tags(self):
141 def _get_tags(self):
142 if self.is_empty():
142 if self.is_empty():
143 return {}
143 return {}
144
144
145 def get_name(ctx):
145 def get_name(ctx):
146 return ctx[0]
146 return ctx[0]
147
147
148 _tags = [(safe_unicode(n), hexlify(h),) for n, h in
148 _tags = [(safe_unicode(n), hexlify(h),) for n, h in
149 self._remote.tags().items()]
149 self._remote.tags().items()]
150
150
151 return OrderedDict(sorted(_tags, key=get_name, reverse=True))
151 return OrderedDict(sorted(_tags, key=get_name, reverse=True))
152
152
153 def tag(self, name, user, commit_id=None, message=None, date=None,
153 def tag(self, name, user, commit_id=None, message=None, date=None,
154 **kwargs):
154 **kwargs):
155 """
155 """
156 Creates and returns a tag for the given ``commit_id``.
156 Creates and returns a tag for the given ``commit_id``.
157
157
158 :param name: name for new tag
158 :param name: name for new tag
159 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
159 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
160 :param commit_id: commit id for which new tag would be created
160 :param commit_id: commit id for which new tag would be created
161 :param message: message of the tag's commit
161 :param message: message of the tag's commit
162 :param date: date of tag's commit
162 :param date: date of tag's commit
163
163
164 :raises TagAlreadyExistError: if tag with same name already exists
164 :raises TagAlreadyExistError: if tag with same name already exists
165 """
165 """
166 if name in self.tags:
166 if name in self.tags:
167 raise TagAlreadyExistError("Tag %s already exists" % name)
167 raise TagAlreadyExistError("Tag %s already exists" % name)
168 commit = self.get_commit(commit_id=commit_id)
168 commit = self.get_commit(commit_id=commit_id)
169 local = kwargs.setdefault('local', False)
169 local = kwargs.setdefault('local', False)
170
170
171 if message is None:
171 if message is None:
172 message = "Added tag %s for commit %s" % (name, commit.short_id)
172 message = "Added tag %s for commit %s" % (name, commit.short_id)
173
173
174 date, tz = date_to_timestamp_plus_offset(date)
174 date, tz = date_to_timestamp_plus_offset(date)
175
175
176 self._remote.tag(
176 self._remote.tag(
177 name, commit.raw_id, message, local, user, date, tz)
177 name, commit.raw_id, message, local, user, date, tz)
178 self._remote.invalidate_vcs_cache()
178 self._remote.invalidate_vcs_cache()
179
179
180 # Reinitialize tags
180 # Reinitialize tags
181 self.tags = self._get_tags()
181 self.tags = self._get_tags()
182 tag_id = self.tags[name]
182 tag_id = self.tags[name]
183
183
184 return self.get_commit(commit_id=tag_id)
184 return self.get_commit(commit_id=tag_id)
185
185
186 def remove_tag(self, name, user, message=None, date=None):
186 def remove_tag(self, name, user, message=None, date=None):
187 """
187 """
188 Removes tag with the given `name`.
188 Removes tag with the given `name`.
189
189
190 :param name: name of the tag to be removed
190 :param name: name of the tag to be removed
191 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
191 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
192 :param message: message of the tag's removal commit
192 :param message: message of the tag's removal commit
193 :param date: date of tag's removal commit
193 :param date: date of tag's removal commit
194
194
195 :raises TagDoesNotExistError: if tag with given name does not exists
195 :raises TagDoesNotExistError: if tag with given name does not exists
196 """
196 """
197 if name not in self.tags:
197 if name not in self.tags:
198 raise TagDoesNotExistError("Tag %s does not exist" % name)
198 raise TagDoesNotExistError("Tag %s does not exist" % name)
199 if message is None:
199 if message is None:
200 message = "Removed tag %s" % name
200 message = "Removed tag %s" % name
201 local = False
201 local = False
202
202
203 date, tz = date_to_timestamp_plus_offset(date)
203 date, tz = date_to_timestamp_plus_offset(date)
204
204
205 self._remote.tag(name, nullid, message, local, user, date, tz)
205 self._remote.tag(name, nullid, message, local, user, date, tz)
206 self._remote.invalidate_vcs_cache()
206 self._remote.invalidate_vcs_cache()
207 self.tags = self._get_tags()
207 self.tags = self._get_tags()
208
208
209 @LazyProperty
209 @LazyProperty
210 def bookmarks(self):
210 def bookmarks(self):
211 """
211 """
212 Gets bookmarks for this repository
212 Gets bookmarks for this repository
213 """
213 """
214 return self._get_bookmarks()
214 return self._get_bookmarks()
215
215
216 def _get_bookmarks(self):
216 def _get_bookmarks(self):
217 if self.is_empty():
217 if self.is_empty():
218 return {}
218 return {}
219
219
220 def get_name(ctx):
220 def get_name(ctx):
221 return ctx[0]
221 return ctx[0]
222
222
223 _bookmarks = [
223 _bookmarks = [
224 (safe_unicode(n), hexlify(h)) for n, h in
224 (safe_unicode(n), hexlify(h)) for n, h in
225 self._remote.bookmarks().items()]
225 self._remote.bookmarks().items()]
226
226
227 return OrderedDict(sorted(_bookmarks, key=get_name))
227 return OrderedDict(sorted(_bookmarks, key=get_name))
228
228
229 def _get_all_commit_ids(self):
229 def _get_all_commit_ids(self):
230 return self._remote.get_all_commit_ids('visible')
230 return self._remote.get_all_commit_ids('visible')
231
231
232 def get_diff(
232 def get_diff(
233 self, commit1, commit2, path='', ignore_whitespace=False,
233 self, commit1, commit2, path='', ignore_whitespace=False,
234 context=3, path1=None):
234 context=3, path1=None):
235 """
235 """
236 Returns (git like) *diff*, as plain text. Shows changes introduced by
236 Returns (git like) *diff*, as plain text. Shows changes introduced by
237 `commit2` since `commit1`.
237 `commit2` since `commit1`.
238
238
239 :param commit1: Entry point from which diff is shown. Can be
239 :param commit1: Entry point from which diff is shown. Can be
240 ``self.EMPTY_COMMIT`` - in this case, patch showing all
240 ``self.EMPTY_COMMIT`` - in this case, patch showing all
241 the changes since empty state of the repository until `commit2`
241 the changes since empty state of the repository until `commit2`
242 :param commit2: Until which commit changes should be shown.
242 :param commit2: Until which commit changes should be shown.
243 :param ignore_whitespace: If set to ``True``, would not show whitespace
243 :param ignore_whitespace: If set to ``True``, would not show whitespace
244 changes. Defaults to ``False``.
244 changes. Defaults to ``False``.
245 :param context: How many lines before/after changed lines should be
245 :param context: How many lines before/after changed lines should be
246 shown. Defaults to ``3``.
246 shown. Defaults to ``3``.
247 """
247 """
248 self._validate_diff_commits(commit1, commit2)
248 self._validate_diff_commits(commit1, commit2)
249 if path1 is not None and path1 != path:
249 if path1 is not None and path1 != path:
250 raise ValueError("Diff of two different paths not supported.")
250 raise ValueError("Diff of two different paths not supported.")
251
251
252 if path:
252 if path:
253 file_filter = [self.path, path]
253 file_filter = [self.path, path]
254 else:
254 else:
255 file_filter = None
255 file_filter = None
256
256
257 diff = self._remote.diff(
257 diff = self._remote.diff(
258 commit1.raw_id, commit2.raw_id, file_filter=file_filter,
258 commit1.raw_id, commit2.raw_id, file_filter=file_filter,
259 opt_git=True, opt_ignorews=ignore_whitespace,
259 opt_git=True, opt_ignorews=ignore_whitespace,
260 context=context)
260 context=context)
261 return MercurialDiff(diff)
261 return MercurialDiff(diff)
262
262
263 def strip(self, commit_id, branch=None):
263 def strip(self, commit_id, branch=None):
264 self._remote.strip(commit_id, update=False, backup="none")
264 self._remote.strip(commit_id, update=False, backup="none")
265
265
266 self._remote.invalidate_vcs_cache()
266 self._remote.invalidate_vcs_cache()
267 self.commit_ids = self._get_all_commit_ids()
267 self.commit_ids = self._get_all_commit_ids()
268 self._rebuild_cache(self.commit_ids)
268 self._rebuild_cache(self.commit_ids)
269
269
270 def 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 try:
676 try:
677 if (target_ref.type == 'branch' and
677 if (target_ref.type == 'branch' and
678 len(self._heads(target_ref.name)) != 1):
678 len(self._heads(target_ref.name)) != 1):
679 return MergeResponse(
679 return MergeResponse(
680 False, False, None,
680 False, False, None,
681 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS)
681 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS)
682 except CommitDoesNotExistError as e:
682 except CommitDoesNotExistError as e:
683 log.exception('Failure when looking up branch heads on hg target')
683 log.exception('Failure when looking up branch heads on hg target')
684 return MergeResponse(
684 return MergeResponse(
685 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
685 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
686
686
687 shadow_repo = self._get_shadow_instance(shadow_repository_path)
687 shadow_repo = self._get_shadow_instance(shadow_repository_path)
688
688
689 log.debug('Pulling in target reference %s', target_ref)
689 log.debug('Pulling in target reference %s', target_ref)
690 self._validate_pull_reference(target_ref)
690 self._validate_pull_reference(target_ref)
691 shadow_repo._local_pull(self.path, target_ref)
691 shadow_repo._local_pull(self.path, target_ref)
692 try:
692 try:
693 log.debug('Pulling in source reference %s', source_ref)
693 log.debug('Pulling in source reference %s', source_ref)
694 source_repo._validate_pull_reference(source_ref)
694 source_repo._validate_pull_reference(source_ref)
695 shadow_repo._local_pull(source_repo.path, source_ref)
695 shadow_repo._local_pull(source_repo.path, source_ref)
696 except CommitDoesNotExistError:
696 except CommitDoesNotExistError:
697 log.exception('Failure when doing local pull on hg shadow repo')
697 log.exception('Failure when doing local pull on hg shadow repo')
698 return MergeResponse(
698 return MergeResponse(
699 False, False, None, MergeFailureReason.MISSING_SOURCE_REF)
699 False, False, None, MergeFailureReason.MISSING_SOURCE_REF)
700
700
701 merge_ref = None
701 merge_ref = None
702 merge_failure_reason = MergeFailureReason.NONE
702 merge_failure_reason = MergeFailureReason.NONE
703
703
704 try:
704 try:
705 merge_commit_id, needs_push = shadow_repo._local_merge(
705 merge_commit_id, needs_push = shadow_repo._local_merge(
706 target_ref, merge_message, merger_name, merger_email,
706 target_ref, merge_message, merger_name, merger_email,
707 source_ref, use_rebase=use_rebase)
707 source_ref, use_rebase=use_rebase)
708 merge_possible = True
708 merge_possible = True
709
709
710 # Set a bookmark pointing to the merge commit. This bookmark may be
710 # Set a bookmark pointing to the merge commit. This bookmark may be
711 # used to easily identify the last successful merge commit in the
711 # used to easily identify the last successful merge commit in the
712 # shadow repository.
712 # shadow repository.
713 shadow_repo.bookmark('pr-merge', revision=merge_commit_id)
713 shadow_repo.bookmark('pr-merge', revision=merge_commit_id)
714 merge_ref = Reference('book', 'pr-merge', merge_commit_id)
714 merge_ref = Reference('book', 'pr-merge', merge_commit_id)
715 except SubrepoMergeError:
716 log.exception(
717 'Subrepo merge error during local merge on hg shadow repo.')
718 merge_possible = False
719 merge_failure_reason = MergeFailureReason.SUBREPO_MERGE_FAILED
715 except RepositoryError:
720 except RepositoryError:
716 log.exception('Failure when doing local merge on hg shadow repo')
721 log.exception('Failure when doing local merge on hg shadow repo')
717 merge_possible = False
722 merge_possible = False
718 merge_failure_reason = MergeFailureReason.MERGE_FAILED
723 merge_failure_reason = MergeFailureReason.MERGE_FAILED
719
724
720 if merge_possible and not dry_run:
725 if merge_possible and not dry_run:
721 if needs_push:
726 if needs_push:
722 # In case the target is a bookmark, update it, so after pushing
727 # In case the target is a bookmark, update it, so after pushing
723 # the bookmarks is also updated in the target.
728 # the bookmarks is also updated in the target.
724 if target_ref.type == 'book':
729 if target_ref.type == 'book':
725 shadow_repo.bookmark(
730 shadow_repo.bookmark(
726 target_ref.name, revision=merge_commit_id)
731 target_ref.name, revision=merge_commit_id)
727
732
728 try:
733 try:
729 shadow_repo_with_hooks = self._get_shadow_instance(
734 shadow_repo_with_hooks = self._get_shadow_instance(
730 shadow_repository_path,
735 shadow_repository_path,
731 enable_hooks=True)
736 enable_hooks=True)
732 # Note: the push_branches option will push any new branch
737 # Note: the push_branches option will push any new branch
733 # defined in the source repository to the target. This may
738 # defined in the source repository to the target. This may
734 # be dangerous as branches are permanent in Mercurial.
739 # be dangerous as branches are permanent in Mercurial.
735 # This feature was requested in issue #441.
740 # This feature was requested in issue #441.
736 shadow_repo_with_hooks._local_push(
741 shadow_repo_with_hooks._local_push(
737 merge_commit_id, self.path, push_branches=True,
742 merge_commit_id, self.path, push_branches=True,
738 enable_hooks=True)
743 enable_hooks=True)
739 merge_succeeded = True
744 merge_succeeded = True
740 except RepositoryError:
745 except RepositoryError:
741 log.exception(
746 log.exception(
742 'Failure when doing local push from the shadow '
747 'Failure when doing local push from the shadow '
743 'repository to the target repository.')
748 'repository to the target repository.')
744 merge_succeeded = False
749 merge_succeeded = False
745 merge_failure_reason = MergeFailureReason.PUSH_FAILED
750 merge_failure_reason = MergeFailureReason.PUSH_FAILED
746 else:
751 else:
747 merge_succeeded = True
752 merge_succeeded = True
748 else:
753 else:
749 merge_succeeded = False
754 merge_succeeded = False
750
755
751 return MergeResponse(
756 return MergeResponse(
752 merge_possible, merge_succeeded, merge_ref, merge_failure_reason)
757 merge_possible, merge_succeeded, merge_ref, merge_failure_reason)
753
758
754 def _get_shadow_instance(
759 def _get_shadow_instance(
755 self, shadow_repository_path, enable_hooks=False):
760 self, shadow_repository_path, enable_hooks=False):
756 config = self.config.copy()
761 config = self.config.copy()
757 if not enable_hooks:
762 if not enable_hooks:
758 config.clear_section('hooks')
763 config.clear_section('hooks')
759 return MercurialRepository(shadow_repository_path, config)
764 return MercurialRepository(shadow_repository_path, config)
760
765
761 def _validate_pull_reference(self, reference):
766 def _validate_pull_reference(self, reference):
762 if not (reference.name in self.bookmarks or
767 if not (reference.name in self.bookmarks or
763 reference.name in self.branches or
768 reference.name in self.branches or
764 self.get_commit(reference.commit_id)):
769 self.get_commit(reference.commit_id)):
765 raise CommitDoesNotExistError(
770 raise CommitDoesNotExistError(
766 'Unknown branch, bookmark or commit id')
771 'Unknown branch, bookmark or commit id')
767
772
768 def _local_pull(self, repository_path, reference):
773 def _local_pull(self, repository_path, reference):
769 """
774 """
770 Fetch a branch, bookmark or commit from a local repository.
775 Fetch a branch, bookmark or commit from a local repository.
771 """
776 """
772 repository_path = os.path.abspath(repository_path)
777 repository_path = os.path.abspath(repository_path)
773 if repository_path == self.path:
778 if repository_path == self.path:
774 raise ValueError('Cannot pull from the same repository')
779 raise ValueError('Cannot pull from the same repository')
775
780
776 reference_type_to_option_name = {
781 reference_type_to_option_name = {
777 'book': 'bookmark',
782 'book': 'bookmark',
778 'branch': 'branch',
783 'branch': 'branch',
779 }
784 }
780 option_name = reference_type_to_option_name.get(
785 option_name = reference_type_to_option_name.get(
781 reference.type, 'revision')
786 reference.type, 'revision')
782
787
783 if option_name == 'revision':
788 if option_name == 'revision':
784 ref = reference.commit_id
789 ref = reference.commit_id
785 else:
790 else:
786 ref = reference.name
791 ref = reference.name
787
792
788 options = {option_name: [ref]}
793 options = {option_name: [ref]}
789 self._remote.pull_cmd(repository_path, hooks=False, **options)
794 self._remote.pull_cmd(repository_path, hooks=False, **options)
790 self._remote.invalidate_vcs_cache()
795 self._remote.invalidate_vcs_cache()
791
796
792 def bookmark(self, bookmark, revision=None):
797 def bookmark(self, bookmark, revision=None):
793 if isinstance(bookmark, unicode):
798 if isinstance(bookmark, unicode):
794 bookmark = safe_str(bookmark)
799 bookmark = safe_str(bookmark)
795 self._remote.bookmark(bookmark, revision=revision)
800 self._remote.bookmark(bookmark, revision=revision)
796 self._remote.invalidate_vcs_cache()
801 self._remote.invalidate_vcs_cache()
797
802
798
803
799 class MercurialIndexBasedCollectionGenerator(CollectionGenerator):
804 class MercurialIndexBasedCollectionGenerator(CollectionGenerator):
800
805
801 def _commit_factory(self, commit_id):
806 def _commit_factory(self, commit_id):
802 return self.repo.get_commit(
807 return self.repo.get_commit(
803 commit_idx=commit_id, pre_load=self.pre_load)
808 commit_idx=commit_id, pre_load=self.pre_load)
General Comments 0
You need to be logged in to leave comments. Login now