##// END OF EJS Templates
mercurial: updated logging and function calls.
marcink -
r2058:7385e055 default
parent child Browse files
Show More
@@ -1,875 +1,882 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):
270 def verify(self):
271 verify = self._remote.verify()
271 verify = self._remote.verify()
272
272
273 self._remote.invalidate_vcs_cache()
273 self._remote.invalidate_vcs_cache()
274 return verify
274 return verify
275
275
276 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
276 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
277 if commit_id1 == commit_id2:
277 if commit_id1 == commit_id2:
278 return commit_id1
278 return commit_id1
279
279
280 ancestors = self._remote.revs_from_revspec(
280 ancestors = self._remote.revs_from_revspec(
281 "ancestor(id(%s), id(%s))", commit_id1, commit_id2,
281 "ancestor(id(%s), id(%s))", commit_id1, commit_id2,
282 other_path=repo2.path)
282 other_path=repo2.path)
283 return repo2[ancestors[0]].raw_id if ancestors else None
283 return repo2[ancestors[0]].raw_id if ancestors else None
284
284
285 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):
286 if commit_id1 == commit_id2:
286 if commit_id1 == commit_id2:
287 commits = []
287 commits = []
288 else:
288 else:
289 if merge:
289 if merge:
290 indexes = self._remote.revs_from_revspec(
290 indexes = self._remote.revs_from_revspec(
291 "ancestors(id(%s)) - ancestors(id(%s)) - id(%s)",
291 "ancestors(id(%s)) - ancestors(id(%s)) - id(%s)",
292 commit_id2, commit_id1, commit_id1, other_path=repo2.path)
292 commit_id2, commit_id1, commit_id1, other_path=repo2.path)
293 else:
293 else:
294 indexes = self._remote.revs_from_revspec(
294 indexes = self._remote.revs_from_revspec(
295 "id(%s)..id(%s) - id(%s)", commit_id1, commit_id2,
295 "id(%s)..id(%s) - id(%s)", commit_id1, commit_id2,
296 commit_id1, other_path=repo2.path)
296 commit_id1, other_path=repo2.path)
297
297
298 commits = [repo2.get_commit(commit_idx=idx, pre_load=pre_load)
298 commits = [repo2.get_commit(commit_idx=idx, pre_load=pre_load)
299 for idx in indexes]
299 for idx in indexes]
300
300
301 return commits
301 return commits
302
302
303 @staticmethod
303 @staticmethod
304 def check_url(url, config):
304 def check_url(url, config):
305 """
305 """
306 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
307 link. Sometimes it may happened that mercurial will issue basic
307 link. Sometimes it may happened that mercurial will issue basic
308 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
309 or other external calls.
309 or other external calls.
310
310
311 On failures it'll raise urllib2.HTTPError, exception is also thrown
311 On failures it'll raise urllib2.HTTPError, exception is also thrown
312 when the return code is non 200
312 when the return code is non 200
313 """
313 """
314 # check first if it's not an local url
314 # check first if it's not an local url
315 if os.path.isdir(url) or url.startswith('file:'):
315 if os.path.isdir(url) or url.startswith('file:'):
316 return True
316 return True
317
317
318 # Request the _remote to verify the url
318 # Request the _remote to verify the url
319 return connection.Hg.check_url(url, config.serialize())
319 return connection.Hg.check_url(url, config.serialize())
320
320
321 @staticmethod
321 @staticmethod
322 def is_valid_repository(path):
322 def is_valid_repository(path):
323 return os.path.isdir(os.path.join(path, '.hg'))
323 return os.path.isdir(os.path.join(path, '.hg'))
324
324
325 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):
326 """
326 """
327 Function will check for mercurial repository in given path. If there
327 Function will check for mercurial repository in given path. If there
328 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
329 `create` parameter is set to True - in that case repository would
329 `create` parameter is set to True - in that case repository would
330 be created.
330 be created.
331
331
332 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
333 location at given clone_point. Additionally it'll make update to
333 location at given clone_point. Additionally it'll make update to
334 working copy accordingly to `update_after_clone` flag.
334 working copy accordingly to `update_after_clone` flag.
335 """
335 """
336 if create and os.path.exists(self.path):
336 if create and os.path.exists(self.path):
337 raise RepositoryError(
337 raise RepositoryError(
338 "Cannot create repository at %s, location already exist"
338 "Cannot create repository at %s, location already exist"
339 % self.path)
339 % self.path)
340
340
341 if src_url:
341 if src_url:
342 url = str(self._get_url(src_url))
342 url = str(self._get_url(src_url))
343 MercurialRepository.check_url(url, self.config)
343 MercurialRepository.check_url(url, self.config)
344
344
345 self._remote.clone(url, self.path, update_after_clone)
345 self._remote.clone(url, self.path, update_after_clone)
346
346
347 # Don't try to create if we've already cloned repo
347 # Don't try to create if we've already cloned repo
348 create = False
348 create = False
349
349
350 if create:
350 if create:
351 os.makedirs(self.path, mode=0755)
351 os.makedirs(self.path, mode=0755)
352
352
353 self._remote.localrepository(create)
353 self._remote.localrepository(create)
354
354
355 @LazyProperty
355 @LazyProperty
356 def in_memory_commit(self):
356 def in_memory_commit(self):
357 return MercurialInMemoryCommit(self)
357 return MercurialInMemoryCommit(self)
358
358
359 @LazyProperty
359 @LazyProperty
360 def description(self):
360 def description(self):
361 description = self._remote.get_config_value(
361 description = self._remote.get_config_value(
362 'web', 'description', untrusted=True)
362 'web', 'description', untrusted=True)
363 return safe_unicode(description or self.DEFAULT_DESCRIPTION)
363 return safe_unicode(description or self.DEFAULT_DESCRIPTION)
364
364
365 @LazyProperty
365 @LazyProperty
366 def contact(self):
366 def contact(self):
367 contact = (
367 contact = (
368 self._remote.get_config_value("web", "contact") or
368 self._remote.get_config_value("web", "contact") or
369 self._remote.get_config_value("ui", "username"))
369 self._remote.get_config_value("ui", "username"))
370 return safe_unicode(contact or self.DEFAULT_CONTACT)
370 return safe_unicode(contact or self.DEFAULT_CONTACT)
371
371
372 @LazyProperty
372 @LazyProperty
373 def last_change(self):
373 def last_change(self):
374 """
374 """
375 Returns last change made on this repository as
375 Returns last change made on this repository as
376 `datetime.datetime` object.
376 `datetime.datetime` object.
377 """
377 """
378 try:
378 try:
379 return self.get_commit().date
379 return self.get_commit().date
380 except RepositoryError:
380 except RepositoryError:
381 tzoffset = makedate()[1]
381 tzoffset = makedate()[1]
382 return utcdate_fromtimestamp(self._get_fs_mtime(), tzoffset)
382 return utcdate_fromtimestamp(self._get_fs_mtime(), tzoffset)
383
383
384 def _get_fs_mtime(self):
384 def _get_fs_mtime(self):
385 # fallback to filesystem
385 # fallback to filesystem
386 cl_path = os.path.join(self.path, '.hg', "00changelog.i")
386 cl_path = os.path.join(self.path, '.hg', "00changelog.i")
387 st_path = os.path.join(self.path, '.hg', "store")
387 st_path = os.path.join(self.path, '.hg', "store")
388 if os.path.exists(cl_path):
388 if os.path.exists(cl_path):
389 return os.stat(cl_path).st_mtime
389 return os.stat(cl_path).st_mtime
390 else:
390 else:
391 return os.stat(st_path).st_mtime
391 return os.stat(st_path).st_mtime
392
392
393 def _sanitize_commit_idx(self, idx):
393 def _sanitize_commit_idx(self, idx):
394 # 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
395 # 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
396 # `int` to `long` here to make sure it is handled correctly.
396 # `int` to `long` here to make sure it is handled correctly.
397 if isinstance(idx, int):
397 if isinstance(idx, int):
398 return long(idx)
398 return long(idx)
399 return idx
399 return idx
400
400
401 def _get_url(self, url):
401 def _get_url(self, url):
402 """
402 """
403 Returns normalized url. If schema is not given, would fall
403 Returns normalized url. If schema is not given, would fall
404 to filesystem
404 to filesystem
405 (``file:///``) schema.
405 (``file:///``) schema.
406 """
406 """
407 url = url.encode('utf8')
407 url = url.encode('utf8')
408 if url != 'default' and '://' not in url:
408 if url != 'default' and '://' not in url:
409 url = "file:" + urllib.pathname2url(url)
409 url = "file:" + urllib.pathname2url(url)
410 return url
410 return url
411
411
412 def get_hook_location(self):
412 def get_hook_location(self):
413 """
413 """
414 returns absolute path to location where hooks are stored
414 returns absolute path to location where hooks are stored
415 """
415 """
416 return os.path.join(self.path, '.hg', '.hgrc')
416 return os.path.join(self.path, '.hg', '.hgrc')
417
417
418 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):
419 """
419 """
420 Returns ``MercurialCommit`` object representing repository's
420 Returns ``MercurialCommit`` object representing repository's
421 commit at the given `commit_id` or `commit_idx`.
421 commit at the given `commit_id` or `commit_idx`.
422 """
422 """
423 if self.is_empty():
423 if self.is_empty():
424 raise EmptyRepositoryError("There are no commits yet")
424 raise EmptyRepositoryError("There are no commits yet")
425
425
426 if commit_id is not None:
426 if commit_id is not None:
427 self._validate_commit_id(commit_id)
427 self._validate_commit_id(commit_id)
428 try:
428 try:
429 idx = self._commit_ids[commit_id]
429 idx = self._commit_ids[commit_id]
430 return MercurialCommit(self, commit_id, idx, pre_load=pre_load)
430 return MercurialCommit(self, commit_id, idx, pre_load=pre_load)
431 except KeyError:
431 except KeyError:
432 pass
432 pass
433 elif commit_idx is not None:
433 elif commit_idx is not None:
434 self._validate_commit_idx(commit_idx)
434 self._validate_commit_idx(commit_idx)
435 commit_idx = self._sanitize_commit_idx(commit_idx)
435 commit_idx = self._sanitize_commit_idx(commit_idx)
436 try:
436 try:
437 id_ = self.commit_ids[commit_idx]
437 id_ = self.commit_ids[commit_idx]
438 if commit_idx < 0:
438 if commit_idx < 0:
439 commit_idx += len(self.commit_ids)
439 commit_idx += len(self.commit_ids)
440 return MercurialCommit(
440 return MercurialCommit(
441 self, id_, commit_idx, pre_load=pre_load)
441 self, id_, commit_idx, pre_load=pre_load)
442 except IndexError:
442 except IndexError:
443 commit_id = commit_idx
443 commit_id = commit_idx
444 else:
444 else:
445 commit_id = "tip"
445 commit_id = "tip"
446
446
447 # TODO Paris: Ugly hack to "serialize" long for msgpack
447 # TODO Paris: Ugly hack to "serialize" long for msgpack
448 if isinstance(commit_id, long):
448 if isinstance(commit_id, long):
449 commit_id = float(commit_id)
449 commit_id = float(commit_id)
450
450
451 if isinstance(commit_id, unicode):
451 if isinstance(commit_id, unicode):
452 commit_id = safe_str(commit_id)
452 commit_id = safe_str(commit_id)
453
453
454 try:
454 try:
455 raw_id, idx = self._remote.lookup(commit_id, both=True)
455 raw_id, idx = self._remote.lookup(commit_id, both=True)
456 except CommitDoesNotExistError:
456 except CommitDoesNotExistError:
457 msg = "Commit %s does not exist for %s" % (
457 msg = "Commit %s does not exist for %s" % (
458 commit_id, self)
458 commit_id, self)
459 raise CommitDoesNotExistError(msg)
459 raise CommitDoesNotExistError(msg)
460
460
461 return MercurialCommit(self, raw_id, idx, pre_load=pre_load)
461 return MercurialCommit(self, raw_id, idx, pre_load=pre_load)
462
462
463 def get_commits(
463 def get_commits(
464 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,
465 branch_name=None, pre_load=None):
465 branch_name=None, pre_load=None):
466 """
466 """
467 Returns generator of ``MercurialCommit`` objects from start to end
467 Returns generator of ``MercurialCommit`` objects from start to end
468 (both are inclusive)
468 (both are inclusive)
469
469
470 :param start_id: None, str(commit_id)
470 :param start_id: None, str(commit_id)
471 :param end_id: None, str(commit_id)
471 :param end_id: None, str(commit_id)
472 :param start_date: if specified, commits with commit date less than
472 :param start_date: if specified, commits with commit date less than
473 ``start_date`` would be filtered out from returned set
473 ``start_date`` would be filtered out from returned set
474 :param end_date: if specified, commits with commit date greater than
474 :param end_date: if specified, commits with commit date greater than
475 ``end_date`` would be filtered out from returned set
475 ``end_date`` would be filtered out from returned set
476 :param branch_name: if specified, commits not reachable from given
476 :param branch_name: if specified, commits not reachable from given
477 branch would be filtered out from returned set
477 branch would be filtered out from returned set
478
478
479 :raise BranchDoesNotExistError: If given ``branch_name`` does not
479 :raise BranchDoesNotExistError: If given ``branch_name`` does not
480 exist.
480 exist.
481 :raise CommitDoesNotExistError: If commit for given ``start`` or
481 :raise CommitDoesNotExistError: If commit for given ``start`` or
482 ``end`` could not be found.
482 ``end`` could not be found.
483 """
483 """
484 # 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
485 branch_ancestors = False
485 branch_ancestors = False
486 if self.is_empty():
486 if self.is_empty():
487 raise EmptyRepositoryError("There are no commits yet")
487 raise EmptyRepositoryError("There are no commits yet")
488 self._validate_branch_name(branch_name)
488 self._validate_branch_name(branch_name)
489
489
490 if start_id is not None:
490 if start_id is not None:
491 self._validate_commit_id(start_id)
491 self._validate_commit_id(start_id)
492 c_start = self.get_commit(commit_id=start_id)
492 c_start = self.get_commit(commit_id=start_id)
493 start_pos = self._commit_ids[c_start.raw_id]
493 start_pos = self._commit_ids[c_start.raw_id]
494 else:
494 else:
495 start_pos = None
495 start_pos = None
496
496
497 if end_id is not None:
497 if end_id is not None:
498 self._validate_commit_id(end_id)
498 self._validate_commit_id(end_id)
499 c_end = self.get_commit(commit_id=end_id)
499 c_end = self.get_commit(commit_id=end_id)
500 end_pos = max(0, self._commit_ids[c_end.raw_id])
500 end_pos = max(0, self._commit_ids[c_end.raw_id])
501 else:
501 else:
502 end_pos = None
502 end_pos = None
503
503
504 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:
505 raise RepositoryError(
505 raise RepositoryError(
506 "Start commit '%s' cannot be after end commit '%s'" %
506 "Start commit '%s' cannot be after end commit '%s'" %
507 (start_id, end_id))
507 (start_id, end_id))
508
508
509 if end_pos is not None:
509 if end_pos is not None:
510 end_pos += 1
510 end_pos += 1
511
511
512 commit_filter = []
512 commit_filter = []
513 if branch_name and not branch_ancestors:
513 if branch_name and not branch_ancestors:
514 commit_filter.append('branch("%s")' % branch_name)
514 commit_filter.append('branch("%s")' % branch_name)
515 elif branch_name and branch_ancestors:
515 elif branch_name and branch_ancestors:
516 commit_filter.append('ancestors(branch("%s"))' % branch_name)
516 commit_filter.append('ancestors(branch("%s"))' % branch_name)
517 if start_date and not end_date:
517 if start_date and not end_date:
518 commit_filter.append('date(">%s")' % start_date)
518 commit_filter.append('date(">%s")' % start_date)
519 if end_date and not start_date:
519 if end_date and not start_date:
520 commit_filter.append('date("<%s")' % end_date)
520 commit_filter.append('date("<%s")' % end_date)
521 if start_date and end_date:
521 if start_date and end_date:
522 commit_filter.append(
522 commit_filter.append(
523 'date(">%s") and date("<%s")' % (start_date, end_date))
523 'date(">%s") and date("<%s")' % (start_date, end_date))
524
524
525 # TODO: johbo: Figure out a simpler way for this solution
525 # TODO: johbo: Figure out a simpler way for this solution
526 collection_generator = CollectionGenerator
526 collection_generator = CollectionGenerator
527 if commit_filter:
527 if commit_filter:
528 commit_filter = map(safe_str, commit_filter)
528 commit_filter = map(safe_str, commit_filter)
529 revisions = self._remote.rev_range(commit_filter)
529 revisions = self._remote.rev_range(commit_filter)
530 collection_generator = MercurialIndexBasedCollectionGenerator
530 collection_generator = MercurialIndexBasedCollectionGenerator
531 else:
531 else:
532 revisions = self.commit_ids
532 revisions = self.commit_ids
533
533
534 if start_pos or end_pos:
534 if start_pos or end_pos:
535 revisions = revisions[start_pos:end_pos]
535 revisions = revisions[start_pos:end_pos]
536
536
537 return collection_generator(self, revisions, pre_load=pre_load)
537 return collection_generator(self, revisions, pre_load=pre_load)
538
538
539 def pull(self, url, commit_ids=None):
539 def pull(self, url, commit_ids=None):
540 """
540 """
541 Tries to pull changes from external location.
541 Tries to pull changes from external location.
542
542
543 :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
544 which shall be pulled from the other repository.
544 which shall be pulled from the other repository.
545 """
545 """
546 url = self._get_url(url)
546 url = self._get_url(url)
547 self._remote.pull(url, commit_ids=commit_ids)
547 self._remote.pull(url, commit_ids=commit_ids)
548 self._remote.invalidate_vcs_cache()
548 self._remote.invalidate_vcs_cache()
549
549
550 def _local_clone(self, clone_path):
550 def _local_clone(self, clone_path):
551 """
551 """
552 Create a local clone of the current repo.
552 Create a local clone of the current repo.
553 """
553 """
554 self._remote.clone(self.path, clone_path, update_after_clone=True,
554 self._remote.clone(self.path, clone_path, update_after_clone=True,
555 hooks=False)
555 hooks=False)
556
556
557 def _update(self, revision, clean=False):
557 def _update(self, revision, clean=False):
558 """
558 """
559 Update the working copty to the specified revision.
559 Update the working copy to the specified revision.
560 """
560 """
561 log.debug('Doing checkout to commit: `%s` for %s', revision, self)
561 log.debug('Doing checkout to commit: `%s` for %s', revision, self)
562 self._remote.update(revision, clean=clean)
562 self._remote.update(revision, clean=clean)
563
563
564 def _identify(self):
564 def _identify(self):
565 """
565 """
566 Return the current state of the working directory.
566 Return the current state of the working directory.
567 """
567 """
568 return self._remote.identify().strip().rstrip('+')
568 return self._remote.identify().strip().rstrip('+')
569
569
570 def _heads(self, branch=None):
570 def _heads(self, branch=None):
571 """
571 """
572 Return the commit ids of the repository heads.
572 Return the commit ids of the repository heads.
573 """
573 """
574 return self._remote.heads(branch=branch).strip().split(' ')
574 return self._remote.heads(branch=branch).strip().split(' ')
575
575
576 def _ancestor(self, revision1, revision2):
576 def _ancestor(self, revision1, revision2):
577 """
577 """
578 Return the common ancestor of the two revisions.
578 Return the common ancestor of the two revisions.
579 """
579 """
580 return self._remote.ancestor(revision1, revision2)
580 return self._remote.ancestor(revision1, revision2)
581
581
582 def _local_push(
582 def _local_push(
583 self, revision, repository_path, push_branches=False,
583 self, revision, repository_path, push_branches=False,
584 enable_hooks=False):
584 enable_hooks=False):
585 """
585 """
586 Push the given revision to the specified repository.
586 Push the given revision to the specified repository.
587
587
588 :param push_branches: allow to create branches in the target repo.
588 :param push_branches: allow to create branches in the target repo.
589 """
589 """
590 self._remote.push(
590 self._remote.push(
591 [revision], repository_path, hooks=enable_hooks,
591 [revision], repository_path, hooks=enable_hooks,
592 push_branches=push_branches)
592 push_branches=push_branches)
593
593
594 def _local_merge(self, target_ref, merge_message, user_name, user_email,
594 def _local_merge(self, target_ref, merge_message, user_name, user_email,
595 source_ref, use_rebase=False):
595 source_ref, use_rebase=False, dry_run=False):
596 """
596 """
597 Merge the given source_revision into the checked out revision.
597 Merge the given source_revision into the checked out revision.
598
598
599 Returns the commit id of the merge and a boolean indicating if the
599 Returns the commit id of the merge and a boolean indicating if the
600 commit needs to be pushed.
600 commit needs to be pushed.
601 """
601 """
602 self._update(target_ref.commit_id)
602 self._update(target_ref.commit_id)
603
603
604 ancestor = self._ancestor(target_ref.commit_id, source_ref.commit_id)
604 ancestor = self._ancestor(target_ref.commit_id, source_ref.commit_id)
605 is_the_same_branch = self._is_the_same_branch(target_ref, source_ref)
605 is_the_same_branch = self._is_the_same_branch(target_ref, source_ref)
606
606
607 if ancestor == source_ref.commit_id:
607 if ancestor == source_ref.commit_id:
608 # Nothing to do, the changes were already integrated
608 # Nothing to do, the changes were already integrated
609 return target_ref.commit_id, False
609 return target_ref.commit_id, False
610
610
611 elif ancestor == target_ref.commit_id and is_the_same_branch:
611 elif ancestor == target_ref.commit_id and is_the_same_branch:
612 # In this case we should force a commit message
612 # In this case we should force a commit message
613 return source_ref.commit_id, True
613 return source_ref.commit_id, True
614
614
615 if use_rebase:
615 if use_rebase:
616 try:
616 try:
617 bookmark_name = 'rcbook%s%s' % (source_ref.commit_id,
617 bookmark_name = 'rcbook%s%s' % (source_ref.commit_id,
618 target_ref.commit_id)
618 target_ref.commit_id)
619 self.bookmark(bookmark_name, revision=source_ref.commit_id)
619 self.bookmark(bookmark_name, revision=source_ref.commit_id)
620 self._remote.rebase(
620 self._remote.rebase(
621 source=source_ref.commit_id, dest=target_ref.commit_id)
621 source=source_ref.commit_id, dest=target_ref.commit_id)
622 self._remote.invalidate_vcs_cache()
622 self._remote.invalidate_vcs_cache()
623 self._update(bookmark_name)
623 self._update(bookmark_name)
624 return self._identify(), True
624 return self._identify(), True
625 except RepositoryError:
625 except RepositoryError:
626 # The rebase-abort may raise another exception which 'hides'
626 # The rebase-abort may raise another exception which 'hides'
627 # the original one, therefore we log it here.
627 # the original one, therefore we log it here.
628 log.exception('Error while rebasing shadow repo during merge.')
628 log.exception('Error while rebasing shadow repo during merge.')
629
629
630 # Cleanup any rebase leftovers
630 # Cleanup any rebase leftovers
631 self._remote.invalidate_vcs_cache()
631 self._remote.invalidate_vcs_cache()
632 self._remote.rebase(abort=True)
632 self._remote.rebase(abort=True)
633 self._remote.invalidate_vcs_cache()
633 self._remote.invalidate_vcs_cache()
634 self._remote.update(clean=True)
634 self._remote.update(clean=True)
635 raise
635 raise
636 else:
636 else:
637 try:
637 try:
638 self._remote.merge(source_ref.commit_id)
638 self._remote.merge(source_ref.commit_id)
639 self._remote.invalidate_vcs_cache()
639 self._remote.invalidate_vcs_cache()
640 self._remote.commit(
640 self._remote.commit(
641 message=safe_str(merge_message),
641 message=safe_str(merge_message),
642 username=safe_str('%s <%s>' % (user_name, user_email)))
642 username=safe_str('%s <%s>' % (user_name, user_email)))
643 self._remote.invalidate_vcs_cache()
643 self._remote.invalidate_vcs_cache()
644 return self._identify(), True
644 return self._identify(), True
645 except RepositoryError:
645 except RepositoryError:
646 # Cleanup any merge leftovers
646 # Cleanup any merge leftovers
647 self._remote.update(clean=True)
647 self._remote.update(clean=True)
648 raise
648 raise
649
649
650 def _local_close(self, target_ref, user_name, user_email,
650 def _local_close(self, target_ref, user_name, user_email,
651 source_ref, close_message=''):
651 source_ref, close_message=''):
652 """
652 """
653 Close the branch of the given source_revision
653 Close the branch of the given source_revision
654
654
655 Returns the commit id of the close and a boolean indicating if the
655 Returns the commit id of the close and a boolean indicating if the
656 commit needs to be pushed.
656 commit needs to be pushed.
657 """
657 """
658 self._update(source_ref.commit_id)
658 self._update(source_ref.commit_id)
659 message = close_message or "Closing branch: `{}`".format(source_ref.name)
659 message = close_message or "Closing branch: `{}`".format(source_ref.name)
660 try:
660 try:
661 self._remote.commit(
661 self._remote.commit(
662 message=safe_str(message),
662 message=safe_str(message),
663 username=safe_str('%s <%s>' % (user_name, user_email)),
663 username=safe_str('%s <%s>' % (user_name, user_email)),
664 close_branch=True)
664 close_branch=True)
665 self._remote.invalidate_vcs_cache()
665 self._remote.invalidate_vcs_cache()
666 return self._identify(), True
666 return self._identify(), True
667 except RepositoryError:
667 except RepositoryError:
668 # Cleanup any commit leftovers
668 # Cleanup any commit leftovers
669 self._remote.update(clean=True)
669 self._remote.update(clean=True)
670 raise
670 raise
671
671
672 def _is_the_same_branch(self, target_ref, source_ref):
672 def _is_the_same_branch(self, target_ref, source_ref):
673 return (
673 return (
674 self._get_branch_name(target_ref) ==
674 self._get_branch_name(target_ref) ==
675 self._get_branch_name(source_ref))
675 self._get_branch_name(source_ref))
676
676
677 def _get_branch_name(self, ref):
677 def _get_branch_name(self, ref):
678 if ref.type == 'branch':
678 if ref.type == 'branch':
679 return ref.name
679 return ref.name
680 return self._remote.ctx_branch(ref.commit_id)
680 return self._remote.ctx_branch(ref.commit_id)
681
681
682 def _get_shadow_repository_path(self, workspace_id):
682 def _get_shadow_repository_path(self, workspace_id):
683 # The name of the shadow repository must start with '.', so it is
683 # The name of the shadow repository must start with '.', so it is
684 # skipped by 'rhodecode.lib.utils.get_filesystem_repos'.
684 # skipped by 'rhodecode.lib.utils.get_filesystem_repos'.
685 return os.path.join(
685 return os.path.join(
686 os.path.dirname(self.path),
686 os.path.dirname(self.path),
687 '.__shadow_%s_%s' % (os.path.basename(self.path), workspace_id))
687 '.__shadow_%s_%s' % (os.path.basename(self.path), workspace_id))
688
688
689 def _maybe_prepare_merge_workspace(self, workspace_id, unused_target_ref):
689 def _maybe_prepare_merge_workspace(self, workspace_id, unused_target_ref):
690 shadow_repository_path = self._get_shadow_repository_path(workspace_id)
690 shadow_repository_path = self._get_shadow_repository_path(workspace_id)
691 if not os.path.exists(shadow_repository_path):
691 if not os.path.exists(shadow_repository_path):
692 self._local_clone(shadow_repository_path)
692 self._local_clone(shadow_repository_path)
693 log.debug(
693 log.debug(
694 'Prepared shadow repository in %s', shadow_repository_path)
694 'Prepared shadow repository in %s', shadow_repository_path)
695
695
696 return shadow_repository_path
696 return shadow_repository_path
697
697
698 def cleanup_merge_workspace(self, workspace_id):
698 def cleanup_merge_workspace(self, workspace_id):
699 shadow_repository_path = self._get_shadow_repository_path(workspace_id)
699 shadow_repository_path = self._get_shadow_repository_path(workspace_id)
700 shutil.rmtree(shadow_repository_path, ignore_errors=True)
700 shutil.rmtree(shadow_repository_path, ignore_errors=True)
701
701
702 def _merge_repo(self, shadow_repository_path, target_ref,
702 def _merge_repo(self, shadow_repository_path, target_ref,
703 source_repo, source_ref, merge_message,
703 source_repo, source_ref, merge_message,
704 merger_name, merger_email, dry_run=False,
704 merger_name, merger_email, dry_run=False,
705 use_rebase=False, close_branch=False):
705 use_rebase=False, close_branch=False):
706
707 log.debug('Executing merge_repo with %s strategy, dry_run mode:%s',
708 'rebase' if use_rebase else 'merge', dry_run)
706 if target_ref.commit_id not in self._heads():
709 if target_ref.commit_id not in self._heads():
707 return MergeResponse(
710 return MergeResponse(
708 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD)
711 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD)
709
712
710 try:
713 try:
711 if (target_ref.type == 'branch' and
714 if (target_ref.type == 'branch' and
712 len(self._heads(target_ref.name)) != 1):
715 len(self._heads(target_ref.name)) != 1):
713 return MergeResponse(
716 return MergeResponse(
714 False, False, None,
717 False, False, None,
715 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS)
718 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS)
716 except CommitDoesNotExistError as e:
719 except CommitDoesNotExistError:
717 log.exception('Failure when looking up branch heads on hg target')
720 log.exception('Failure when looking up branch heads on hg target')
718 return MergeResponse(
721 return MergeResponse(
719 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
722 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
720
723
721 shadow_repo = self._get_shadow_instance(shadow_repository_path)
724 shadow_repo = self._get_shadow_instance(shadow_repository_path)
722
725
723 log.debug('Pulling in target reference %s', target_ref)
726 log.debug('Pulling in target reference %s', target_ref)
724 self._validate_pull_reference(target_ref)
727 self._validate_pull_reference(target_ref)
725 shadow_repo._local_pull(self.path, target_ref)
728 shadow_repo._local_pull(self.path, target_ref)
726 try:
729 try:
727 log.debug('Pulling in source reference %s', source_ref)
730 log.debug('Pulling in source reference %s', source_ref)
728 source_repo._validate_pull_reference(source_ref)
731 source_repo._validate_pull_reference(source_ref)
729 shadow_repo._local_pull(source_repo.path, source_ref)
732 shadow_repo._local_pull(source_repo.path, source_ref)
730 except CommitDoesNotExistError:
733 except CommitDoesNotExistError:
731 log.exception('Failure when doing local pull on hg shadow repo')
734 log.exception('Failure when doing local pull on hg shadow repo')
732 return MergeResponse(
735 return MergeResponse(
733 False, False, None, MergeFailureReason.MISSING_SOURCE_REF)
736 False, False, None, MergeFailureReason.MISSING_SOURCE_REF)
734
737
735 merge_ref = None
738 merge_ref = None
736 merge_commit_id = None
739 merge_commit_id = None
737 close_commit_id = None
740 close_commit_id = None
738 merge_failure_reason = MergeFailureReason.NONE
741 merge_failure_reason = MergeFailureReason.NONE
739
742
740 # enforce that close branch should be used only in case we source from
743 # enforce that close branch should be used only in case we source from
741 # an actual Branch
744 # an actual Branch
742 close_branch = close_branch and source_ref.type == 'branch'
745 close_branch = close_branch and source_ref.type == 'branch'
743
746
744 # don't allow to close branch if source and target are the same
747 # don't allow to close branch if source and target are the same
745 close_branch = close_branch and source_ref.name != target_ref.name
748 close_branch = close_branch and source_ref.name != target_ref.name
746
749
747 needs_push_on_close = False
750 needs_push_on_close = False
748 if close_branch and not use_rebase and not dry_run:
751 if close_branch and not use_rebase and not dry_run:
749 try:
752 try:
750 close_commit_id, needs_push_on_close = shadow_repo._local_close(
753 close_commit_id, needs_push_on_close = shadow_repo._local_close(
751 target_ref, merger_name, merger_email, source_ref)
754 target_ref, merger_name, merger_email, source_ref)
752 merge_possible = True
755 merge_possible = True
753 except RepositoryError:
756 except RepositoryError:
754 log.exception(
757 log.exception(
755 'Failure when doing close branch on hg shadow repo')
758 'Failure when doing close branch on hg shadow repo')
756 merge_possible = False
759 merge_possible = False
757 merge_failure_reason = MergeFailureReason.MERGE_FAILED
760 merge_failure_reason = MergeFailureReason.MERGE_FAILED
758 else:
761 else:
759 merge_possible = True
762 merge_possible = True
760
763
761 if merge_possible:
764 if merge_possible:
762 try:
765 try:
763 merge_commit_id, needs_push = shadow_repo._local_merge(
766 merge_commit_id, needs_push = shadow_repo._local_merge(
764 target_ref, merge_message, merger_name, merger_email,
767 target_ref, merge_message, merger_name, merger_email,
765 source_ref, use_rebase=use_rebase)
768 source_ref, use_rebase=use_rebase, dry_run=dry_run)
766 merge_possible = True
769 merge_possible = True
767
770
768 # read the state of the close action, if it
771 # read the state of the close action, if it
769 # maybe required a push
772 # maybe required a push
770 needs_push = needs_push or needs_push_on_close
773 needs_push = needs_push or needs_push_on_close
771
774
772 # Set a bookmark pointing to the merge commit. This bookmark
775 # Set a bookmark pointing to the merge commit. This bookmark
773 # may be used to easily identify the last successful merge
776 # may be used to easily identify the last successful merge
774 # commit in the shadow repository.
777 # commit in the shadow repository.
775 shadow_repo.bookmark('pr-merge', revision=merge_commit_id)
778 shadow_repo.bookmark('pr-merge', revision=merge_commit_id)
776 merge_ref = Reference('book', 'pr-merge', merge_commit_id)
779 merge_ref = Reference('book', 'pr-merge', merge_commit_id)
777 except SubrepoMergeError:
780 except SubrepoMergeError:
778 log.exception(
781 log.exception(
779 'Subrepo merge error during local merge on hg shadow repo.')
782 'Subrepo merge error during local merge on hg shadow repo.')
780 merge_possible = False
783 merge_possible = False
781 merge_failure_reason = MergeFailureReason.SUBREPO_MERGE_FAILED
784 merge_failure_reason = MergeFailureReason.SUBREPO_MERGE_FAILED
785 needs_push = False
782 except RepositoryError:
786 except RepositoryError:
783 log.exception('Failure when doing local merge on hg shadow repo')
787 log.exception('Failure when doing local merge on hg shadow repo')
784 merge_possible = False
788 merge_possible = False
785 merge_failure_reason = MergeFailureReason.MERGE_FAILED
789 merge_failure_reason = MergeFailureReason.MERGE_FAILED
790 needs_push = False
786
791
787 if merge_possible and not dry_run:
792 if merge_possible and not dry_run:
788 if needs_push:
793 if needs_push:
789 # In case the target is a bookmark, update it, so after pushing
794 # In case the target is a bookmark, update it, so after pushing
790 # the bookmarks is also updated in the target.
795 # the bookmarks is also updated in the target.
791 if target_ref.type == 'book':
796 if target_ref.type == 'book':
792 shadow_repo.bookmark(
797 shadow_repo.bookmark(
793 target_ref.name, revision=merge_commit_id)
798 target_ref.name, revision=merge_commit_id)
794 try:
799 try:
795 shadow_repo_with_hooks = self._get_shadow_instance(
800 shadow_repo_with_hooks = self._get_shadow_instance(
796 shadow_repository_path,
801 shadow_repository_path,
797 enable_hooks=True)
802 enable_hooks=True)
803 # This is the actual merge action, we push from shadow
804 # into origin.
798 # Note: the push_branches option will push any new branch
805 # Note: the push_branches option will push any new branch
799 # defined in the source repository to the target. This may
806 # defined in the source repository to the target. This may
800 # be dangerous as branches are permanent in Mercurial.
807 # be dangerous as branches are permanent in Mercurial.
801 # This feature was requested in issue #441.
808 # This feature was requested in issue #441.
802 shadow_repo_with_hooks._local_push(
809 shadow_repo_with_hooks._local_push(
803 merge_commit_id, self.path, push_branches=True,
810 merge_commit_id, self.path, push_branches=True,
804 enable_hooks=True)
811 enable_hooks=True)
805
812
806 # maybe we also need to push the close_commit_id
813 # maybe we also need to push the close_commit_id
807 if close_commit_id:
814 if close_commit_id:
808 shadow_repo_with_hooks._local_push(
815 shadow_repo_with_hooks._local_push(
809 close_commit_id, self.path, push_branches=True,
816 close_commit_id, self.path, push_branches=True,
810 enable_hooks=True)
817 enable_hooks=True)
811 merge_succeeded = True
818 merge_succeeded = True
812 except RepositoryError:
819 except RepositoryError:
813 log.exception(
820 log.exception(
814 'Failure when doing local push from the shadow '
821 'Failure when doing local push from the shadow '
815 'repository to the target repository.')
822 'repository to the target repository.')
816 merge_succeeded = False
823 merge_succeeded = False
817 merge_failure_reason = MergeFailureReason.PUSH_FAILED
824 merge_failure_reason = MergeFailureReason.PUSH_FAILED
818 else:
825 else:
819 merge_succeeded = True
826 merge_succeeded = True
820 else:
827 else:
821 merge_succeeded = False
828 merge_succeeded = False
822
829
823 return MergeResponse(
830 return MergeResponse(
824 merge_possible, merge_succeeded, merge_ref, merge_failure_reason)
831 merge_possible, merge_succeeded, merge_ref, merge_failure_reason)
825
832
826 def _get_shadow_instance(
833 def _get_shadow_instance(
827 self, shadow_repository_path, enable_hooks=False):
834 self, shadow_repository_path, enable_hooks=False):
828 config = self.config.copy()
835 config = self.config.copy()
829 if not enable_hooks:
836 if not enable_hooks:
830 config.clear_section('hooks')
837 config.clear_section('hooks')
831 return MercurialRepository(shadow_repository_path, config)
838 return MercurialRepository(shadow_repository_path, config)
832
839
833 def _validate_pull_reference(self, reference):
840 def _validate_pull_reference(self, reference):
834 if not (reference.name in self.bookmarks or
841 if not (reference.name in self.bookmarks or
835 reference.name in self.branches or
842 reference.name in self.branches or
836 self.get_commit(reference.commit_id)):
843 self.get_commit(reference.commit_id)):
837 raise CommitDoesNotExistError(
844 raise CommitDoesNotExistError(
838 'Unknown branch, bookmark or commit id')
845 'Unknown branch, bookmark or commit id')
839
846
840 def _local_pull(self, repository_path, reference):
847 def _local_pull(self, repository_path, reference):
841 """
848 """
842 Fetch a branch, bookmark or commit from a local repository.
849 Fetch a branch, bookmark or commit from a local repository.
843 """
850 """
844 repository_path = os.path.abspath(repository_path)
851 repository_path = os.path.abspath(repository_path)
845 if repository_path == self.path:
852 if repository_path == self.path:
846 raise ValueError('Cannot pull from the same repository')
853 raise ValueError('Cannot pull from the same repository')
847
854
848 reference_type_to_option_name = {
855 reference_type_to_option_name = {
849 'book': 'bookmark',
856 'book': 'bookmark',
850 'branch': 'branch',
857 'branch': 'branch',
851 }
858 }
852 option_name = reference_type_to_option_name.get(
859 option_name = reference_type_to_option_name.get(
853 reference.type, 'revision')
860 reference.type, 'revision')
854
861
855 if option_name == 'revision':
862 if option_name == 'revision':
856 ref = reference.commit_id
863 ref = reference.commit_id
857 else:
864 else:
858 ref = reference.name
865 ref = reference.name
859
866
860 options = {option_name: [ref]}
867 options = {option_name: [ref]}
861 self._remote.pull_cmd(repository_path, hooks=False, **options)
868 self._remote.pull_cmd(repository_path, hooks=False, **options)
862 self._remote.invalidate_vcs_cache()
869 self._remote.invalidate_vcs_cache()
863
870
864 def bookmark(self, bookmark, revision=None):
871 def bookmark(self, bookmark, revision=None):
865 if isinstance(bookmark, unicode):
872 if isinstance(bookmark, unicode):
866 bookmark = safe_str(bookmark)
873 bookmark = safe_str(bookmark)
867 self._remote.bookmark(bookmark, revision=revision)
874 self._remote.bookmark(bookmark, revision=revision)
868 self._remote.invalidate_vcs_cache()
875 self._remote.invalidate_vcs_cache()
869
876
870
877
871 class MercurialIndexBasedCollectionGenerator(CollectionGenerator):
878 class MercurialIndexBasedCollectionGenerator(CollectionGenerator):
872
879
873 def _commit_factory(self, commit_id):
880 def _commit_factory(self, commit_id):
874 return self.repo.get_commit(
881 return self.repo.get_commit(
875 commit_idx=commit_id, pre_load=self.pre_load)
882 commit_idx=commit_id, pre_load=self.pre_load)
General Comments 0
You need to be logged in to leave comments. Login now