##// END OF EJS Templates
path-permissions: Throw RepositoryRequirementError is hgacl cannot be read
idlsoft -
r2622:e5345aa5 default
parent child Browse files
Show More
@@ -1,926 +1,929 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2014-2018 RhodeCode GmbH
3 # Copyright (C) 2014-2018 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 import ConfigParser
24 import ConfigParser
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, exceptions
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, BasePathPermissionChecker)
41 MergeFailureReason, Reference, BasePathPermissionChecker)
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
75
76 self.path = safe_str(os.path.abspath(repo_path))
76 self.path = safe_str(os.path.abspath(repo_path))
77 # mercurial since 4.4.X requires certain configuration to be present
77 # mercurial since 4.4.X requires certain configuration to be present
78 # because sometimes we init the repos with config we need to meet
78 # because sometimes we init the repos with config we need to meet
79 # special requirements
79 # special requirements
80 self.config = config if config else self.get_default_config(
80 self.config = config if config else self.get_default_config(
81 default=[('extensions', 'largefiles', '1')])
81 default=[('extensions', 'largefiles', '1')])
82
82
83 self._remote = connection.Hg(
83 self._remote = connection.Hg(
84 self.path, self.config, with_wire=with_wire)
84 self.path, self.config, with_wire=with_wire)
85
85
86 self._init_repo(create, src_url, update_after_clone)
86 self._init_repo(create, src_url, update_after_clone)
87
87
88 # caches
88 # caches
89 self._commit_ids = {}
89 self._commit_ids = {}
90
90
91 @LazyProperty
91 @LazyProperty
92 def commit_ids(self):
92 def commit_ids(self):
93 """
93 """
94 Returns list of commit ids, in ascending order. Being lazy
94 Returns list of commit ids, in ascending order. Being lazy
95 attribute allows external tools to inject shas from cache.
95 attribute allows external tools to inject shas from cache.
96 """
96 """
97 commit_ids = self._get_all_commit_ids()
97 commit_ids = self._get_all_commit_ids()
98 self._rebuild_cache(commit_ids)
98 self._rebuild_cache(commit_ids)
99 return commit_ids
99 return commit_ids
100
100
101 def _rebuild_cache(self, commit_ids):
101 def _rebuild_cache(self, commit_ids):
102 self._commit_ids = dict((commit_id, index)
102 self._commit_ids = dict((commit_id, index)
103 for index, commit_id in enumerate(commit_ids))
103 for index, commit_id in enumerate(commit_ids))
104
104
105 @LazyProperty
105 @LazyProperty
106 def branches(self):
106 def branches(self):
107 return self._get_branches()
107 return self._get_branches()
108
108
109 @LazyProperty
109 @LazyProperty
110 def branches_closed(self):
110 def branches_closed(self):
111 return self._get_branches(active=False, closed=True)
111 return self._get_branches(active=False, closed=True)
112
112
113 @LazyProperty
113 @LazyProperty
114 def branches_all(self):
114 def branches_all(self):
115 all_branches = {}
115 all_branches = {}
116 all_branches.update(self.branches)
116 all_branches.update(self.branches)
117 all_branches.update(self.branches_closed)
117 all_branches.update(self.branches_closed)
118 return all_branches
118 return all_branches
119
119
120 def _get_branches(self, active=True, closed=False):
120 def _get_branches(self, active=True, closed=False):
121 """
121 """
122 Gets branches for this repository
122 Gets branches for this repository
123 Returns only not closed active branches by default
123 Returns only not closed active branches by default
124
124
125 :param active: return also active branches
125 :param active: return also active branches
126 :param closed: return also closed branches
126 :param closed: return also closed branches
127
127
128 """
128 """
129 if self.is_empty():
129 if self.is_empty():
130 return {}
130 return {}
131
131
132 def get_name(ctx):
132 def get_name(ctx):
133 return ctx[0]
133 return ctx[0]
134
134
135 _branches = [(safe_unicode(n), hexlify(h),) for n, h in
135 _branches = [(safe_unicode(n), hexlify(h),) for n, h in
136 self._remote.branches(active, closed).items()]
136 self._remote.branches(active, closed).items()]
137
137
138 return OrderedDict(sorted(_branches, key=get_name, reverse=False))
138 return OrderedDict(sorted(_branches, key=get_name, reverse=False))
139
139
140 @LazyProperty
140 @LazyProperty
141 def tags(self):
141 def tags(self):
142 """
142 """
143 Gets tags for this repository
143 Gets tags for this repository
144 """
144 """
145 return self._get_tags()
145 return self._get_tags()
146
146
147 def _get_tags(self):
147 def _get_tags(self):
148 if self.is_empty():
148 if self.is_empty():
149 return {}
149 return {}
150
150
151 def get_name(ctx):
151 def get_name(ctx):
152 return ctx[0]
152 return ctx[0]
153
153
154 _tags = [(safe_unicode(n), hexlify(h),) for n, h in
154 _tags = [(safe_unicode(n), hexlify(h),) for n, h in
155 self._remote.tags().items()]
155 self._remote.tags().items()]
156
156
157 return OrderedDict(sorted(_tags, key=get_name, reverse=True))
157 return OrderedDict(sorted(_tags, key=get_name, reverse=True))
158
158
159 def tag(self, name, user, commit_id=None, message=None, date=None,
159 def tag(self, name, user, commit_id=None, message=None, date=None,
160 **kwargs):
160 **kwargs):
161 """
161 """
162 Creates and returns a tag for the given ``commit_id``.
162 Creates and returns a tag for the given ``commit_id``.
163
163
164 :param name: name for new tag
164 :param name: name for new tag
165 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
165 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
166 :param commit_id: commit id for which new tag would be created
166 :param commit_id: commit id for which new tag would be created
167 :param message: message of the tag's commit
167 :param message: message of the tag's commit
168 :param date: date of tag's commit
168 :param date: date of tag's commit
169
169
170 :raises TagAlreadyExistError: if tag with same name already exists
170 :raises TagAlreadyExistError: if tag with same name already exists
171 """
171 """
172 if name in self.tags:
172 if name in self.tags:
173 raise TagAlreadyExistError("Tag %s already exists" % name)
173 raise TagAlreadyExistError("Tag %s already exists" % name)
174 commit = self.get_commit(commit_id=commit_id)
174 commit = self.get_commit(commit_id=commit_id)
175 local = kwargs.setdefault('local', False)
175 local = kwargs.setdefault('local', False)
176
176
177 if message is None:
177 if message is None:
178 message = "Added tag %s for commit %s" % (name, commit.short_id)
178 message = "Added tag %s for commit %s" % (name, commit.short_id)
179
179
180 date, tz = date_to_timestamp_plus_offset(date)
180 date, tz = date_to_timestamp_plus_offset(date)
181
181
182 self._remote.tag(
182 self._remote.tag(
183 name, commit.raw_id, message, local, user, date, tz)
183 name, commit.raw_id, message, local, user, date, tz)
184 self._remote.invalidate_vcs_cache()
184 self._remote.invalidate_vcs_cache()
185
185
186 # Reinitialize tags
186 # Reinitialize tags
187 self.tags = self._get_tags()
187 self.tags = self._get_tags()
188 tag_id = self.tags[name]
188 tag_id = self.tags[name]
189
189
190 return self.get_commit(commit_id=tag_id)
190 return self.get_commit(commit_id=tag_id)
191
191
192 def remove_tag(self, name, user, message=None, date=None):
192 def remove_tag(self, name, user, message=None, date=None):
193 """
193 """
194 Removes tag with the given `name`.
194 Removes tag with the given `name`.
195
195
196 :param name: name of the tag to be removed
196 :param name: name of the tag to be removed
197 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
197 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
198 :param message: message of the tag's removal commit
198 :param message: message of the tag's removal commit
199 :param date: date of tag's removal commit
199 :param date: date of tag's removal commit
200
200
201 :raises TagDoesNotExistError: if tag with given name does not exists
201 :raises TagDoesNotExistError: if tag with given name does not exists
202 """
202 """
203 if name not in self.tags:
203 if name not in self.tags:
204 raise TagDoesNotExistError("Tag %s does not exist" % name)
204 raise TagDoesNotExistError("Tag %s does not exist" % name)
205 if message is None:
205 if message is None:
206 message = "Removed tag %s" % name
206 message = "Removed tag %s" % name
207 local = False
207 local = False
208
208
209 date, tz = date_to_timestamp_plus_offset(date)
209 date, tz = date_to_timestamp_plus_offset(date)
210
210
211 self._remote.tag(name, nullid, message, local, user, date, tz)
211 self._remote.tag(name, nullid, message, local, user, date, tz)
212 self._remote.invalidate_vcs_cache()
212 self._remote.invalidate_vcs_cache()
213 self.tags = self._get_tags()
213 self.tags = self._get_tags()
214
214
215 @LazyProperty
215 @LazyProperty
216 def bookmarks(self):
216 def bookmarks(self):
217 """
217 """
218 Gets bookmarks for this repository
218 Gets bookmarks for this repository
219 """
219 """
220 return self._get_bookmarks()
220 return self._get_bookmarks()
221
221
222 def _get_bookmarks(self):
222 def _get_bookmarks(self):
223 if self.is_empty():
223 if self.is_empty():
224 return {}
224 return {}
225
225
226 def get_name(ctx):
226 def get_name(ctx):
227 return ctx[0]
227 return ctx[0]
228
228
229 _bookmarks = [
229 _bookmarks = [
230 (safe_unicode(n), hexlify(h)) for n, h in
230 (safe_unicode(n), hexlify(h)) for n, h in
231 self._remote.bookmarks().items()]
231 self._remote.bookmarks().items()]
232
232
233 return OrderedDict(sorted(_bookmarks, key=get_name))
233 return OrderedDict(sorted(_bookmarks, key=get_name))
234
234
235 def _get_all_commit_ids(self):
235 def _get_all_commit_ids(self):
236 return self._remote.get_all_commit_ids('visible')
236 return self._remote.get_all_commit_ids('visible')
237
237
238 def get_diff(
238 def get_diff(
239 self, commit1, commit2, path='', ignore_whitespace=False,
239 self, commit1, commit2, path='', ignore_whitespace=False,
240 context=3, path1=None):
240 context=3, path1=None):
241 """
241 """
242 Returns (git like) *diff*, as plain text. Shows changes introduced by
242 Returns (git like) *diff*, as plain text. Shows changes introduced by
243 `commit2` since `commit1`.
243 `commit2` since `commit1`.
244
244
245 :param commit1: Entry point from which diff is shown. Can be
245 :param commit1: Entry point from which diff is shown. Can be
246 ``self.EMPTY_COMMIT`` - in this case, patch showing all
246 ``self.EMPTY_COMMIT`` - in this case, patch showing all
247 the changes since empty state of the repository until `commit2`
247 the changes since empty state of the repository until `commit2`
248 :param commit2: Until which commit changes should be shown.
248 :param commit2: Until which commit changes should be shown.
249 :param ignore_whitespace: If set to ``True``, would not show whitespace
249 :param ignore_whitespace: If set to ``True``, would not show whitespace
250 changes. Defaults to ``False``.
250 changes. Defaults to ``False``.
251 :param context: How many lines before/after changed lines should be
251 :param context: How many lines before/after changed lines should be
252 shown. Defaults to ``3``.
252 shown. Defaults to ``3``.
253 """
253 """
254 self._validate_diff_commits(commit1, commit2)
254 self._validate_diff_commits(commit1, commit2)
255 if path1 is not None and path1 != path:
255 if path1 is not None and path1 != path:
256 raise ValueError("Diff of two different paths not supported.")
256 raise ValueError("Diff of two different paths not supported.")
257
257
258 if path:
258 if path:
259 file_filter = [self.path, path]
259 file_filter = [self.path, path]
260 else:
260 else:
261 file_filter = None
261 file_filter = None
262
262
263 diff = self._remote.diff(
263 diff = self._remote.diff(
264 commit1.raw_id, commit2.raw_id, file_filter=file_filter,
264 commit1.raw_id, commit2.raw_id, file_filter=file_filter,
265 opt_git=True, opt_ignorews=ignore_whitespace,
265 opt_git=True, opt_ignorews=ignore_whitespace,
266 context=context)
266 context=context)
267 return MercurialDiff(diff)
267 return MercurialDiff(diff)
268
268
269 def strip(self, commit_id, branch=None):
269 def strip(self, commit_id, branch=None):
270 self._remote.strip(commit_id, update=False, backup="none")
270 self._remote.strip(commit_id, update=False, backup="none")
271
271
272 self._remote.invalidate_vcs_cache()
272 self._remote.invalidate_vcs_cache()
273 self.commit_ids = self._get_all_commit_ids()
273 self.commit_ids = self._get_all_commit_ids()
274 self._rebuild_cache(self.commit_ids)
274 self._rebuild_cache(self.commit_ids)
275
275
276 def verify(self):
276 def verify(self):
277 verify = self._remote.verify()
277 verify = self._remote.verify()
278
278
279 self._remote.invalidate_vcs_cache()
279 self._remote.invalidate_vcs_cache()
280 return verify
280 return verify
281
281
282 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
282 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
283 if commit_id1 == commit_id2:
283 if commit_id1 == commit_id2:
284 return commit_id1
284 return commit_id1
285
285
286 ancestors = self._remote.revs_from_revspec(
286 ancestors = self._remote.revs_from_revspec(
287 "ancestor(id(%s), id(%s))", commit_id1, commit_id2,
287 "ancestor(id(%s), id(%s))", commit_id1, commit_id2,
288 other_path=repo2.path)
288 other_path=repo2.path)
289 return repo2[ancestors[0]].raw_id if ancestors else None
289 return repo2[ancestors[0]].raw_id if ancestors else None
290
290
291 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
291 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
292 if commit_id1 == commit_id2:
292 if commit_id1 == commit_id2:
293 commits = []
293 commits = []
294 else:
294 else:
295 if merge:
295 if merge:
296 indexes = self._remote.revs_from_revspec(
296 indexes = self._remote.revs_from_revspec(
297 "ancestors(id(%s)) - ancestors(id(%s)) - id(%s)",
297 "ancestors(id(%s)) - ancestors(id(%s)) - id(%s)",
298 commit_id2, commit_id1, commit_id1, other_path=repo2.path)
298 commit_id2, commit_id1, commit_id1, other_path=repo2.path)
299 else:
299 else:
300 indexes = self._remote.revs_from_revspec(
300 indexes = self._remote.revs_from_revspec(
301 "id(%s)..id(%s) - id(%s)", commit_id1, commit_id2,
301 "id(%s)..id(%s) - id(%s)", commit_id1, commit_id2,
302 commit_id1, other_path=repo2.path)
302 commit_id1, other_path=repo2.path)
303
303
304 commits = [repo2.get_commit(commit_idx=idx, pre_load=pre_load)
304 commits = [repo2.get_commit(commit_idx=idx, pre_load=pre_load)
305 for idx in indexes]
305 for idx in indexes]
306
306
307 return commits
307 return commits
308
308
309 @staticmethod
309 @staticmethod
310 def check_url(url, config):
310 def check_url(url, config):
311 """
311 """
312 Function will check given url and try to verify if it's a valid
312 Function will check given url and try to verify if it's a valid
313 link. Sometimes it may happened that mercurial will issue basic
313 link. Sometimes it may happened that mercurial will issue basic
314 auth request that can cause whole API to hang when used from python
314 auth request that can cause whole API to hang when used from python
315 or other external calls.
315 or other external calls.
316
316
317 On failures it'll raise urllib2.HTTPError, exception is also thrown
317 On failures it'll raise urllib2.HTTPError, exception is also thrown
318 when the return code is non 200
318 when the return code is non 200
319 """
319 """
320 # check first if it's not an local url
320 # check first if it's not an local url
321 if os.path.isdir(url) or url.startswith('file:'):
321 if os.path.isdir(url) or url.startswith('file:'):
322 return True
322 return True
323
323
324 # Request the _remote to verify the url
324 # Request the _remote to verify the url
325 return connection.Hg.check_url(url, config.serialize())
325 return connection.Hg.check_url(url, config.serialize())
326
326
327 @staticmethod
327 @staticmethod
328 def is_valid_repository(path):
328 def is_valid_repository(path):
329 return os.path.isdir(os.path.join(path, '.hg'))
329 return os.path.isdir(os.path.join(path, '.hg'))
330
330
331 def _init_repo(self, create, src_url=None, update_after_clone=False):
331 def _init_repo(self, create, src_url=None, update_after_clone=False):
332 """
332 """
333 Function will check for mercurial repository in given path. If there
333 Function will check for mercurial repository in given path. If there
334 is no repository in that path it will raise an exception unless
334 is no repository in that path it will raise an exception unless
335 `create` parameter is set to True - in that case repository would
335 `create` parameter is set to True - in that case repository would
336 be created.
336 be created.
337
337
338 If `src_url` is given, would try to clone repository from the
338 If `src_url` is given, would try to clone repository from the
339 location at given clone_point. Additionally it'll make update to
339 location at given clone_point. Additionally it'll make update to
340 working copy accordingly to `update_after_clone` flag.
340 working copy accordingly to `update_after_clone` flag.
341 """
341 """
342 if create and os.path.exists(self.path):
342 if create and os.path.exists(self.path):
343 raise RepositoryError(
343 raise RepositoryError(
344 "Cannot create repository at %s, location already exist"
344 "Cannot create repository at %s, location already exist"
345 % self.path)
345 % self.path)
346
346
347 if src_url:
347 if src_url:
348 url = str(self._get_url(src_url))
348 url = str(self._get_url(src_url))
349 MercurialRepository.check_url(url, self.config)
349 MercurialRepository.check_url(url, self.config)
350
350
351 self._remote.clone(url, self.path, update_after_clone)
351 self._remote.clone(url, self.path, update_after_clone)
352
352
353 # Don't try to create if we've already cloned repo
353 # Don't try to create if we've already cloned repo
354 create = False
354 create = False
355
355
356 if create:
356 if create:
357 os.makedirs(self.path, mode=0755)
357 os.makedirs(self.path, mode=0755)
358
358
359 self._remote.localrepository(create)
359 self._remote.localrepository(create)
360
360
361 @LazyProperty
361 @LazyProperty
362 def in_memory_commit(self):
362 def in_memory_commit(self):
363 return MercurialInMemoryCommit(self)
363 return MercurialInMemoryCommit(self)
364
364
365 @LazyProperty
365 @LazyProperty
366 def description(self):
366 def description(self):
367 description = self._remote.get_config_value(
367 description = self._remote.get_config_value(
368 'web', 'description', untrusted=True)
368 'web', 'description', untrusted=True)
369 return safe_unicode(description or self.DEFAULT_DESCRIPTION)
369 return safe_unicode(description or self.DEFAULT_DESCRIPTION)
370
370
371 @LazyProperty
371 @LazyProperty
372 def contact(self):
372 def contact(self):
373 contact = (
373 contact = (
374 self._remote.get_config_value("web", "contact") or
374 self._remote.get_config_value("web", "contact") or
375 self._remote.get_config_value("ui", "username"))
375 self._remote.get_config_value("ui", "username"))
376 return safe_unicode(contact or self.DEFAULT_CONTACT)
376 return safe_unicode(contact or self.DEFAULT_CONTACT)
377
377
378 @LazyProperty
378 @LazyProperty
379 def last_change(self):
379 def last_change(self):
380 """
380 """
381 Returns last change made on this repository as
381 Returns last change made on this repository as
382 `datetime.datetime` object.
382 `datetime.datetime` object.
383 """
383 """
384 try:
384 try:
385 return self.get_commit().date
385 return self.get_commit().date
386 except RepositoryError:
386 except RepositoryError:
387 tzoffset = makedate()[1]
387 tzoffset = makedate()[1]
388 return utcdate_fromtimestamp(self._get_fs_mtime(), tzoffset)
388 return utcdate_fromtimestamp(self._get_fs_mtime(), tzoffset)
389
389
390 def _get_fs_mtime(self):
390 def _get_fs_mtime(self):
391 # fallback to filesystem
391 # fallback to filesystem
392 cl_path = os.path.join(self.path, '.hg', "00changelog.i")
392 cl_path = os.path.join(self.path, '.hg', "00changelog.i")
393 st_path = os.path.join(self.path, '.hg', "store")
393 st_path = os.path.join(self.path, '.hg', "store")
394 if os.path.exists(cl_path):
394 if os.path.exists(cl_path):
395 return os.stat(cl_path).st_mtime
395 return os.stat(cl_path).st_mtime
396 else:
396 else:
397 return os.stat(st_path).st_mtime
397 return os.stat(st_path).st_mtime
398
398
399 def _sanitize_commit_idx(self, idx):
399 def _sanitize_commit_idx(self, idx):
400 # Note: Mercurial has ``int(-1)`` reserved as not existing id_or_idx
400 # Note: Mercurial has ``int(-1)`` reserved as not existing id_or_idx
401 # number. A `long` is treated in the correct way though. So we convert
401 # number. A `long` is treated in the correct way though. So we convert
402 # `int` to `long` here to make sure it is handled correctly.
402 # `int` to `long` here to make sure it is handled correctly.
403 if isinstance(idx, int):
403 if isinstance(idx, int):
404 return long(idx)
404 return long(idx)
405 return idx
405 return idx
406
406
407 def _get_url(self, url):
407 def _get_url(self, url):
408 """
408 """
409 Returns normalized url. If schema is not given, would fall
409 Returns normalized url. If schema is not given, would fall
410 to filesystem
410 to filesystem
411 (``file:///``) schema.
411 (``file:///``) schema.
412 """
412 """
413 url = url.encode('utf8')
413 url = url.encode('utf8')
414 if url != 'default' and '://' not in url:
414 if url != 'default' and '://' not in url:
415 url = "file:" + urllib.pathname2url(url)
415 url = "file:" + urllib.pathname2url(url)
416 return url
416 return url
417
417
418 def get_hook_location(self):
418 def get_hook_location(self):
419 """
419 """
420 returns absolute path to location where hooks are stored
420 returns absolute path to location where hooks are stored
421 """
421 """
422 return os.path.join(self.path, '.hg', '.hgrc')
422 return os.path.join(self.path, '.hg', '.hgrc')
423
423
424 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
424 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
425 """
425 """
426 Returns ``MercurialCommit`` object representing repository's
426 Returns ``MercurialCommit`` object representing repository's
427 commit at the given `commit_id` or `commit_idx`.
427 commit at the given `commit_id` or `commit_idx`.
428 """
428 """
429 if self.is_empty():
429 if self.is_empty():
430 raise EmptyRepositoryError("There are no commits yet")
430 raise EmptyRepositoryError("There are no commits yet")
431
431
432 if commit_id is not None:
432 if commit_id is not None:
433 self._validate_commit_id(commit_id)
433 self._validate_commit_id(commit_id)
434 try:
434 try:
435 idx = self._commit_ids[commit_id]
435 idx = self._commit_ids[commit_id]
436 return MercurialCommit(self, commit_id, idx, pre_load=pre_load)
436 return MercurialCommit(self, commit_id, idx, pre_load=pre_load)
437 except KeyError:
437 except KeyError:
438 pass
438 pass
439 elif commit_idx is not None:
439 elif commit_idx is not None:
440 self._validate_commit_idx(commit_idx)
440 self._validate_commit_idx(commit_idx)
441 commit_idx = self._sanitize_commit_idx(commit_idx)
441 commit_idx = self._sanitize_commit_idx(commit_idx)
442 try:
442 try:
443 id_ = self.commit_ids[commit_idx]
443 id_ = self.commit_ids[commit_idx]
444 if commit_idx < 0:
444 if commit_idx < 0:
445 commit_idx += len(self.commit_ids)
445 commit_idx += len(self.commit_ids)
446 return MercurialCommit(
446 return MercurialCommit(
447 self, id_, commit_idx, pre_load=pre_load)
447 self, id_, commit_idx, pre_load=pre_load)
448 except IndexError:
448 except IndexError:
449 commit_id = commit_idx
449 commit_id = commit_idx
450 else:
450 else:
451 commit_id = "tip"
451 commit_id = "tip"
452
452
453 # TODO Paris: Ugly hack to "serialize" long for msgpack
453 # TODO Paris: Ugly hack to "serialize" long for msgpack
454 if isinstance(commit_id, long):
454 if isinstance(commit_id, long):
455 commit_id = float(commit_id)
455 commit_id = float(commit_id)
456
456
457 if isinstance(commit_id, unicode):
457 if isinstance(commit_id, unicode):
458 commit_id = safe_str(commit_id)
458 commit_id = safe_str(commit_id)
459
459
460 try:
460 try:
461 raw_id, idx = self._remote.lookup(commit_id, both=True)
461 raw_id, idx = self._remote.lookup(commit_id, both=True)
462 except CommitDoesNotExistError:
462 except CommitDoesNotExistError:
463 msg = "Commit %s does not exist for %s" % (
463 msg = "Commit %s does not exist for %s" % (
464 commit_id, self)
464 commit_id, self)
465 raise CommitDoesNotExistError(msg)
465 raise CommitDoesNotExistError(msg)
466
466
467 return MercurialCommit(self, raw_id, idx, pre_load=pre_load)
467 return MercurialCommit(self, raw_id, idx, pre_load=pre_load)
468
468
469 def get_commits(
469 def get_commits(
470 self, start_id=None, end_id=None, start_date=None, end_date=None,
470 self, start_id=None, end_id=None, start_date=None, end_date=None,
471 branch_name=None, show_hidden=False, pre_load=None):
471 branch_name=None, show_hidden=False, pre_load=None):
472 """
472 """
473 Returns generator of ``MercurialCommit`` objects from start to end
473 Returns generator of ``MercurialCommit`` objects from start to end
474 (both are inclusive)
474 (both are inclusive)
475
475
476 :param start_id: None, str(commit_id)
476 :param start_id: None, str(commit_id)
477 :param end_id: None, str(commit_id)
477 :param end_id: None, str(commit_id)
478 :param start_date: if specified, commits with commit date less than
478 :param start_date: if specified, commits with commit date less than
479 ``start_date`` would be filtered out from returned set
479 ``start_date`` would be filtered out from returned set
480 :param end_date: if specified, commits with commit date greater than
480 :param end_date: if specified, commits with commit date greater than
481 ``end_date`` would be filtered out from returned set
481 ``end_date`` would be filtered out from returned set
482 :param branch_name: if specified, commits not reachable from given
482 :param branch_name: if specified, commits not reachable from given
483 branch would be filtered out from returned set
483 branch would be filtered out from returned set
484 :param show_hidden: Show hidden commits such as obsolete or hidden from
484 :param show_hidden: Show hidden commits such as obsolete or hidden from
485 Mercurial evolve
485 Mercurial evolve
486 :raise BranchDoesNotExistError: If given ``branch_name`` does not
486 :raise BranchDoesNotExistError: If given ``branch_name`` does not
487 exist.
487 exist.
488 :raise CommitDoesNotExistError: If commit for given ``start`` or
488 :raise CommitDoesNotExistError: If commit for given ``start`` or
489 ``end`` could not be found.
489 ``end`` could not be found.
490 """
490 """
491 # actually we should check now if it's not an empty repo
491 # actually we should check now if it's not an empty repo
492 branch_ancestors = False
492 branch_ancestors = False
493 if self.is_empty():
493 if self.is_empty():
494 raise EmptyRepositoryError("There are no commits yet")
494 raise EmptyRepositoryError("There are no commits yet")
495 self._validate_branch_name(branch_name)
495 self._validate_branch_name(branch_name)
496
496
497 if start_id is not None:
497 if start_id is not None:
498 self._validate_commit_id(start_id)
498 self._validate_commit_id(start_id)
499 c_start = self.get_commit(commit_id=start_id)
499 c_start = self.get_commit(commit_id=start_id)
500 start_pos = self._commit_ids[c_start.raw_id]
500 start_pos = self._commit_ids[c_start.raw_id]
501 else:
501 else:
502 start_pos = None
502 start_pos = None
503
503
504 if end_id is not None:
504 if end_id is not None:
505 self._validate_commit_id(end_id)
505 self._validate_commit_id(end_id)
506 c_end = self.get_commit(commit_id=end_id)
506 c_end = self.get_commit(commit_id=end_id)
507 end_pos = max(0, self._commit_ids[c_end.raw_id])
507 end_pos = max(0, self._commit_ids[c_end.raw_id])
508 else:
508 else:
509 end_pos = None
509 end_pos = None
510
510
511 if None not in [start_id, end_id] and start_pos > end_pos:
511 if None not in [start_id, end_id] and start_pos > end_pos:
512 raise RepositoryError(
512 raise RepositoryError(
513 "Start commit '%s' cannot be after end commit '%s'" %
513 "Start commit '%s' cannot be after end commit '%s'" %
514 (start_id, end_id))
514 (start_id, end_id))
515
515
516 if end_pos is not None:
516 if end_pos is not None:
517 end_pos += 1
517 end_pos += 1
518
518
519 commit_filter = []
519 commit_filter = []
520
520
521 if branch_name and not branch_ancestors:
521 if branch_name and not branch_ancestors:
522 commit_filter.append('branch("%s")' % (branch_name,))
522 commit_filter.append('branch("%s")' % (branch_name,))
523 elif branch_name and branch_ancestors:
523 elif branch_name and branch_ancestors:
524 commit_filter.append('ancestors(branch("%s"))' % (branch_name,))
524 commit_filter.append('ancestors(branch("%s"))' % (branch_name,))
525
525
526 if start_date and not end_date:
526 if start_date and not end_date:
527 commit_filter.append('date(">%s")' % (start_date,))
527 commit_filter.append('date(">%s")' % (start_date,))
528 if end_date and not start_date:
528 if end_date and not start_date:
529 commit_filter.append('date("<%s")' % (end_date,))
529 commit_filter.append('date("<%s")' % (end_date,))
530 if start_date and end_date:
530 if start_date and end_date:
531 commit_filter.append(
531 commit_filter.append(
532 'date(">%s") and date("<%s")' % (start_date, end_date))
532 'date(">%s") and date("<%s")' % (start_date, end_date))
533
533
534 if not show_hidden:
534 if not show_hidden:
535 commit_filter.append('not obsolete()')
535 commit_filter.append('not obsolete()')
536 commit_filter.append('not hidden()')
536 commit_filter.append('not hidden()')
537
537
538 # TODO: johbo: Figure out a simpler way for this solution
538 # TODO: johbo: Figure out a simpler way for this solution
539 collection_generator = CollectionGenerator
539 collection_generator = CollectionGenerator
540 if commit_filter:
540 if commit_filter:
541 commit_filter = ' and '.join(map(safe_str, commit_filter))
541 commit_filter = ' and '.join(map(safe_str, commit_filter))
542 revisions = self._remote.rev_range([commit_filter])
542 revisions = self._remote.rev_range([commit_filter])
543 collection_generator = MercurialIndexBasedCollectionGenerator
543 collection_generator = MercurialIndexBasedCollectionGenerator
544 else:
544 else:
545 revisions = self.commit_ids
545 revisions = self.commit_ids
546
546
547 if start_pos or end_pos:
547 if start_pos or end_pos:
548 revisions = revisions[start_pos:end_pos]
548 revisions = revisions[start_pos:end_pos]
549
549
550 return collection_generator(self, revisions, pre_load=pre_load)
550 return collection_generator(self, revisions, pre_load=pre_load)
551
551
552 def pull(self, url, commit_ids=None):
552 def pull(self, url, commit_ids=None):
553 """
553 """
554 Tries to pull changes from external location.
554 Tries to pull changes from external location.
555
555
556 :param commit_ids: Optional. Can be set to a list of commit ids
556 :param commit_ids: Optional. Can be set to a list of commit ids
557 which shall be pulled from the other repository.
557 which shall be pulled from the other repository.
558 """
558 """
559 url = self._get_url(url)
559 url = self._get_url(url)
560 self._remote.pull(url, commit_ids=commit_ids)
560 self._remote.pull(url, commit_ids=commit_ids)
561 self._remote.invalidate_vcs_cache()
561 self._remote.invalidate_vcs_cache()
562
562
563 def push(self, url):
563 def push(self, url):
564 url = self._get_url(url)
564 url = self._get_url(url)
565 self._remote.sync_push(url)
565 self._remote.sync_push(url)
566
566
567 def _local_clone(self, clone_path):
567 def _local_clone(self, clone_path):
568 """
568 """
569 Create a local clone of the current repo.
569 Create a local clone of the current repo.
570 """
570 """
571 self._remote.clone(self.path, clone_path, update_after_clone=True,
571 self._remote.clone(self.path, clone_path, update_after_clone=True,
572 hooks=False)
572 hooks=False)
573
573
574 def _update(self, revision, clean=False):
574 def _update(self, revision, clean=False):
575 """
575 """
576 Update the working copy to the specified revision.
576 Update the working copy to the specified revision.
577 """
577 """
578 log.debug('Doing checkout to commit: `%s` for %s', revision, self)
578 log.debug('Doing checkout to commit: `%s` for %s', revision, self)
579 self._remote.update(revision, clean=clean)
579 self._remote.update(revision, clean=clean)
580
580
581 def _identify(self):
581 def _identify(self):
582 """
582 """
583 Return the current state of the working directory.
583 Return the current state of the working directory.
584 """
584 """
585 return self._remote.identify().strip().rstrip('+')
585 return self._remote.identify().strip().rstrip('+')
586
586
587 def _heads(self, branch=None):
587 def _heads(self, branch=None):
588 """
588 """
589 Return the commit ids of the repository heads.
589 Return the commit ids of the repository heads.
590 """
590 """
591 return self._remote.heads(branch=branch).strip().split(' ')
591 return self._remote.heads(branch=branch).strip().split(' ')
592
592
593 def _ancestor(self, revision1, revision2):
593 def _ancestor(self, revision1, revision2):
594 """
594 """
595 Return the common ancestor of the two revisions.
595 Return the common ancestor of the two revisions.
596 """
596 """
597 return self._remote.ancestor(revision1, revision2)
597 return self._remote.ancestor(revision1, revision2)
598
598
599 def _local_push(
599 def _local_push(
600 self, revision, repository_path, push_branches=False,
600 self, revision, repository_path, push_branches=False,
601 enable_hooks=False):
601 enable_hooks=False):
602 """
602 """
603 Push the given revision to the specified repository.
603 Push the given revision to the specified repository.
604
604
605 :param push_branches: allow to create branches in the target repo.
605 :param push_branches: allow to create branches in the target repo.
606 """
606 """
607 self._remote.push(
607 self._remote.push(
608 [revision], repository_path, hooks=enable_hooks,
608 [revision], repository_path, hooks=enable_hooks,
609 push_branches=push_branches)
609 push_branches=push_branches)
610
610
611 def _local_merge(self, target_ref, merge_message, user_name, user_email,
611 def _local_merge(self, target_ref, merge_message, user_name, user_email,
612 source_ref, use_rebase=False, dry_run=False):
612 source_ref, use_rebase=False, dry_run=False):
613 """
613 """
614 Merge the given source_revision into the checked out revision.
614 Merge the given source_revision into the checked out revision.
615
615
616 Returns the commit id of the merge and a boolean indicating if the
616 Returns the commit id of the merge and a boolean indicating if the
617 commit needs to be pushed.
617 commit needs to be pushed.
618 """
618 """
619 self._update(target_ref.commit_id)
619 self._update(target_ref.commit_id)
620
620
621 ancestor = self._ancestor(target_ref.commit_id, source_ref.commit_id)
621 ancestor = self._ancestor(target_ref.commit_id, source_ref.commit_id)
622 is_the_same_branch = self._is_the_same_branch(target_ref, source_ref)
622 is_the_same_branch = self._is_the_same_branch(target_ref, source_ref)
623
623
624 if ancestor == source_ref.commit_id:
624 if ancestor == source_ref.commit_id:
625 # Nothing to do, the changes were already integrated
625 # Nothing to do, the changes were already integrated
626 return target_ref.commit_id, False
626 return target_ref.commit_id, False
627
627
628 elif ancestor == target_ref.commit_id and is_the_same_branch:
628 elif ancestor == target_ref.commit_id and is_the_same_branch:
629 # In this case we should force a commit message
629 # In this case we should force a commit message
630 return source_ref.commit_id, True
630 return source_ref.commit_id, True
631
631
632 if use_rebase:
632 if use_rebase:
633 try:
633 try:
634 bookmark_name = 'rcbook%s%s' % (source_ref.commit_id,
634 bookmark_name = 'rcbook%s%s' % (source_ref.commit_id,
635 target_ref.commit_id)
635 target_ref.commit_id)
636 self.bookmark(bookmark_name, revision=source_ref.commit_id)
636 self.bookmark(bookmark_name, revision=source_ref.commit_id)
637 self._remote.rebase(
637 self._remote.rebase(
638 source=source_ref.commit_id, dest=target_ref.commit_id)
638 source=source_ref.commit_id, dest=target_ref.commit_id)
639 self._remote.invalidate_vcs_cache()
639 self._remote.invalidate_vcs_cache()
640 self._update(bookmark_name)
640 self._update(bookmark_name)
641 return self._identify(), True
641 return self._identify(), True
642 except RepositoryError:
642 except RepositoryError:
643 # The rebase-abort may raise another exception which 'hides'
643 # The rebase-abort may raise another exception which 'hides'
644 # the original one, therefore we log it here.
644 # the original one, therefore we log it here.
645 log.exception('Error while rebasing shadow repo during merge.')
645 log.exception('Error while rebasing shadow repo during merge.')
646
646
647 # Cleanup any rebase leftovers
647 # Cleanup any rebase leftovers
648 self._remote.invalidate_vcs_cache()
648 self._remote.invalidate_vcs_cache()
649 self._remote.rebase(abort=True)
649 self._remote.rebase(abort=True)
650 self._remote.invalidate_vcs_cache()
650 self._remote.invalidate_vcs_cache()
651 self._remote.update(clean=True)
651 self._remote.update(clean=True)
652 raise
652 raise
653 else:
653 else:
654 try:
654 try:
655 self._remote.merge(source_ref.commit_id)
655 self._remote.merge(source_ref.commit_id)
656 self._remote.invalidate_vcs_cache()
656 self._remote.invalidate_vcs_cache()
657 self._remote.commit(
657 self._remote.commit(
658 message=safe_str(merge_message),
658 message=safe_str(merge_message),
659 username=safe_str('%s <%s>' % (user_name, user_email)))
659 username=safe_str('%s <%s>' % (user_name, user_email)))
660 self._remote.invalidate_vcs_cache()
660 self._remote.invalidate_vcs_cache()
661 return self._identify(), True
661 return self._identify(), True
662 except RepositoryError:
662 except RepositoryError:
663 # Cleanup any merge leftovers
663 # Cleanup any merge leftovers
664 self._remote.update(clean=True)
664 self._remote.update(clean=True)
665 raise
665 raise
666
666
667 def _local_close(self, target_ref, user_name, user_email,
667 def _local_close(self, target_ref, user_name, user_email,
668 source_ref, close_message=''):
668 source_ref, close_message=''):
669 """
669 """
670 Close the branch of the given source_revision
670 Close the branch of the given source_revision
671
671
672 Returns the commit id of the close and a boolean indicating if the
672 Returns the commit id of the close and a boolean indicating if the
673 commit needs to be pushed.
673 commit needs to be pushed.
674 """
674 """
675 self._update(source_ref.commit_id)
675 self._update(source_ref.commit_id)
676 message = close_message or "Closing branch: `{}`".format(source_ref.name)
676 message = close_message or "Closing branch: `{}`".format(source_ref.name)
677 try:
677 try:
678 self._remote.commit(
678 self._remote.commit(
679 message=safe_str(message),
679 message=safe_str(message),
680 username=safe_str('%s <%s>' % (user_name, user_email)),
680 username=safe_str('%s <%s>' % (user_name, user_email)),
681 close_branch=True)
681 close_branch=True)
682 self._remote.invalidate_vcs_cache()
682 self._remote.invalidate_vcs_cache()
683 return self._identify(), True
683 return self._identify(), True
684 except RepositoryError:
684 except RepositoryError:
685 # Cleanup any commit leftovers
685 # Cleanup any commit leftovers
686 self._remote.update(clean=True)
686 self._remote.update(clean=True)
687 raise
687 raise
688
688
689 def _is_the_same_branch(self, target_ref, source_ref):
689 def _is_the_same_branch(self, target_ref, source_ref):
690 return (
690 return (
691 self._get_branch_name(target_ref) ==
691 self._get_branch_name(target_ref) ==
692 self._get_branch_name(source_ref))
692 self._get_branch_name(source_ref))
693
693
694 def _get_branch_name(self, ref):
694 def _get_branch_name(self, ref):
695 if ref.type == 'branch':
695 if ref.type == 'branch':
696 return ref.name
696 return ref.name
697 return self._remote.ctx_branch(ref.commit_id)
697 return self._remote.ctx_branch(ref.commit_id)
698
698
699 def _get_shadow_repository_path(self, workspace_id):
699 def _get_shadow_repository_path(self, workspace_id):
700 # The name of the shadow repository must start with '.', so it is
700 # The name of the shadow repository must start with '.', so it is
701 # skipped by 'rhodecode.lib.utils.get_filesystem_repos'.
701 # skipped by 'rhodecode.lib.utils.get_filesystem_repos'.
702 return os.path.join(
702 return os.path.join(
703 os.path.dirname(self.path),
703 os.path.dirname(self.path),
704 '.__shadow_%s_%s' % (os.path.basename(self.path), workspace_id))
704 '.__shadow_%s_%s' % (os.path.basename(self.path), workspace_id))
705
705
706 def _maybe_prepare_merge_workspace(self, workspace_id, unused_target_ref, unused_source_ref):
706 def _maybe_prepare_merge_workspace(self, workspace_id, unused_target_ref, unused_source_ref):
707 shadow_repository_path = self._get_shadow_repository_path(workspace_id)
707 shadow_repository_path = self._get_shadow_repository_path(workspace_id)
708 if not os.path.exists(shadow_repository_path):
708 if not os.path.exists(shadow_repository_path):
709 self._local_clone(shadow_repository_path)
709 self._local_clone(shadow_repository_path)
710 log.debug(
710 log.debug(
711 'Prepared shadow repository in %s', shadow_repository_path)
711 'Prepared shadow repository in %s', shadow_repository_path)
712
712
713 return shadow_repository_path
713 return shadow_repository_path
714
714
715 def cleanup_merge_workspace(self, workspace_id):
715 def cleanup_merge_workspace(self, workspace_id):
716 shadow_repository_path = self._get_shadow_repository_path(workspace_id)
716 shadow_repository_path = self._get_shadow_repository_path(workspace_id)
717 shutil.rmtree(shadow_repository_path, ignore_errors=True)
717 shutil.rmtree(shadow_repository_path, ignore_errors=True)
718
718
719 def _merge_repo(self, shadow_repository_path, target_ref,
719 def _merge_repo(self, shadow_repository_path, target_ref,
720 source_repo, source_ref, merge_message,
720 source_repo, source_ref, merge_message,
721 merger_name, merger_email, dry_run=False,
721 merger_name, merger_email, dry_run=False,
722 use_rebase=False, close_branch=False):
722 use_rebase=False, close_branch=False):
723
723
724 log.debug('Executing merge_repo with %s strategy, dry_run mode:%s',
724 log.debug('Executing merge_repo with %s strategy, dry_run mode:%s',
725 'rebase' if use_rebase else 'merge', dry_run)
725 'rebase' if use_rebase else 'merge', dry_run)
726 if target_ref.commit_id not in self._heads():
726 if target_ref.commit_id not in self._heads():
727 return MergeResponse(
727 return MergeResponse(
728 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD)
728 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD)
729
729
730 try:
730 try:
731 if (target_ref.type == 'branch' and
731 if (target_ref.type == 'branch' and
732 len(self._heads(target_ref.name)) != 1):
732 len(self._heads(target_ref.name)) != 1):
733 return MergeResponse(
733 return MergeResponse(
734 False, False, None,
734 False, False, None,
735 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS)
735 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS)
736 except CommitDoesNotExistError:
736 except CommitDoesNotExistError:
737 log.exception('Failure when looking up branch heads on hg target')
737 log.exception('Failure when looking up branch heads on hg target')
738 return MergeResponse(
738 return MergeResponse(
739 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
739 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
740
740
741 shadow_repo = self._get_shadow_instance(shadow_repository_path)
741 shadow_repo = self._get_shadow_instance(shadow_repository_path)
742
742
743 log.debug('Pulling in target reference %s', target_ref)
743 log.debug('Pulling in target reference %s', target_ref)
744 self._validate_pull_reference(target_ref)
744 self._validate_pull_reference(target_ref)
745 shadow_repo._local_pull(self.path, target_ref)
745 shadow_repo._local_pull(self.path, target_ref)
746 try:
746 try:
747 log.debug('Pulling in source reference %s', source_ref)
747 log.debug('Pulling in source reference %s', source_ref)
748 source_repo._validate_pull_reference(source_ref)
748 source_repo._validate_pull_reference(source_ref)
749 shadow_repo._local_pull(source_repo.path, source_ref)
749 shadow_repo._local_pull(source_repo.path, source_ref)
750 except CommitDoesNotExistError:
750 except CommitDoesNotExistError:
751 log.exception('Failure when doing local pull on hg shadow repo')
751 log.exception('Failure when doing local pull on hg shadow repo')
752 return MergeResponse(
752 return MergeResponse(
753 False, False, None, MergeFailureReason.MISSING_SOURCE_REF)
753 False, False, None, MergeFailureReason.MISSING_SOURCE_REF)
754
754
755 merge_ref = None
755 merge_ref = None
756 merge_commit_id = None
756 merge_commit_id = None
757 close_commit_id = None
757 close_commit_id = None
758 merge_failure_reason = MergeFailureReason.NONE
758 merge_failure_reason = MergeFailureReason.NONE
759
759
760 # enforce that close branch should be used only in case we source from
760 # enforce that close branch should be used only in case we source from
761 # an actual Branch
761 # an actual Branch
762 close_branch = close_branch and source_ref.type == 'branch'
762 close_branch = close_branch and source_ref.type == 'branch'
763
763
764 # don't allow to close branch if source and target are the same
764 # don't allow to close branch if source and target are the same
765 close_branch = close_branch and source_ref.name != target_ref.name
765 close_branch = close_branch and source_ref.name != target_ref.name
766
766
767 needs_push_on_close = False
767 needs_push_on_close = False
768 if close_branch and not use_rebase and not dry_run:
768 if close_branch and not use_rebase and not dry_run:
769 try:
769 try:
770 close_commit_id, needs_push_on_close = shadow_repo._local_close(
770 close_commit_id, needs_push_on_close = shadow_repo._local_close(
771 target_ref, merger_name, merger_email, source_ref)
771 target_ref, merger_name, merger_email, source_ref)
772 merge_possible = True
772 merge_possible = True
773 except RepositoryError:
773 except RepositoryError:
774 log.exception(
774 log.exception(
775 'Failure when doing close branch on hg shadow repo')
775 'Failure when doing close branch on hg shadow repo')
776 merge_possible = False
776 merge_possible = False
777 merge_failure_reason = MergeFailureReason.MERGE_FAILED
777 merge_failure_reason = MergeFailureReason.MERGE_FAILED
778 else:
778 else:
779 merge_possible = True
779 merge_possible = True
780
780
781 if merge_possible:
781 if merge_possible:
782 try:
782 try:
783 merge_commit_id, needs_push = shadow_repo._local_merge(
783 merge_commit_id, needs_push = shadow_repo._local_merge(
784 target_ref, merge_message, merger_name, merger_email,
784 target_ref, merge_message, merger_name, merger_email,
785 source_ref, use_rebase=use_rebase, dry_run=dry_run)
785 source_ref, use_rebase=use_rebase, dry_run=dry_run)
786 merge_possible = True
786 merge_possible = True
787
787
788 # read the state of the close action, if it
788 # read the state of the close action, if it
789 # maybe required a push
789 # maybe required a push
790 needs_push = needs_push or needs_push_on_close
790 needs_push = needs_push or needs_push_on_close
791
791
792 # Set a bookmark pointing to the merge commit. This bookmark
792 # Set a bookmark pointing to the merge commit. This bookmark
793 # may be used to easily identify the last successful merge
793 # may be used to easily identify the last successful merge
794 # commit in the shadow repository.
794 # commit in the shadow repository.
795 shadow_repo.bookmark('pr-merge', revision=merge_commit_id)
795 shadow_repo.bookmark('pr-merge', revision=merge_commit_id)
796 merge_ref = Reference('book', 'pr-merge', merge_commit_id)
796 merge_ref = Reference('book', 'pr-merge', merge_commit_id)
797 except SubrepoMergeError:
797 except SubrepoMergeError:
798 log.exception(
798 log.exception(
799 'Subrepo merge error during local merge on hg shadow repo.')
799 'Subrepo merge error during local merge on hg shadow repo.')
800 merge_possible = False
800 merge_possible = False
801 merge_failure_reason = MergeFailureReason.SUBREPO_MERGE_FAILED
801 merge_failure_reason = MergeFailureReason.SUBREPO_MERGE_FAILED
802 needs_push = False
802 needs_push = False
803 except RepositoryError:
803 except RepositoryError:
804 log.exception('Failure when doing local merge on hg shadow repo')
804 log.exception('Failure when doing local merge on hg shadow repo')
805 merge_possible = False
805 merge_possible = False
806 merge_failure_reason = MergeFailureReason.MERGE_FAILED
806 merge_failure_reason = MergeFailureReason.MERGE_FAILED
807 needs_push = False
807 needs_push = False
808
808
809 if merge_possible and not dry_run:
809 if merge_possible and not dry_run:
810 if needs_push:
810 if needs_push:
811 # In case the target is a bookmark, update it, so after pushing
811 # In case the target is a bookmark, update it, so after pushing
812 # the bookmarks is also updated in the target.
812 # the bookmarks is also updated in the target.
813 if target_ref.type == 'book':
813 if target_ref.type == 'book':
814 shadow_repo.bookmark(
814 shadow_repo.bookmark(
815 target_ref.name, revision=merge_commit_id)
815 target_ref.name, revision=merge_commit_id)
816 try:
816 try:
817 shadow_repo_with_hooks = self._get_shadow_instance(
817 shadow_repo_with_hooks = self._get_shadow_instance(
818 shadow_repository_path,
818 shadow_repository_path,
819 enable_hooks=True)
819 enable_hooks=True)
820 # This is the actual merge action, we push from shadow
820 # This is the actual merge action, we push from shadow
821 # into origin.
821 # into origin.
822 # Note: the push_branches option will push any new branch
822 # Note: the push_branches option will push any new branch
823 # defined in the source repository to the target. This may
823 # defined in the source repository to the target. This may
824 # be dangerous as branches are permanent in Mercurial.
824 # be dangerous as branches are permanent in Mercurial.
825 # This feature was requested in issue #441.
825 # This feature was requested in issue #441.
826 shadow_repo_with_hooks._local_push(
826 shadow_repo_with_hooks._local_push(
827 merge_commit_id, self.path, push_branches=True,
827 merge_commit_id, self.path, push_branches=True,
828 enable_hooks=True)
828 enable_hooks=True)
829
829
830 # maybe we also need to push the close_commit_id
830 # maybe we also need to push the close_commit_id
831 if close_commit_id:
831 if close_commit_id:
832 shadow_repo_with_hooks._local_push(
832 shadow_repo_with_hooks._local_push(
833 close_commit_id, self.path, push_branches=True,
833 close_commit_id, self.path, push_branches=True,
834 enable_hooks=True)
834 enable_hooks=True)
835 merge_succeeded = True
835 merge_succeeded = True
836 except RepositoryError:
836 except RepositoryError:
837 log.exception(
837 log.exception(
838 'Failure when doing local push from the shadow '
838 'Failure when doing local push from the shadow '
839 'repository to the target repository.')
839 'repository to the target repository.')
840 merge_succeeded = False
840 merge_succeeded = False
841 merge_failure_reason = MergeFailureReason.PUSH_FAILED
841 merge_failure_reason = MergeFailureReason.PUSH_FAILED
842 else:
842 else:
843 merge_succeeded = True
843 merge_succeeded = True
844 else:
844 else:
845 merge_succeeded = False
845 merge_succeeded = False
846
846
847 return MergeResponse(
847 return MergeResponse(
848 merge_possible, merge_succeeded, merge_ref, merge_failure_reason)
848 merge_possible, merge_succeeded, merge_ref, merge_failure_reason)
849
849
850 def _get_shadow_instance(
850 def _get_shadow_instance(
851 self, shadow_repository_path, enable_hooks=False):
851 self, shadow_repository_path, enable_hooks=False):
852 config = self.config.copy()
852 config = self.config.copy()
853 if not enable_hooks:
853 if not enable_hooks:
854 config.clear_section('hooks')
854 config.clear_section('hooks')
855 return MercurialRepository(shadow_repository_path, config)
855 return MercurialRepository(shadow_repository_path, config)
856
856
857 def _validate_pull_reference(self, reference):
857 def _validate_pull_reference(self, reference):
858 if not (reference.name in self.bookmarks or
858 if not (reference.name in self.bookmarks or
859 reference.name in self.branches or
859 reference.name in self.branches or
860 self.get_commit(reference.commit_id)):
860 self.get_commit(reference.commit_id)):
861 raise CommitDoesNotExistError(
861 raise CommitDoesNotExistError(
862 'Unknown branch, bookmark or commit id')
862 'Unknown branch, bookmark or commit id')
863
863
864 def _local_pull(self, repository_path, reference):
864 def _local_pull(self, repository_path, reference):
865 """
865 """
866 Fetch a branch, bookmark or commit from a local repository.
866 Fetch a branch, bookmark or commit from a local repository.
867 """
867 """
868 repository_path = os.path.abspath(repository_path)
868 repository_path = os.path.abspath(repository_path)
869 if repository_path == self.path:
869 if repository_path == self.path:
870 raise ValueError('Cannot pull from the same repository')
870 raise ValueError('Cannot pull from the same repository')
871
871
872 reference_type_to_option_name = {
872 reference_type_to_option_name = {
873 'book': 'bookmark',
873 'book': 'bookmark',
874 'branch': 'branch',
874 'branch': 'branch',
875 }
875 }
876 option_name = reference_type_to_option_name.get(
876 option_name = reference_type_to_option_name.get(
877 reference.type, 'revision')
877 reference.type, 'revision')
878
878
879 if option_name == 'revision':
879 if option_name == 'revision':
880 ref = reference.commit_id
880 ref = reference.commit_id
881 else:
881 else:
882 ref = reference.name
882 ref = reference.name
883
883
884 options = {option_name: [ref]}
884 options = {option_name: [ref]}
885 self._remote.pull_cmd(repository_path, hooks=False, **options)
885 self._remote.pull_cmd(repository_path, hooks=False, **options)
886 self._remote.invalidate_vcs_cache()
886 self._remote.invalidate_vcs_cache()
887
887
888 def bookmark(self, bookmark, revision=None):
888 def bookmark(self, bookmark, revision=None):
889 if isinstance(bookmark, unicode):
889 if isinstance(bookmark, unicode):
890 bookmark = safe_str(bookmark)
890 bookmark = safe_str(bookmark)
891 self._remote.bookmark(bookmark, revision=revision)
891 self._remote.bookmark(bookmark, revision=revision)
892 self._remote.invalidate_vcs_cache()
892 self._remote.invalidate_vcs_cache()
893
893
894 def get_path_permissions(self, username):
894 def get_path_permissions(self, username):
895 hgacl_file = self.path + '/.hg/hgacl'
895 hgacl_file = self.path + '/.hg/hgacl'
896 if os.path.exists(hgacl_file):
896 if os.path.exists(hgacl_file):
897 hgacl = ConfigParser.RawConfigParser()
897 try:
898 hgacl.read(hgacl_file)
898 hgacl = ConfigParser.RawConfigParser()
899 def read_patterns(suffix):
899 hgacl.read(hgacl_file)
900 svalue = None
900 def read_patterns(suffix):
901 try:
901 svalue = None
902 svalue = hgacl.get('narrowhgacl', username + suffix)
903 except ConfigParser.NoOptionError:
904 try:
902 try:
905 svalue = hgacl.get('narrowhgacl', 'default' + suffix)
903 svalue = hgacl.get('narrowhgacl', username + suffix)
906 except ConfigParser.NoOptionError:
904 except ConfigParser.NoOptionError:
907 pass
905 try:
908 if not svalue:
906 svalue = hgacl.get('narrowhgacl', 'default' + suffix)
909 return None
907 except ConfigParser.NoOptionError:
910 result = ['/']
908 pass
911 for pattern in svalue.split():
909 if not svalue:
912 result.append(pattern)
910 return None
913 if '*' not in pattern and '?' not in pattern:
911 result = ['/']
914 result.append(pattern + '/*')
912 for pattern in svalue.split():
915 return result
913 result.append(pattern)
916 includes = read_patterns('.includes')
914 if '*' not in pattern and '?' not in pattern:
917 excludes = read_patterns('.excludes')
915 result.append(pattern + '/*')
918 return BasePathPermissionChecker.create_from_patterns(includes, excludes)
916 return result
917 includes = read_patterns('.includes')
918 excludes = read_patterns('.excludes')
919 return BasePathPermissionChecker.create_from_patterns(includes, excludes)
920 except BaseException as e:
921 raise exceptions.RepositoryRequirementError('Cannot read ACL settings for {}: {}'.format(self.name, e))
919 else:
922 else:
920 return None
923 return None
921
924
922 class MercurialIndexBasedCollectionGenerator(CollectionGenerator):
925 class MercurialIndexBasedCollectionGenerator(CollectionGenerator):
923
926
924 def _commit_factory(self, commit_id):
927 def _commit_factory(self, commit_id):
925 return self.repo.get_commit(
928 return self.repo.get_commit(
926 commit_idx=commit_id, pre_load=self.pre_load)
929 commit_idx=commit_id, pre_load=self.pre_load)
General Comments 0
You need to be logged in to leave comments. Login now