##// END OF EJS Templates
mercurial: enabled support of verify command.
marcink -
r1553:f3cfafee default
parent child Browse files
Show More
@@ -1,813 +1,819 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2014-2017 RhodeCode GmbH
3 # Copyright (C) 2014-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 HG repository module
22 HG repository module
23 """
23 """
24
24
25 import logging
25 import logging
26 import binascii
26 import binascii
27 import os
27 import os
28 import shutil
28 import shutil
29 import urllib
29 import urllib
30
30
31 from zope.cachedescriptors.property import Lazy as LazyProperty
31 from zope.cachedescriptors.property import Lazy as LazyProperty
32
32
33 from rhodecode.lib.compat import OrderedDict
33 from rhodecode.lib.compat import OrderedDict
34 from rhodecode.lib.datelib import (
34 from rhodecode.lib.datelib import (
35 date_to_timestamp_plus_offset, utcdate_fromtimestamp, makedate,
35 date_to_timestamp_plus_offset, utcdate_fromtimestamp, makedate,
36 date_astimestamp)
36 date_astimestamp)
37 from rhodecode.lib.utils import safe_unicode, safe_str
37 from rhodecode.lib.utils import safe_unicode, safe_str
38 from rhodecode.lib.vcs import connection
38 from rhodecode.lib.vcs import connection
39 from rhodecode.lib.vcs.backends.base import (
39 from rhodecode.lib.vcs.backends.base import (
40 BaseRepository, CollectionGenerator, Config, MergeResponse,
40 BaseRepository, CollectionGenerator, Config, MergeResponse,
41 MergeFailureReason, Reference)
41 MergeFailureReason, Reference)
42 from rhodecode.lib.vcs.backends.hg.commit import MercurialCommit
42 from rhodecode.lib.vcs.backends.hg.commit import MercurialCommit
43 from rhodecode.lib.vcs.backends.hg.diff import MercurialDiff
43 from rhodecode.lib.vcs.backends.hg.diff import MercurialDiff
44 from rhodecode.lib.vcs.backends.hg.inmemory import MercurialInMemoryCommit
44 from rhodecode.lib.vcs.backends.hg.inmemory import MercurialInMemoryCommit
45 from rhodecode.lib.vcs.exceptions import (
45 from rhodecode.lib.vcs.exceptions import (
46 EmptyRepositoryError, RepositoryError, TagAlreadyExistError,
46 EmptyRepositoryError, RepositoryError, TagAlreadyExistError,
47 TagDoesNotExistError, CommitDoesNotExistError, SubrepoMergeError)
47 TagDoesNotExistError, CommitDoesNotExistError, SubrepoMergeError)
48
48
49 hexlify = binascii.hexlify
49 hexlify = binascii.hexlify
50 nullid = "\0" * 20
50 nullid = "\0" * 20
51
51
52 log = logging.getLogger(__name__)
52 log = logging.getLogger(__name__)
53
53
54
54
55 class MercurialRepository(BaseRepository):
55 class MercurialRepository(BaseRepository):
56 """
56 """
57 Mercurial repository backend
57 Mercurial repository backend
58 """
58 """
59 DEFAULT_BRANCH_NAME = 'default'
59 DEFAULT_BRANCH_NAME = 'default'
60
60
61 def __init__(self, repo_path, config=None, create=False, src_url=None,
61 def __init__(self, repo_path, config=None, create=False, src_url=None,
62 update_after_clone=False, with_wire=None):
62 update_after_clone=False, with_wire=None):
63 """
63 """
64 Raises RepositoryError if repository could not be find at the given
64 Raises RepositoryError if repository could not be find at the given
65 ``repo_path``.
65 ``repo_path``.
66
66
67 :param repo_path: local path of the repository
67 :param repo_path: local path of the repository
68 :param config: config object containing the repo configuration
68 :param config: config object containing the repo configuration
69 :param create=False: if set to True, would try to create repository if
69 :param create=False: if set to True, would try to create repository if
70 it does not exist rather than raising exception
70 it does not exist rather than raising exception
71 :param src_url=None: would try to clone repository from given location
71 :param src_url=None: would try to clone repository from given location
72 :param update_after_clone=False: sets update of working copy after
72 :param update_after_clone=False: sets update of working copy after
73 making a clone
73 making a clone
74 """
74 """
75 self.path = safe_str(os.path.abspath(repo_path))
75 self.path = safe_str(os.path.abspath(repo_path))
76 self.config = config if config else Config()
76 self.config = config if config else Config()
77 self._remote = connection.Hg(
77 self._remote = connection.Hg(
78 self.path, self.config, with_wire=with_wire)
78 self.path, self.config, with_wire=with_wire)
79
79
80 self._init_repo(create, src_url, update_after_clone)
80 self._init_repo(create, src_url, update_after_clone)
81
81
82 # caches
82 # caches
83 self._commit_ids = {}
83 self._commit_ids = {}
84
84
85 @LazyProperty
85 @LazyProperty
86 def commit_ids(self):
86 def commit_ids(self):
87 """
87 """
88 Returns list of commit ids, in ascending order. Being lazy
88 Returns list of commit ids, in ascending order. Being lazy
89 attribute allows external tools to inject shas from cache.
89 attribute allows external tools to inject shas from cache.
90 """
90 """
91 commit_ids = self._get_all_commit_ids()
91 commit_ids = self._get_all_commit_ids()
92 self._rebuild_cache(commit_ids)
92 self._rebuild_cache(commit_ids)
93 return commit_ids
93 return commit_ids
94
94
95 def _rebuild_cache(self, commit_ids):
95 def _rebuild_cache(self, commit_ids):
96 self._commit_ids = dict((commit_id, index)
96 self._commit_ids = dict((commit_id, index)
97 for index, commit_id in enumerate(commit_ids))
97 for index, commit_id in enumerate(commit_ids))
98
98
99 @LazyProperty
99 @LazyProperty
100 def branches(self):
100 def branches(self):
101 return self._get_branches()
101 return self._get_branches()
102
102
103 @LazyProperty
103 @LazyProperty
104 def branches_closed(self):
104 def branches_closed(self):
105 return self._get_branches(active=False, closed=True)
105 return self._get_branches(active=False, closed=True)
106
106
107 @LazyProperty
107 @LazyProperty
108 def branches_all(self):
108 def branches_all(self):
109 all_branches = {}
109 all_branches = {}
110 all_branches.update(self.branches)
110 all_branches.update(self.branches)
111 all_branches.update(self.branches_closed)
111 all_branches.update(self.branches_closed)
112 return all_branches
112 return all_branches
113
113
114 def _get_branches(self, active=True, closed=False):
114 def _get_branches(self, active=True, closed=False):
115 """
115 """
116 Gets branches for this repository
116 Gets branches for this repository
117 Returns only not closed active branches by default
117 Returns only not closed active branches by default
118
118
119 :param active: return also active branches
119 :param active: return also active branches
120 :param closed: return also closed branches
120 :param closed: return also closed branches
121
121
122 """
122 """
123 if self.is_empty():
123 if self.is_empty():
124 return {}
124 return {}
125
125
126 def get_name(ctx):
126 def get_name(ctx):
127 return ctx[0]
127 return ctx[0]
128
128
129 _branches = [(safe_unicode(n), hexlify(h),) for n, h in
129 _branches = [(safe_unicode(n), hexlify(h),) for n, h in
130 self._remote.branches(active, closed).items()]
130 self._remote.branches(active, closed).items()]
131
131
132 return OrderedDict(sorted(_branches, key=get_name, reverse=False))
132 return OrderedDict(sorted(_branches, key=get_name, reverse=False))
133
133
134 @LazyProperty
134 @LazyProperty
135 def tags(self):
135 def tags(self):
136 """
136 """
137 Gets tags for this repository
137 Gets tags for this repository
138 """
138 """
139 return self._get_tags()
139 return self._get_tags()
140
140
141 def _get_tags(self):
141 def _get_tags(self):
142 if self.is_empty():
142 if self.is_empty():
143 return {}
143 return {}
144
144
145 def get_name(ctx):
145 def get_name(ctx):
146 return ctx[0]
146 return ctx[0]
147
147
148 _tags = [(safe_unicode(n), hexlify(h),) for n, h in
148 _tags = [(safe_unicode(n), hexlify(h),) for n, h in
149 self._remote.tags().items()]
149 self._remote.tags().items()]
150
150
151 return OrderedDict(sorted(_tags, key=get_name, reverse=True))
151 return OrderedDict(sorted(_tags, key=get_name, reverse=True))
152
152
153 def tag(self, name, user, commit_id=None, message=None, date=None,
153 def tag(self, name, user, commit_id=None, message=None, date=None,
154 **kwargs):
154 **kwargs):
155 """
155 """
156 Creates and returns a tag for the given ``commit_id``.
156 Creates and returns a tag for the given ``commit_id``.
157
157
158 :param name: name for new tag
158 :param name: name for new tag
159 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
159 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
160 :param commit_id: commit id for which new tag would be created
160 :param commit_id: commit id for which new tag would be created
161 :param message: message of the tag's commit
161 :param message: message of the tag's commit
162 :param date: date of tag's commit
162 :param date: date of tag's commit
163
163
164 :raises TagAlreadyExistError: if tag with same name already exists
164 :raises TagAlreadyExistError: if tag with same name already exists
165 """
165 """
166 if name in self.tags:
166 if name in self.tags:
167 raise TagAlreadyExistError("Tag %s already exists" % name)
167 raise TagAlreadyExistError("Tag %s already exists" % name)
168 commit = self.get_commit(commit_id=commit_id)
168 commit = self.get_commit(commit_id=commit_id)
169 local = kwargs.setdefault('local', False)
169 local = kwargs.setdefault('local', False)
170
170
171 if message is None:
171 if message is None:
172 message = "Added tag %s for commit %s" % (name, commit.short_id)
172 message = "Added tag %s for commit %s" % (name, commit.short_id)
173
173
174 date, tz = date_to_timestamp_plus_offset(date)
174 date, tz = date_to_timestamp_plus_offset(date)
175
175
176 self._remote.tag(
176 self._remote.tag(
177 name, commit.raw_id, message, local, user, date, tz)
177 name, commit.raw_id, message, local, user, date, tz)
178 self._remote.invalidate_vcs_cache()
178 self._remote.invalidate_vcs_cache()
179
179
180 # Reinitialize tags
180 # Reinitialize tags
181 self.tags = self._get_tags()
181 self.tags = self._get_tags()
182 tag_id = self.tags[name]
182 tag_id = self.tags[name]
183
183
184 return self.get_commit(commit_id=tag_id)
184 return self.get_commit(commit_id=tag_id)
185
185
186 def remove_tag(self, name, user, message=None, date=None):
186 def remove_tag(self, name, user, message=None, date=None):
187 """
187 """
188 Removes tag with the given `name`.
188 Removes tag with the given `name`.
189
189
190 :param name: name of the tag to be removed
190 :param name: name of the tag to be removed
191 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
191 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
192 :param message: message of the tag's removal commit
192 :param message: message of the tag's removal commit
193 :param date: date of tag's removal commit
193 :param date: date of tag's removal commit
194
194
195 :raises TagDoesNotExistError: if tag with given name does not exists
195 :raises TagDoesNotExistError: if tag with given name does not exists
196 """
196 """
197 if name not in self.tags:
197 if name not in self.tags:
198 raise TagDoesNotExistError("Tag %s does not exist" % name)
198 raise TagDoesNotExistError("Tag %s does not exist" % name)
199 if message is None:
199 if message is None:
200 message = "Removed tag %s" % name
200 message = "Removed tag %s" % name
201 local = False
201 local = False
202
202
203 date, tz = date_to_timestamp_plus_offset(date)
203 date, tz = date_to_timestamp_plus_offset(date)
204
204
205 self._remote.tag(name, nullid, message, local, user, date, tz)
205 self._remote.tag(name, nullid, message, local, user, date, tz)
206 self._remote.invalidate_vcs_cache()
206 self._remote.invalidate_vcs_cache()
207 self.tags = self._get_tags()
207 self.tags = self._get_tags()
208
208
209 @LazyProperty
209 @LazyProperty
210 def bookmarks(self):
210 def bookmarks(self):
211 """
211 """
212 Gets bookmarks for this repository
212 Gets bookmarks for this repository
213 """
213 """
214 return self._get_bookmarks()
214 return self._get_bookmarks()
215
215
216 def _get_bookmarks(self):
216 def _get_bookmarks(self):
217 if self.is_empty():
217 if self.is_empty():
218 return {}
218 return {}
219
219
220 def get_name(ctx):
220 def get_name(ctx):
221 return ctx[0]
221 return ctx[0]
222
222
223 _bookmarks = [
223 _bookmarks = [
224 (safe_unicode(n), hexlify(h)) for n, h in
224 (safe_unicode(n), hexlify(h)) for n, h in
225 self._remote.bookmarks().items()]
225 self._remote.bookmarks().items()]
226
226
227 return OrderedDict(sorted(_bookmarks, key=get_name))
227 return OrderedDict(sorted(_bookmarks, key=get_name))
228
228
229 def _get_all_commit_ids(self):
229 def _get_all_commit_ids(self):
230 return self._remote.get_all_commit_ids('visible')
230 return self._remote.get_all_commit_ids('visible')
231
231
232 def get_diff(
232 def get_diff(
233 self, commit1, commit2, path='', ignore_whitespace=False,
233 self, commit1, commit2, path='', ignore_whitespace=False,
234 context=3, path1=None):
234 context=3, path1=None):
235 """
235 """
236 Returns (git like) *diff*, as plain text. Shows changes introduced by
236 Returns (git like) *diff*, as plain text. Shows changes introduced by
237 `commit2` since `commit1`.
237 `commit2` since `commit1`.
238
238
239 :param commit1: Entry point from which diff is shown. Can be
239 :param commit1: Entry point from which diff is shown. Can be
240 ``self.EMPTY_COMMIT`` - in this case, patch showing all
240 ``self.EMPTY_COMMIT`` - in this case, patch showing all
241 the changes since empty state of the repository until `commit2`
241 the changes since empty state of the repository until `commit2`
242 :param commit2: Until which commit changes should be shown.
242 :param commit2: Until which commit changes should be shown.
243 :param ignore_whitespace: If set to ``True``, would not show whitespace
243 :param ignore_whitespace: If set to ``True``, would not show whitespace
244 changes. Defaults to ``False``.
244 changes. Defaults to ``False``.
245 :param context: How many lines before/after changed lines should be
245 :param context: How many lines before/after changed lines should be
246 shown. Defaults to ``3``.
246 shown. Defaults to ``3``.
247 """
247 """
248 self._validate_diff_commits(commit1, commit2)
248 self._validate_diff_commits(commit1, commit2)
249 if path1 is not None and path1 != path:
249 if path1 is not None and path1 != path:
250 raise ValueError("Diff of two different paths not supported.")
250 raise ValueError("Diff of two different paths not supported.")
251
251
252 if path:
252 if path:
253 file_filter = [self.path, path]
253 file_filter = [self.path, path]
254 else:
254 else:
255 file_filter = None
255 file_filter = None
256
256
257 diff = self._remote.diff(
257 diff = self._remote.diff(
258 commit1.raw_id, commit2.raw_id, file_filter=file_filter,
258 commit1.raw_id, commit2.raw_id, file_filter=file_filter,
259 opt_git=True, opt_ignorews=ignore_whitespace,
259 opt_git=True, opt_ignorews=ignore_whitespace,
260 context=context)
260 context=context)
261 return MercurialDiff(diff)
261 return MercurialDiff(diff)
262
262
263 def strip(self, commit_id, branch=None):
263 def strip(self, commit_id, branch=None):
264 self._remote.strip(commit_id, update=False, backup="none")
264 self._remote.strip(commit_id, update=False, backup="none")
265
265
266 self._remote.invalidate_vcs_cache()
266 self._remote.invalidate_vcs_cache()
267 self.commit_ids = self._get_all_commit_ids()
267 self.commit_ids = self._get_all_commit_ids()
268 self._rebuild_cache(self.commit_ids)
268 self._rebuild_cache(self.commit_ids)
269
269
270 def verify(self):
271 verify = self._remote.verify()
272
273 self._remote.invalidate_vcs_cache()
274 return verify
275
270 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
276 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
271 if commit_id1 == commit_id2:
277 if commit_id1 == commit_id2:
272 return commit_id1
278 return commit_id1
273
279
274 ancestors = self._remote.revs_from_revspec(
280 ancestors = self._remote.revs_from_revspec(
275 "ancestor(id(%s), id(%s))", commit_id1, commit_id2,
281 "ancestor(id(%s), id(%s))", commit_id1, commit_id2,
276 other_path=repo2.path)
282 other_path=repo2.path)
277 return repo2[ancestors[0]].raw_id if ancestors else None
283 return repo2[ancestors[0]].raw_id if ancestors else None
278
284
279 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
285 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
280 if commit_id1 == commit_id2:
286 if commit_id1 == commit_id2:
281 commits = []
287 commits = []
282 else:
288 else:
283 if merge:
289 if merge:
284 indexes = self._remote.revs_from_revspec(
290 indexes = self._remote.revs_from_revspec(
285 "ancestors(id(%s)) - ancestors(id(%s)) - id(%s)",
291 "ancestors(id(%s)) - ancestors(id(%s)) - id(%s)",
286 commit_id2, commit_id1, commit_id1, other_path=repo2.path)
292 commit_id2, commit_id1, commit_id1, other_path=repo2.path)
287 else:
293 else:
288 indexes = self._remote.revs_from_revspec(
294 indexes = self._remote.revs_from_revspec(
289 "id(%s)..id(%s) - id(%s)", commit_id1, commit_id2,
295 "id(%s)..id(%s) - id(%s)", commit_id1, commit_id2,
290 commit_id1, other_path=repo2.path)
296 commit_id1, other_path=repo2.path)
291
297
292 commits = [repo2.get_commit(commit_idx=idx, pre_load=pre_load)
298 commits = [repo2.get_commit(commit_idx=idx, pre_load=pre_load)
293 for idx in indexes]
299 for idx in indexes]
294
300
295 return commits
301 return commits
296
302
297 @staticmethod
303 @staticmethod
298 def check_url(url, config):
304 def check_url(url, config):
299 """
305 """
300 Function will check given url and try to verify if it's a valid
306 Function will check given url and try to verify if it's a valid
301 link. Sometimes it may happened that mercurial will issue basic
307 link. Sometimes it may happened that mercurial will issue basic
302 auth request that can cause whole API to hang when used from python
308 auth request that can cause whole API to hang when used from python
303 or other external calls.
309 or other external calls.
304
310
305 On failures it'll raise urllib2.HTTPError, exception is also thrown
311 On failures it'll raise urllib2.HTTPError, exception is also thrown
306 when the return code is non 200
312 when the return code is non 200
307 """
313 """
308 # check first if it's not an local url
314 # check first if it's not an local url
309 if os.path.isdir(url) or url.startswith('file:'):
315 if os.path.isdir(url) or url.startswith('file:'):
310 return True
316 return True
311
317
312 # Request the _remote to verify the url
318 # Request the _remote to verify the url
313 return connection.Hg.check_url(url, config.serialize())
319 return connection.Hg.check_url(url, config.serialize())
314
320
315 @staticmethod
321 @staticmethod
316 def is_valid_repository(path):
322 def is_valid_repository(path):
317 return os.path.isdir(os.path.join(path, '.hg'))
323 return os.path.isdir(os.path.join(path, '.hg'))
318
324
319 def _init_repo(self, create, src_url=None, update_after_clone=False):
325 def _init_repo(self, create, src_url=None, update_after_clone=False):
320 """
326 """
321 Function will check for mercurial repository in given path. If there
327 Function will check for mercurial repository in given path. If there
322 is no repository in that path it will raise an exception unless
328 is no repository in that path it will raise an exception unless
323 `create` parameter is set to True - in that case repository would
329 `create` parameter is set to True - in that case repository would
324 be created.
330 be created.
325
331
326 If `src_url` is given, would try to clone repository from the
332 If `src_url` is given, would try to clone repository from the
327 location at given clone_point. Additionally it'll make update to
333 location at given clone_point. Additionally it'll make update to
328 working copy accordingly to `update_after_clone` flag.
334 working copy accordingly to `update_after_clone` flag.
329 """
335 """
330 if create and os.path.exists(self.path):
336 if create and os.path.exists(self.path):
331 raise RepositoryError(
337 raise RepositoryError(
332 "Cannot create repository at %s, location already exist"
338 "Cannot create repository at %s, location already exist"
333 % self.path)
339 % self.path)
334
340
335 if src_url:
341 if src_url:
336 url = str(self._get_url(src_url))
342 url = str(self._get_url(src_url))
337 MercurialRepository.check_url(url, self.config)
343 MercurialRepository.check_url(url, self.config)
338
344
339 self._remote.clone(url, self.path, update_after_clone)
345 self._remote.clone(url, self.path, update_after_clone)
340
346
341 # Don't try to create if we've already cloned repo
347 # Don't try to create if we've already cloned repo
342 create = False
348 create = False
343
349
344 if create:
350 if create:
345 os.makedirs(self.path, mode=0755)
351 os.makedirs(self.path, mode=0755)
346
352
347 self._remote.localrepository(create)
353 self._remote.localrepository(create)
348
354
349 @LazyProperty
355 @LazyProperty
350 def in_memory_commit(self):
356 def in_memory_commit(self):
351 return MercurialInMemoryCommit(self)
357 return MercurialInMemoryCommit(self)
352
358
353 @LazyProperty
359 @LazyProperty
354 def description(self):
360 def description(self):
355 description = self._remote.get_config_value(
361 description = self._remote.get_config_value(
356 'web', 'description', untrusted=True)
362 'web', 'description', untrusted=True)
357 return safe_unicode(description or self.DEFAULT_DESCRIPTION)
363 return safe_unicode(description or self.DEFAULT_DESCRIPTION)
358
364
359 @LazyProperty
365 @LazyProperty
360 def contact(self):
366 def contact(self):
361 contact = (
367 contact = (
362 self._remote.get_config_value("web", "contact") or
368 self._remote.get_config_value("web", "contact") or
363 self._remote.get_config_value("ui", "username"))
369 self._remote.get_config_value("ui", "username"))
364 return safe_unicode(contact or self.DEFAULT_CONTACT)
370 return safe_unicode(contact or self.DEFAULT_CONTACT)
365
371
366 @LazyProperty
372 @LazyProperty
367 def last_change(self):
373 def last_change(self):
368 """
374 """
369 Returns last change made on this repository as
375 Returns last change made on this repository as
370 `datetime.datetime` object.
376 `datetime.datetime` object.
371 """
377 """
372 try:
378 try:
373 return self.get_commit().date
379 return self.get_commit().date
374 except RepositoryError:
380 except RepositoryError:
375 tzoffset = makedate()[1]
381 tzoffset = makedate()[1]
376 return utcdate_fromtimestamp(self._get_fs_mtime(), tzoffset)
382 return utcdate_fromtimestamp(self._get_fs_mtime(), tzoffset)
377
383
378 def _get_fs_mtime(self):
384 def _get_fs_mtime(self):
379 # fallback to filesystem
385 # fallback to filesystem
380 cl_path = os.path.join(self.path, '.hg', "00changelog.i")
386 cl_path = os.path.join(self.path, '.hg', "00changelog.i")
381 st_path = os.path.join(self.path, '.hg', "store")
387 st_path = os.path.join(self.path, '.hg', "store")
382 if os.path.exists(cl_path):
388 if os.path.exists(cl_path):
383 return os.stat(cl_path).st_mtime
389 return os.stat(cl_path).st_mtime
384 else:
390 else:
385 return os.stat(st_path).st_mtime
391 return os.stat(st_path).st_mtime
386
392
387 def _sanitize_commit_idx(self, idx):
393 def _sanitize_commit_idx(self, idx):
388 # Note: Mercurial has ``int(-1)`` reserved as not existing id_or_idx
394 # Note: Mercurial has ``int(-1)`` reserved as not existing id_or_idx
389 # number. A `long` is treated in the correct way though. So we convert
395 # number. A `long` is treated in the correct way though. So we convert
390 # `int` to `long` here to make sure it is handled correctly.
396 # `int` to `long` here to make sure it is handled correctly.
391 if isinstance(idx, int):
397 if isinstance(idx, int):
392 return long(idx)
398 return long(idx)
393 return idx
399 return idx
394
400
395 def _get_url(self, url):
401 def _get_url(self, url):
396 """
402 """
397 Returns normalized url. If schema is not given, would fall
403 Returns normalized url. If schema is not given, would fall
398 to filesystem
404 to filesystem
399 (``file:///``) schema.
405 (``file:///``) schema.
400 """
406 """
401 url = url.encode('utf8')
407 url = url.encode('utf8')
402 if url != 'default' and '://' not in url:
408 if url != 'default' and '://' not in url:
403 url = "file:" + urllib.pathname2url(url)
409 url = "file:" + urllib.pathname2url(url)
404 return url
410 return url
405
411
406 def get_hook_location(self):
412 def get_hook_location(self):
407 """
413 """
408 returns absolute path to location where hooks are stored
414 returns absolute path to location where hooks are stored
409 """
415 """
410 return os.path.join(self.path, '.hg', '.hgrc')
416 return os.path.join(self.path, '.hg', '.hgrc')
411
417
412 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
418 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
413 """
419 """
414 Returns ``MercurialCommit`` object representing repository's
420 Returns ``MercurialCommit`` object representing repository's
415 commit at the given `commit_id` or `commit_idx`.
421 commit at the given `commit_id` or `commit_idx`.
416 """
422 """
417 if self.is_empty():
423 if self.is_empty():
418 raise EmptyRepositoryError("There are no commits yet")
424 raise EmptyRepositoryError("There are no commits yet")
419
425
420 if commit_id is not None:
426 if commit_id is not None:
421 self._validate_commit_id(commit_id)
427 self._validate_commit_id(commit_id)
422 try:
428 try:
423 idx = self._commit_ids[commit_id]
429 idx = self._commit_ids[commit_id]
424 return MercurialCommit(self, commit_id, idx, pre_load=pre_load)
430 return MercurialCommit(self, commit_id, idx, pre_load=pre_load)
425 except KeyError:
431 except KeyError:
426 pass
432 pass
427 elif commit_idx is not None:
433 elif commit_idx is not None:
428 self._validate_commit_idx(commit_idx)
434 self._validate_commit_idx(commit_idx)
429 commit_idx = self._sanitize_commit_idx(commit_idx)
435 commit_idx = self._sanitize_commit_idx(commit_idx)
430 try:
436 try:
431 id_ = self.commit_ids[commit_idx]
437 id_ = self.commit_ids[commit_idx]
432 if commit_idx < 0:
438 if commit_idx < 0:
433 commit_idx += len(self.commit_ids)
439 commit_idx += len(self.commit_ids)
434 return MercurialCommit(
440 return MercurialCommit(
435 self, id_, commit_idx, pre_load=pre_load)
441 self, id_, commit_idx, pre_load=pre_load)
436 except IndexError:
442 except IndexError:
437 commit_id = commit_idx
443 commit_id = commit_idx
438 else:
444 else:
439 commit_id = "tip"
445 commit_id = "tip"
440
446
441 # TODO Paris: Ugly hack to "serialize" long for msgpack
447 # TODO Paris: Ugly hack to "serialize" long for msgpack
442 if isinstance(commit_id, long):
448 if isinstance(commit_id, long):
443 commit_id = float(commit_id)
449 commit_id = float(commit_id)
444
450
445 if isinstance(commit_id, unicode):
451 if isinstance(commit_id, unicode):
446 commit_id = safe_str(commit_id)
452 commit_id = safe_str(commit_id)
447
453
448 try:
454 try:
449 raw_id, idx = self._remote.lookup(commit_id, both=True)
455 raw_id, idx = self._remote.lookup(commit_id, both=True)
450 except CommitDoesNotExistError:
456 except CommitDoesNotExistError:
451 msg = "Commit %s does not exist for %s" % (
457 msg = "Commit %s does not exist for %s" % (
452 commit_id, self)
458 commit_id, self)
453 raise CommitDoesNotExistError(msg)
459 raise CommitDoesNotExistError(msg)
454
460
455 return MercurialCommit(self, raw_id, idx, pre_load=pre_load)
461 return MercurialCommit(self, raw_id, idx, pre_load=pre_load)
456
462
457 def get_commits(
463 def get_commits(
458 self, start_id=None, end_id=None, start_date=None, end_date=None,
464 self, start_id=None, end_id=None, start_date=None, end_date=None,
459 branch_name=None, pre_load=None):
465 branch_name=None, pre_load=None):
460 """
466 """
461 Returns generator of ``MercurialCommit`` objects from start to end
467 Returns generator of ``MercurialCommit`` objects from start to end
462 (both are inclusive)
468 (both are inclusive)
463
469
464 :param start_id: None, str(commit_id)
470 :param start_id: None, str(commit_id)
465 :param end_id: None, str(commit_id)
471 :param end_id: None, str(commit_id)
466 :param start_date: if specified, commits with commit date less than
472 :param start_date: if specified, commits with commit date less than
467 ``start_date`` would be filtered out from returned set
473 ``start_date`` would be filtered out from returned set
468 :param end_date: if specified, commits with commit date greater than
474 :param end_date: if specified, commits with commit date greater than
469 ``end_date`` would be filtered out from returned set
475 ``end_date`` would be filtered out from returned set
470 :param branch_name: if specified, commits not reachable from given
476 :param branch_name: if specified, commits not reachable from given
471 branch would be filtered out from returned set
477 branch would be filtered out from returned set
472
478
473 :raise BranchDoesNotExistError: If given ``branch_name`` does not
479 :raise BranchDoesNotExistError: If given ``branch_name`` does not
474 exist.
480 exist.
475 :raise CommitDoesNotExistError: If commit for given ``start`` or
481 :raise CommitDoesNotExistError: If commit for given ``start`` or
476 ``end`` could not be found.
482 ``end`` could not be found.
477 """
483 """
478 # actually we should check now if it's not an empty repo
484 # actually we should check now if it's not an empty repo
479 branch_ancestors = False
485 branch_ancestors = False
480 if self.is_empty():
486 if self.is_empty():
481 raise EmptyRepositoryError("There are no commits yet")
487 raise EmptyRepositoryError("There are no commits yet")
482 self._validate_branch_name(branch_name)
488 self._validate_branch_name(branch_name)
483
489
484 if start_id is not None:
490 if start_id is not None:
485 self._validate_commit_id(start_id)
491 self._validate_commit_id(start_id)
486 c_start = self.get_commit(commit_id=start_id)
492 c_start = self.get_commit(commit_id=start_id)
487 start_pos = self._commit_ids[c_start.raw_id]
493 start_pos = self._commit_ids[c_start.raw_id]
488 else:
494 else:
489 start_pos = None
495 start_pos = None
490
496
491 if end_id is not None:
497 if end_id is not None:
492 self._validate_commit_id(end_id)
498 self._validate_commit_id(end_id)
493 c_end = self.get_commit(commit_id=end_id)
499 c_end = self.get_commit(commit_id=end_id)
494 end_pos = max(0, self._commit_ids[c_end.raw_id])
500 end_pos = max(0, self._commit_ids[c_end.raw_id])
495 else:
501 else:
496 end_pos = None
502 end_pos = None
497
503
498 if None not in [start_id, end_id] and start_pos > end_pos:
504 if None not in [start_id, end_id] and start_pos > end_pos:
499 raise RepositoryError(
505 raise RepositoryError(
500 "Start commit '%s' cannot be after end commit '%s'" %
506 "Start commit '%s' cannot be after end commit '%s'" %
501 (start_id, end_id))
507 (start_id, end_id))
502
508
503 if end_pos is not None:
509 if end_pos is not None:
504 end_pos += 1
510 end_pos += 1
505
511
506 commit_filter = []
512 commit_filter = []
507 if branch_name and not branch_ancestors:
513 if branch_name and not branch_ancestors:
508 commit_filter.append('branch("%s")' % branch_name)
514 commit_filter.append('branch("%s")' % branch_name)
509 elif branch_name and branch_ancestors:
515 elif branch_name and branch_ancestors:
510 commit_filter.append('ancestors(branch("%s"))' % branch_name)
516 commit_filter.append('ancestors(branch("%s"))' % branch_name)
511 if start_date and not end_date:
517 if start_date and not end_date:
512 commit_filter.append('date(">%s")' % start_date)
518 commit_filter.append('date(">%s")' % start_date)
513 if end_date and not start_date:
519 if end_date and not start_date:
514 commit_filter.append('date("<%s")' % end_date)
520 commit_filter.append('date("<%s")' % end_date)
515 if start_date and end_date:
521 if start_date and end_date:
516 commit_filter.append(
522 commit_filter.append(
517 'date(">%s") and date("<%s")' % (start_date, end_date))
523 'date(">%s") and date("<%s")' % (start_date, end_date))
518
524
519 # TODO: johbo: Figure out a simpler way for this solution
525 # TODO: johbo: Figure out a simpler way for this solution
520 collection_generator = CollectionGenerator
526 collection_generator = CollectionGenerator
521 if commit_filter:
527 if commit_filter:
522 commit_filter = map(safe_str, commit_filter)
528 commit_filter = map(safe_str, commit_filter)
523 revisions = self._remote.rev_range(commit_filter)
529 revisions = self._remote.rev_range(commit_filter)
524 collection_generator = MercurialIndexBasedCollectionGenerator
530 collection_generator = MercurialIndexBasedCollectionGenerator
525 else:
531 else:
526 revisions = self.commit_ids
532 revisions = self.commit_ids
527
533
528 if start_pos or end_pos:
534 if start_pos or end_pos:
529 revisions = revisions[start_pos:end_pos]
535 revisions = revisions[start_pos:end_pos]
530
536
531 return collection_generator(self, revisions, pre_load=pre_load)
537 return collection_generator(self, revisions, pre_load=pre_load)
532
538
533 def pull(self, url, commit_ids=None):
539 def pull(self, url, commit_ids=None):
534 """
540 """
535 Tries to pull changes from external location.
541 Tries to pull changes from external location.
536
542
537 :param commit_ids: Optional. Can be set to a list of commit ids
543 :param commit_ids: Optional. Can be set to a list of commit ids
538 which shall be pulled from the other repository.
544 which shall be pulled from the other repository.
539 """
545 """
540 url = self._get_url(url)
546 url = self._get_url(url)
541 self._remote.pull(url, commit_ids=commit_ids)
547 self._remote.pull(url, commit_ids=commit_ids)
542 self._remote.invalidate_vcs_cache()
548 self._remote.invalidate_vcs_cache()
543
549
544 def _local_clone(self, clone_path):
550 def _local_clone(self, clone_path):
545 """
551 """
546 Create a local clone of the current repo.
552 Create a local clone of the current repo.
547 """
553 """
548 self._remote.clone(self.path, clone_path, update_after_clone=True,
554 self._remote.clone(self.path, clone_path, update_after_clone=True,
549 hooks=False)
555 hooks=False)
550
556
551 def _update(self, revision, clean=False):
557 def _update(self, revision, clean=False):
552 """
558 """
553 Update the working copty to the specified revision.
559 Update the working copty to the specified revision.
554 """
560 """
555 self._remote.update(revision, clean=clean)
561 self._remote.update(revision, clean=clean)
556
562
557 def _identify(self):
563 def _identify(self):
558 """
564 """
559 Return the current state of the working directory.
565 Return the current state of the working directory.
560 """
566 """
561 return self._remote.identify().strip().rstrip('+')
567 return self._remote.identify().strip().rstrip('+')
562
568
563 def _heads(self, branch=None):
569 def _heads(self, branch=None):
564 """
570 """
565 Return the commit ids of the repository heads.
571 Return the commit ids of the repository heads.
566 """
572 """
567 return self._remote.heads(branch=branch).strip().split(' ')
573 return self._remote.heads(branch=branch).strip().split(' ')
568
574
569 def _ancestor(self, revision1, revision2):
575 def _ancestor(self, revision1, revision2):
570 """
576 """
571 Return the common ancestor of the two revisions.
577 Return the common ancestor of the two revisions.
572 """
578 """
573 return self._remote.ancestor(revision1, revision2)
579 return self._remote.ancestor(revision1, revision2)
574
580
575 def _local_push(
581 def _local_push(
576 self, revision, repository_path, push_branches=False,
582 self, revision, repository_path, push_branches=False,
577 enable_hooks=False):
583 enable_hooks=False):
578 """
584 """
579 Push the given revision to the specified repository.
585 Push the given revision to the specified repository.
580
586
581 :param push_branches: allow to create branches in the target repo.
587 :param push_branches: allow to create branches in the target repo.
582 """
588 """
583 self._remote.push(
589 self._remote.push(
584 [revision], repository_path, hooks=enable_hooks,
590 [revision], repository_path, hooks=enable_hooks,
585 push_branches=push_branches)
591 push_branches=push_branches)
586
592
587 def _local_merge(self, target_ref, merge_message, user_name, user_email,
593 def _local_merge(self, target_ref, merge_message, user_name, user_email,
588 source_ref, use_rebase=False):
594 source_ref, use_rebase=False):
589 """
595 """
590 Merge the given source_revision into the checked out revision.
596 Merge the given source_revision into the checked out revision.
591
597
592 Returns the commit id of the merge and a boolean indicating if the
598 Returns the commit id of the merge and a boolean indicating if the
593 commit needs to be pushed.
599 commit needs to be pushed.
594 """
600 """
595 self._update(target_ref.commit_id)
601 self._update(target_ref.commit_id)
596
602
597 ancestor = self._ancestor(target_ref.commit_id, source_ref.commit_id)
603 ancestor = self._ancestor(target_ref.commit_id, source_ref.commit_id)
598 is_the_same_branch = self._is_the_same_branch(target_ref, source_ref)
604 is_the_same_branch = self._is_the_same_branch(target_ref, source_ref)
599
605
600 if ancestor == source_ref.commit_id:
606 if ancestor == source_ref.commit_id:
601 # Nothing to do, the changes were already integrated
607 # Nothing to do, the changes were already integrated
602 return target_ref.commit_id, False
608 return target_ref.commit_id, False
603
609
604 elif ancestor == target_ref.commit_id and is_the_same_branch:
610 elif ancestor == target_ref.commit_id and is_the_same_branch:
605 # In this case we should force a commit message
611 # In this case we should force a commit message
606 return source_ref.commit_id, True
612 return source_ref.commit_id, True
607
613
608 if use_rebase:
614 if use_rebase:
609 try:
615 try:
610 bookmark_name = 'rcbook%s%s' % (source_ref.commit_id,
616 bookmark_name = 'rcbook%s%s' % (source_ref.commit_id,
611 target_ref.commit_id)
617 target_ref.commit_id)
612 self.bookmark(bookmark_name, revision=source_ref.commit_id)
618 self.bookmark(bookmark_name, revision=source_ref.commit_id)
613 self._remote.rebase(
619 self._remote.rebase(
614 source=source_ref.commit_id, dest=target_ref.commit_id)
620 source=source_ref.commit_id, dest=target_ref.commit_id)
615 self._remote.invalidate_vcs_cache()
621 self._remote.invalidate_vcs_cache()
616 self._update(bookmark_name)
622 self._update(bookmark_name)
617 return self._identify(), True
623 return self._identify(), True
618 except RepositoryError:
624 except RepositoryError:
619 # The rebase-abort may raise another exception which 'hides'
625 # The rebase-abort may raise another exception which 'hides'
620 # the original one, therefore we log it here.
626 # the original one, therefore we log it here.
621 log.exception('Error while rebasing shadow repo during merge.')
627 log.exception('Error while rebasing shadow repo during merge.')
622
628
623 # Cleanup any rebase leftovers
629 # Cleanup any rebase leftovers
624 self._remote.invalidate_vcs_cache()
630 self._remote.invalidate_vcs_cache()
625 self._remote.rebase(abort=True)
631 self._remote.rebase(abort=True)
626 self._remote.invalidate_vcs_cache()
632 self._remote.invalidate_vcs_cache()
627 self._remote.update(clean=True)
633 self._remote.update(clean=True)
628 raise
634 raise
629 else:
635 else:
630 try:
636 try:
631 self._remote.merge(source_ref.commit_id)
637 self._remote.merge(source_ref.commit_id)
632 self._remote.invalidate_vcs_cache()
638 self._remote.invalidate_vcs_cache()
633 self._remote.commit(
639 self._remote.commit(
634 message=safe_str(merge_message),
640 message=safe_str(merge_message),
635 username=safe_str('%s <%s>' % (user_name, user_email)))
641 username=safe_str('%s <%s>' % (user_name, user_email)))
636 self._remote.invalidate_vcs_cache()
642 self._remote.invalidate_vcs_cache()
637 return self._identify(), True
643 return self._identify(), True
638 except RepositoryError:
644 except RepositoryError:
639 # Cleanup any merge leftovers
645 # Cleanup any merge leftovers
640 self._remote.update(clean=True)
646 self._remote.update(clean=True)
641 raise
647 raise
642
648
643 def _is_the_same_branch(self, target_ref, source_ref):
649 def _is_the_same_branch(self, target_ref, source_ref):
644 return (
650 return (
645 self._get_branch_name(target_ref) ==
651 self._get_branch_name(target_ref) ==
646 self._get_branch_name(source_ref))
652 self._get_branch_name(source_ref))
647
653
648 def _get_branch_name(self, ref):
654 def _get_branch_name(self, ref):
649 if ref.type == 'branch':
655 if ref.type == 'branch':
650 return ref.name
656 return ref.name
651 return self._remote.ctx_branch(ref.commit_id)
657 return self._remote.ctx_branch(ref.commit_id)
652
658
653 def _get_shadow_repository_path(self, workspace_id):
659 def _get_shadow_repository_path(self, workspace_id):
654 # The name of the shadow repository must start with '.', so it is
660 # The name of the shadow repository must start with '.', so it is
655 # skipped by 'rhodecode.lib.utils.get_filesystem_repos'.
661 # skipped by 'rhodecode.lib.utils.get_filesystem_repos'.
656 return os.path.join(
662 return os.path.join(
657 os.path.dirname(self.path),
663 os.path.dirname(self.path),
658 '.__shadow_%s_%s' % (os.path.basename(self.path), workspace_id))
664 '.__shadow_%s_%s' % (os.path.basename(self.path), workspace_id))
659
665
660 def _maybe_prepare_merge_workspace(self, workspace_id, unused_target_ref):
666 def _maybe_prepare_merge_workspace(self, workspace_id, unused_target_ref):
661 shadow_repository_path = self._get_shadow_repository_path(workspace_id)
667 shadow_repository_path = self._get_shadow_repository_path(workspace_id)
662 if not os.path.exists(shadow_repository_path):
668 if not os.path.exists(shadow_repository_path):
663 self._local_clone(shadow_repository_path)
669 self._local_clone(shadow_repository_path)
664 log.debug(
670 log.debug(
665 'Prepared shadow repository in %s', shadow_repository_path)
671 'Prepared shadow repository in %s', shadow_repository_path)
666
672
667 return shadow_repository_path
673 return shadow_repository_path
668
674
669 def cleanup_merge_workspace(self, workspace_id):
675 def cleanup_merge_workspace(self, workspace_id):
670 shadow_repository_path = self._get_shadow_repository_path(workspace_id)
676 shadow_repository_path = self._get_shadow_repository_path(workspace_id)
671 shutil.rmtree(shadow_repository_path, ignore_errors=True)
677 shutil.rmtree(shadow_repository_path, ignore_errors=True)
672
678
673 def _merge_repo(self, shadow_repository_path, target_ref,
679 def _merge_repo(self, shadow_repository_path, target_ref,
674 source_repo, source_ref, merge_message,
680 source_repo, source_ref, merge_message,
675 merger_name, merger_email, dry_run=False,
681 merger_name, merger_email, dry_run=False,
676 use_rebase=False):
682 use_rebase=False):
677 if target_ref.commit_id not in self._heads():
683 if target_ref.commit_id not in self._heads():
678 return MergeResponse(
684 return MergeResponse(
679 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD)
685 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD)
680
686
681 try:
687 try:
682 if (target_ref.type == 'branch' and
688 if (target_ref.type == 'branch' and
683 len(self._heads(target_ref.name)) != 1):
689 len(self._heads(target_ref.name)) != 1):
684 return MergeResponse(
690 return MergeResponse(
685 False, False, None,
691 False, False, None,
686 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS)
692 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS)
687 except CommitDoesNotExistError as e:
693 except CommitDoesNotExistError as e:
688 log.exception('Failure when looking up branch heads on hg target')
694 log.exception('Failure when looking up branch heads on hg target')
689 return MergeResponse(
695 return MergeResponse(
690 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
696 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
691
697
692 shadow_repo = self._get_shadow_instance(shadow_repository_path)
698 shadow_repo = self._get_shadow_instance(shadow_repository_path)
693
699
694 log.debug('Pulling in target reference %s', target_ref)
700 log.debug('Pulling in target reference %s', target_ref)
695 self._validate_pull_reference(target_ref)
701 self._validate_pull_reference(target_ref)
696 shadow_repo._local_pull(self.path, target_ref)
702 shadow_repo._local_pull(self.path, target_ref)
697 try:
703 try:
698 log.debug('Pulling in source reference %s', source_ref)
704 log.debug('Pulling in source reference %s', source_ref)
699 source_repo._validate_pull_reference(source_ref)
705 source_repo._validate_pull_reference(source_ref)
700 shadow_repo._local_pull(source_repo.path, source_ref)
706 shadow_repo._local_pull(source_repo.path, source_ref)
701 except CommitDoesNotExistError:
707 except CommitDoesNotExistError:
702 log.exception('Failure when doing local pull on hg shadow repo')
708 log.exception('Failure when doing local pull on hg shadow repo')
703 return MergeResponse(
709 return MergeResponse(
704 False, False, None, MergeFailureReason.MISSING_SOURCE_REF)
710 False, False, None, MergeFailureReason.MISSING_SOURCE_REF)
705
711
706 merge_ref = None
712 merge_ref = None
707 merge_failure_reason = MergeFailureReason.NONE
713 merge_failure_reason = MergeFailureReason.NONE
708
714
709 try:
715 try:
710 merge_commit_id, needs_push = shadow_repo._local_merge(
716 merge_commit_id, needs_push = shadow_repo._local_merge(
711 target_ref, merge_message, merger_name, merger_email,
717 target_ref, merge_message, merger_name, merger_email,
712 source_ref, use_rebase=use_rebase)
718 source_ref, use_rebase=use_rebase)
713 merge_possible = True
719 merge_possible = True
714
720
715 # Set a bookmark pointing to the merge commit. This bookmark may be
721 # Set a bookmark pointing to the merge commit. This bookmark may be
716 # used to easily identify the last successful merge commit in the
722 # used to easily identify the last successful merge commit in the
717 # shadow repository.
723 # shadow repository.
718 shadow_repo.bookmark('pr-merge', revision=merge_commit_id)
724 shadow_repo.bookmark('pr-merge', revision=merge_commit_id)
719 merge_ref = Reference('book', 'pr-merge', merge_commit_id)
725 merge_ref = Reference('book', 'pr-merge', merge_commit_id)
720 except SubrepoMergeError:
726 except SubrepoMergeError:
721 log.exception(
727 log.exception(
722 'Subrepo merge error during local merge on hg shadow repo.')
728 'Subrepo merge error during local merge on hg shadow repo.')
723 merge_possible = False
729 merge_possible = False
724 merge_failure_reason = MergeFailureReason.SUBREPO_MERGE_FAILED
730 merge_failure_reason = MergeFailureReason.SUBREPO_MERGE_FAILED
725 except RepositoryError:
731 except RepositoryError:
726 log.exception('Failure when doing local merge on hg shadow repo')
732 log.exception('Failure when doing local merge on hg shadow repo')
727 merge_possible = False
733 merge_possible = False
728 merge_failure_reason = MergeFailureReason.MERGE_FAILED
734 merge_failure_reason = MergeFailureReason.MERGE_FAILED
729
735
730 if merge_possible and not dry_run:
736 if merge_possible and not dry_run:
731 if needs_push:
737 if needs_push:
732 # In case the target is a bookmark, update it, so after pushing
738 # In case the target is a bookmark, update it, so after pushing
733 # the bookmarks is also updated in the target.
739 # the bookmarks is also updated in the target.
734 if target_ref.type == 'book':
740 if target_ref.type == 'book':
735 shadow_repo.bookmark(
741 shadow_repo.bookmark(
736 target_ref.name, revision=merge_commit_id)
742 target_ref.name, revision=merge_commit_id)
737
743
738 try:
744 try:
739 shadow_repo_with_hooks = self._get_shadow_instance(
745 shadow_repo_with_hooks = self._get_shadow_instance(
740 shadow_repository_path,
746 shadow_repository_path,
741 enable_hooks=True)
747 enable_hooks=True)
742 # Note: the push_branches option will push any new branch
748 # Note: the push_branches option will push any new branch
743 # defined in the source repository to the target. This may
749 # defined in the source repository to the target. This may
744 # be dangerous as branches are permanent in Mercurial.
750 # be dangerous as branches are permanent in Mercurial.
745 # This feature was requested in issue #441.
751 # This feature was requested in issue #441.
746 shadow_repo_with_hooks._local_push(
752 shadow_repo_with_hooks._local_push(
747 merge_commit_id, self.path, push_branches=True,
753 merge_commit_id, self.path, push_branches=True,
748 enable_hooks=True)
754 enable_hooks=True)
749 merge_succeeded = True
755 merge_succeeded = True
750 except RepositoryError:
756 except RepositoryError:
751 log.exception(
757 log.exception(
752 'Failure when doing local push from the shadow '
758 'Failure when doing local push from the shadow '
753 'repository to the target repository.')
759 'repository to the target repository.')
754 merge_succeeded = False
760 merge_succeeded = False
755 merge_failure_reason = MergeFailureReason.PUSH_FAILED
761 merge_failure_reason = MergeFailureReason.PUSH_FAILED
756 else:
762 else:
757 merge_succeeded = True
763 merge_succeeded = True
758 else:
764 else:
759 merge_succeeded = False
765 merge_succeeded = False
760
766
761 return MergeResponse(
767 return MergeResponse(
762 merge_possible, merge_succeeded, merge_ref, merge_failure_reason)
768 merge_possible, merge_succeeded, merge_ref, merge_failure_reason)
763
769
764 def _get_shadow_instance(
770 def _get_shadow_instance(
765 self, shadow_repository_path, enable_hooks=False):
771 self, shadow_repository_path, enable_hooks=False):
766 config = self.config.copy()
772 config = self.config.copy()
767 if not enable_hooks:
773 if not enable_hooks:
768 config.clear_section('hooks')
774 config.clear_section('hooks')
769 return MercurialRepository(shadow_repository_path, config)
775 return MercurialRepository(shadow_repository_path, config)
770
776
771 def _validate_pull_reference(self, reference):
777 def _validate_pull_reference(self, reference):
772 if not (reference.name in self.bookmarks or
778 if not (reference.name in self.bookmarks or
773 reference.name in self.branches or
779 reference.name in self.branches or
774 self.get_commit(reference.commit_id)):
780 self.get_commit(reference.commit_id)):
775 raise CommitDoesNotExistError(
781 raise CommitDoesNotExistError(
776 'Unknown branch, bookmark or commit id')
782 'Unknown branch, bookmark or commit id')
777
783
778 def _local_pull(self, repository_path, reference):
784 def _local_pull(self, repository_path, reference):
779 """
785 """
780 Fetch a branch, bookmark or commit from a local repository.
786 Fetch a branch, bookmark or commit from a local repository.
781 """
787 """
782 repository_path = os.path.abspath(repository_path)
788 repository_path = os.path.abspath(repository_path)
783 if repository_path == self.path:
789 if repository_path == self.path:
784 raise ValueError('Cannot pull from the same repository')
790 raise ValueError('Cannot pull from the same repository')
785
791
786 reference_type_to_option_name = {
792 reference_type_to_option_name = {
787 'book': 'bookmark',
793 'book': 'bookmark',
788 'branch': 'branch',
794 'branch': 'branch',
789 }
795 }
790 option_name = reference_type_to_option_name.get(
796 option_name = reference_type_to_option_name.get(
791 reference.type, 'revision')
797 reference.type, 'revision')
792
798
793 if option_name == 'revision':
799 if option_name == 'revision':
794 ref = reference.commit_id
800 ref = reference.commit_id
795 else:
801 else:
796 ref = reference.name
802 ref = reference.name
797
803
798 options = {option_name: [ref]}
804 options = {option_name: [ref]}
799 self._remote.pull_cmd(repository_path, hooks=False, **options)
805 self._remote.pull_cmd(repository_path, hooks=False, **options)
800 self._remote.invalidate_vcs_cache()
806 self._remote.invalidate_vcs_cache()
801
807
802 def bookmark(self, bookmark, revision=None):
808 def bookmark(self, bookmark, revision=None):
803 if isinstance(bookmark, unicode):
809 if isinstance(bookmark, unicode):
804 bookmark = safe_str(bookmark)
810 bookmark = safe_str(bookmark)
805 self._remote.bookmark(bookmark, revision=revision)
811 self._remote.bookmark(bookmark, revision=revision)
806 self._remote.invalidate_vcs_cache()
812 self._remote.invalidate_vcs_cache()
807
813
808
814
809 class MercurialIndexBasedCollectionGenerator(CollectionGenerator):
815 class MercurialIndexBasedCollectionGenerator(CollectionGenerator):
810
816
811 def _commit_factory(self, commit_id):
817 def _commit_factory(self, commit_id):
812 return self.repo.get_commit(
818 return self.repo.get_commit(
813 commit_idx=commit_id, pre_load=self.pre_load)
819 commit_idx=commit_id, pre_load=self.pre_load)
General Comments 0
You need to be logged in to leave comments. Login now