##// END OF EJS Templates
logging: log the original error when a merge failure occurs
dan -
r63:ce662a0f default
parent child Browse files
Show More
@@ -1,906 +1,910 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2014-2016 RhodeCode GmbH
3 # Copyright (C) 2014-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 GIT repository module
22 GIT repository module
23 """
23 """
24
24
25 import logging
25 import logging
26 import os
26 import os
27 import re
27 import re
28 import shutil
28 import shutil
29 import time
29 import time
30
30
31 from zope.cachedescriptors.property import Lazy as LazyProperty
31 from zope.cachedescriptors.property import Lazy as LazyProperty
32
32
33 from rhodecode.lib.compat import OrderedDict
33 from rhodecode.lib.compat import OrderedDict
34 from rhodecode.lib.datelib import makedate, date_fromtimestamp
34 from rhodecode.lib.datelib import makedate, date_fromtimestamp
35 from rhodecode.lib.utils import safe_unicode, safe_str
35 from rhodecode.lib.utils import safe_unicode, safe_str
36 from rhodecode.lib.vcs import connection, path as vcspath
36 from rhodecode.lib.vcs import connection, path as vcspath
37 from rhodecode.lib.vcs.backends.base import (
37 from rhodecode.lib.vcs.backends.base import (
38 BaseRepository, CollectionGenerator, Config, MergeResponse,
38 BaseRepository, CollectionGenerator, Config, MergeResponse,
39 MergeFailureReason)
39 MergeFailureReason)
40 from rhodecode.lib.vcs.backends.git.commit import GitCommit
40 from rhodecode.lib.vcs.backends.git.commit import GitCommit
41 from rhodecode.lib.vcs.backends.git.diff import GitDiff
41 from rhodecode.lib.vcs.backends.git.diff import GitDiff
42 from rhodecode.lib.vcs.backends.git.inmemory import GitInMemoryCommit
42 from rhodecode.lib.vcs.backends.git.inmemory import GitInMemoryCommit
43 from rhodecode.lib.vcs.conf import settings
43 from rhodecode.lib.vcs.conf import settings
44 from rhodecode.lib.vcs.exceptions import (
44 from rhodecode.lib.vcs.exceptions import (
45 CommitDoesNotExistError, EmptyRepositoryError,
45 CommitDoesNotExistError, EmptyRepositoryError,
46 RepositoryError, TagAlreadyExistError, TagDoesNotExistError, VCSError)
46 RepositoryError, TagAlreadyExistError, TagDoesNotExistError, VCSError)
47
47
48
48
49 SHA_PATTERN = re.compile(r'^[[0-9a-fA-F]{12}|[0-9a-fA-F]{40}]$')
49 SHA_PATTERN = re.compile(r'^[[0-9a-fA-F]{12}|[0-9a-fA-F]{40}]$')
50
50
51 log = logging.getLogger(__name__)
51 log = logging.getLogger(__name__)
52
52
53
53
54 class GitRepository(BaseRepository):
54 class GitRepository(BaseRepository):
55 """
55 """
56 Git repository backend.
56 Git repository backend.
57 """
57 """
58 DEFAULT_BRANCH_NAME = 'master'
58 DEFAULT_BRANCH_NAME = 'master'
59
59
60 contact = BaseRepository.DEFAULT_CONTACT
60 contact = BaseRepository.DEFAULT_CONTACT
61
61
62 def __init__(self, repo_path, config=None, create=False, src_url=None,
62 def __init__(self, repo_path, config=None, create=False, src_url=None,
63 update_after_clone=False, with_wire=None, bare=False):
63 update_after_clone=False, with_wire=None, bare=False):
64
64
65 self.path = safe_str(os.path.abspath(repo_path))
65 self.path = safe_str(os.path.abspath(repo_path))
66 self.config = config if config else Config()
66 self.config = config if config else Config()
67 self._remote = connection.Git(
67 self._remote = connection.Git(
68 self.path, self.config, with_wire=with_wire)
68 self.path, self.config, with_wire=with_wire)
69
69
70 self._init_repo(create, src_url, update_after_clone, bare)
70 self._init_repo(create, src_url, update_after_clone, bare)
71
71
72 # caches
72 # caches
73 self._commit_ids = {}
73 self._commit_ids = {}
74
74
75 self.bookmarks = {}
75 self.bookmarks = {}
76
76
77 @LazyProperty
77 @LazyProperty
78 def bare(self):
78 def bare(self):
79 return self._remote.bare()
79 return self._remote.bare()
80
80
81 @LazyProperty
81 @LazyProperty
82 def head(self):
82 def head(self):
83 return self._remote.head()
83 return self._remote.head()
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 commit ids from cache.
89 attribute allows external tools to inject commit ids 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 def run_git_command(self, cmd, **opts):
99 def run_git_command(self, cmd, **opts):
100 """
100 """
101 Runs given ``cmd`` as git command and returns tuple
101 Runs given ``cmd`` as git command and returns tuple
102 (stdout, stderr).
102 (stdout, stderr).
103
103
104 :param cmd: git command to be executed
104 :param cmd: git command to be executed
105 :param opts: env options to pass into Subprocess command
105 :param opts: env options to pass into Subprocess command
106 """
106 """
107 if not isinstance(cmd, list):
107 if not isinstance(cmd, list):
108 raise ValueError('cmd must be a list, got %s instead' % type(cmd))
108 raise ValueError('cmd must be a list, got %s instead' % type(cmd))
109
109
110 out, err = self._remote.run_git_command(cmd, **opts)
110 out, err = self._remote.run_git_command(cmd, **opts)
111 log.debug('Stderr output of git command "%s":\n%s', cmd, err)
111 log.debug('Stderr output of git command "%s":\n%s', cmd, err)
112 return out, err
112 return out, err
113
113
114 @staticmethod
114 @staticmethod
115 def check_url(url, config):
115 def check_url(url, config):
116 """
116 """
117 Function will check given url and try to verify if it's a valid
117 Function will check given url and try to verify if it's a valid
118 link. Sometimes it may happened that git will issue basic
118 link. Sometimes it may happened that git will issue basic
119 auth request that can cause whole API to hang when used from python
119 auth request that can cause whole API to hang when used from python
120 or other external calls.
120 or other external calls.
121
121
122 On failures it'll raise urllib2.HTTPError, exception is also thrown
122 On failures it'll raise urllib2.HTTPError, exception is also thrown
123 when the return code is non 200
123 when the return code is non 200
124 """
124 """
125 # check first if it's not an url
125 # check first if it's not an url
126 if os.path.isdir(url) or url.startswith('file:'):
126 if os.path.isdir(url) or url.startswith('file:'):
127 return True
127 return True
128
128
129 if '+' in url.split('://', 1)[0]:
129 if '+' in url.split('://', 1)[0]:
130 url = url.split('+', 1)[1]
130 url = url.split('+', 1)[1]
131
131
132 # Request the _remote to verify the url
132 # Request the _remote to verify the url
133 return connection.Git.check_url(url, config.serialize())
133 return connection.Git.check_url(url, config.serialize())
134
134
135 @staticmethod
135 @staticmethod
136 def is_valid_repository(path):
136 def is_valid_repository(path):
137 if os.path.isdir(os.path.join(path, '.git')):
137 if os.path.isdir(os.path.join(path, '.git')):
138 return True
138 return True
139 # check case of bare repository
139 # check case of bare repository
140 try:
140 try:
141 GitRepository(path)
141 GitRepository(path)
142 return True
142 return True
143 except VCSError:
143 except VCSError:
144 pass
144 pass
145 return False
145 return False
146
146
147 def _init_repo(self, create, src_url=None, update_after_clone=False,
147 def _init_repo(self, create, src_url=None, update_after_clone=False,
148 bare=False):
148 bare=False):
149 if create and os.path.exists(self.path):
149 if create and os.path.exists(self.path):
150 raise RepositoryError(
150 raise RepositoryError(
151 "Cannot create repository at %s, location already exist"
151 "Cannot create repository at %s, location already exist"
152 % self.path)
152 % self.path)
153
153
154 try:
154 try:
155 if create and src_url:
155 if create and src_url:
156 GitRepository.check_url(src_url, self.config)
156 GitRepository.check_url(src_url, self.config)
157 self.clone(src_url, update_after_clone, bare)
157 self.clone(src_url, update_after_clone, bare)
158 elif create:
158 elif create:
159 os.makedirs(self.path, mode=0755)
159 os.makedirs(self.path, mode=0755)
160
160
161 if bare:
161 if bare:
162 self._remote.init_bare()
162 self._remote.init_bare()
163 else:
163 else:
164 self._remote.init()
164 self._remote.init()
165 else:
165 else:
166 self._remote.assert_correct_path()
166 self._remote.assert_correct_path()
167 # TODO: johbo: check if we have to translate the OSError here
167 # TODO: johbo: check if we have to translate the OSError here
168 except OSError as err:
168 except OSError as err:
169 raise RepositoryError(err)
169 raise RepositoryError(err)
170
170
171 def _get_all_commit_ids(self, filters=None):
171 def _get_all_commit_ids(self, filters=None):
172 # we must check if this repo is not empty, since later command
172 # we must check if this repo is not empty, since later command
173 # fails if it is. And it's cheaper to ask than throw the subprocess
173 # fails if it is. And it's cheaper to ask than throw the subprocess
174 # errors
174 # errors
175 try:
175 try:
176 self._remote.head()
176 self._remote.head()
177 except KeyError:
177 except KeyError:
178 return []
178 return []
179
179
180 rev_filter = ['--branches', '--tags']
180 rev_filter = ['--branches', '--tags']
181 extra_filter = []
181 extra_filter = []
182
182
183 if filters:
183 if filters:
184 if filters.get('since'):
184 if filters.get('since'):
185 extra_filter.append('--since=%s' % (filters['since']))
185 extra_filter.append('--since=%s' % (filters['since']))
186 if filters.get('until'):
186 if filters.get('until'):
187 extra_filter.append('--until=%s' % (filters['until']))
187 extra_filter.append('--until=%s' % (filters['until']))
188 if filters.get('branch_name'):
188 if filters.get('branch_name'):
189 rev_filter = ['--tags']
189 rev_filter = ['--tags']
190 extra_filter.append(filters['branch_name'])
190 extra_filter.append(filters['branch_name'])
191 rev_filter.extend(extra_filter)
191 rev_filter.extend(extra_filter)
192
192
193 # if filters.get('start') or filters.get('end'):
193 # if filters.get('start') or filters.get('end'):
194 # # skip is offset, max-count is limit
194 # # skip is offset, max-count is limit
195 # if filters.get('start'):
195 # if filters.get('start'):
196 # extra_filter += ' --skip=%s' % filters['start']
196 # extra_filter += ' --skip=%s' % filters['start']
197 # if filters.get('end'):
197 # if filters.get('end'):
198 # extra_filter += ' --max-count=%s' % (filters['end'] - (filters['start'] or 0))
198 # extra_filter += ' --max-count=%s' % (filters['end'] - (filters['start'] or 0))
199
199
200 cmd = ['rev-list', '--reverse', '--date-order'] + rev_filter
200 cmd = ['rev-list', '--reverse', '--date-order'] + rev_filter
201 try:
201 try:
202 output, __ = self.run_git_command(cmd)
202 output, __ = self.run_git_command(cmd)
203 except RepositoryError:
203 except RepositoryError:
204 # Can be raised for empty repositories
204 # Can be raised for empty repositories
205 return []
205 return []
206 return output.splitlines()
206 return output.splitlines()
207
207
208 def _get_all_commit_ids2(self):
208 def _get_all_commit_ids2(self):
209 # alternate implementation
209 # alternate implementation
210 includes = [x[1][0] for x in self._parsed_refs.iteritems()
210 includes = [x[1][0] for x in self._parsed_refs.iteritems()
211 if x[1][1] != 'T']
211 if x[1][1] != 'T']
212 return [c.commit.id for c in self._remote.get_walker(include=includes)]
212 return [c.commit.id for c in self._remote.get_walker(include=includes)]
213
213
214 def _get_commit_id(self, commit_id_or_idx):
214 def _get_commit_id(self, commit_id_or_idx):
215 def is_null(value):
215 def is_null(value):
216 return len(value) == commit_id_or_idx.count('0')
216 return len(value) == commit_id_or_idx.count('0')
217
217
218 if self.is_empty():
218 if self.is_empty():
219 raise EmptyRepositoryError("There are no commits yet")
219 raise EmptyRepositoryError("There are no commits yet")
220
220
221 if commit_id_or_idx in (None, '', 'tip', 'HEAD', 'head', -1):
221 if commit_id_or_idx in (None, '', 'tip', 'HEAD', 'head', -1):
222 return self.commit_ids[-1]
222 return self.commit_ids[-1]
223
223
224 is_bstr = isinstance(commit_id_or_idx, (str, unicode))
224 is_bstr = isinstance(commit_id_or_idx, (str, unicode))
225 if ((is_bstr and commit_id_or_idx.isdigit() and len(commit_id_or_idx) < 12)
225 if ((is_bstr and commit_id_or_idx.isdigit() and len(commit_id_or_idx) < 12)
226 or isinstance(commit_id_or_idx, int) or is_null(commit_id_or_idx)):
226 or isinstance(commit_id_or_idx, int) or is_null(commit_id_or_idx)):
227 try:
227 try:
228 commit_id_or_idx = self.commit_ids[int(commit_id_or_idx)]
228 commit_id_or_idx = self.commit_ids[int(commit_id_or_idx)]
229 except Exception:
229 except Exception:
230 msg = "Commit %s does not exist for %s" % (
230 msg = "Commit %s does not exist for %s" % (
231 commit_id_or_idx, self)
231 commit_id_or_idx, self)
232 raise CommitDoesNotExistError(msg)
232 raise CommitDoesNotExistError(msg)
233
233
234 elif is_bstr:
234 elif is_bstr:
235 # get by branch/tag name
235 # get by branch/tag name
236 ref_id = self._parsed_refs.get(commit_id_or_idx)
236 ref_id = self._parsed_refs.get(commit_id_or_idx)
237 if ref_id: # and ref_id[1] in ['H', 'RH', 'T']:
237 if ref_id: # and ref_id[1] in ['H', 'RH', 'T']:
238 return ref_id[0]
238 return ref_id[0]
239
239
240 tag_ids = self.tags.values()
240 tag_ids = self.tags.values()
241 # maybe it's a tag ? we don't have them in self.commit_ids
241 # maybe it's a tag ? we don't have them in self.commit_ids
242 if commit_id_or_idx in tag_ids:
242 if commit_id_or_idx in tag_ids:
243 return commit_id_or_idx
243 return commit_id_or_idx
244
244
245 elif (not SHA_PATTERN.match(commit_id_or_idx) or
245 elif (not SHA_PATTERN.match(commit_id_or_idx) or
246 commit_id_or_idx not in self.commit_ids):
246 commit_id_or_idx not in self.commit_ids):
247 msg = "Commit %s does not exist for %s" % (
247 msg = "Commit %s does not exist for %s" % (
248 commit_id_or_idx, self)
248 commit_id_or_idx, self)
249 raise CommitDoesNotExistError(msg)
249 raise CommitDoesNotExistError(msg)
250
250
251 # Ensure we return full id
251 # Ensure we return full id
252 if not SHA_PATTERN.match(str(commit_id_or_idx)):
252 if not SHA_PATTERN.match(str(commit_id_or_idx)):
253 raise CommitDoesNotExistError(
253 raise CommitDoesNotExistError(
254 "Given commit id %s not recognized" % commit_id_or_idx)
254 "Given commit id %s not recognized" % commit_id_or_idx)
255 return commit_id_or_idx
255 return commit_id_or_idx
256
256
257 def get_hook_location(self):
257 def get_hook_location(self):
258 """
258 """
259 returns absolute path to location where hooks are stored
259 returns absolute path to location where hooks are stored
260 """
260 """
261 loc = os.path.join(self.path, 'hooks')
261 loc = os.path.join(self.path, 'hooks')
262 if not self.bare:
262 if not self.bare:
263 loc = os.path.join(self.path, '.git', 'hooks')
263 loc = os.path.join(self.path, '.git', 'hooks')
264 return loc
264 return loc
265
265
266 @LazyProperty
266 @LazyProperty
267 def last_change(self):
267 def last_change(self):
268 """
268 """
269 Returns last change made on this repository as
269 Returns last change made on this repository as
270 `datetime.datetime` object.
270 `datetime.datetime` object.
271 """
271 """
272 return date_fromtimestamp(self._get_mtime(), makedate()[1])
272 return date_fromtimestamp(self._get_mtime(), makedate()[1])
273
273
274 def _get_mtime(self):
274 def _get_mtime(self):
275 try:
275 try:
276 return time.mktime(self.get_commit().date.timetuple())
276 return time.mktime(self.get_commit().date.timetuple())
277 except RepositoryError:
277 except RepositoryError:
278 idx_loc = '' if self.bare else '.git'
278 idx_loc = '' if self.bare else '.git'
279 # fallback to filesystem
279 # fallback to filesystem
280 in_path = os.path.join(self.path, idx_loc, "index")
280 in_path = os.path.join(self.path, idx_loc, "index")
281 he_path = os.path.join(self.path, idx_loc, "HEAD")
281 he_path = os.path.join(self.path, idx_loc, "HEAD")
282 if os.path.exists(in_path):
282 if os.path.exists(in_path):
283 return os.stat(in_path).st_mtime
283 return os.stat(in_path).st_mtime
284 else:
284 else:
285 return os.stat(he_path).st_mtime
285 return os.stat(he_path).st_mtime
286
286
287 @LazyProperty
287 @LazyProperty
288 def description(self):
288 def description(self):
289 description = self._remote.get_description()
289 description = self._remote.get_description()
290 return safe_unicode(description or self.DEFAULT_DESCRIPTION)
290 return safe_unicode(description or self.DEFAULT_DESCRIPTION)
291
291
292 def _get_refs_entry(self, value, reverse):
292 def _get_refs_entry(self, value, reverse):
293 if self.is_empty():
293 if self.is_empty():
294 return {}
294 return {}
295
295
296 def get_name(ctx):
296 def get_name(ctx):
297 return ctx[0]
297 return ctx[0]
298
298
299 _branches = [
299 _branches = [
300 (safe_unicode(x[0]), x[1][0])
300 (safe_unicode(x[0]), x[1][0])
301 for x in self._parsed_refs.iteritems() if x[1][1] == value]
301 for x in self._parsed_refs.iteritems() if x[1][1] == value]
302 return OrderedDict(sorted(_branches, key=get_name, reverse=reverse))
302 return OrderedDict(sorted(_branches, key=get_name, reverse=reverse))
303
303
304 def _get_branches(self):
304 def _get_branches(self):
305 return self._get_refs_entry('H', False)
305 return self._get_refs_entry('H', False)
306
306
307 @LazyProperty
307 @LazyProperty
308 def branches(self):
308 def branches(self):
309 return self._get_branches()
309 return self._get_branches()
310
310
311 @LazyProperty
311 @LazyProperty
312 def branches_closed(self):
312 def branches_closed(self):
313 return {}
313 return {}
314
314
315 @LazyProperty
315 @LazyProperty
316 def branches_all(self):
316 def branches_all(self):
317 all_branches = {}
317 all_branches = {}
318 all_branches.update(self.branches)
318 all_branches.update(self.branches)
319 all_branches.update(self.branches_closed)
319 all_branches.update(self.branches_closed)
320 return all_branches
320 return all_branches
321
321
322 @LazyProperty
322 @LazyProperty
323 def tags(self):
323 def tags(self):
324 return self._get_tags()
324 return self._get_tags()
325
325
326 def _get_tags(self):
326 def _get_tags(self):
327 return self._get_refs_entry('T', True)
327 return self._get_refs_entry('T', True)
328
328
329 def tag(self, name, user, commit_id=None, message=None, date=None,
329 def tag(self, name, user, commit_id=None, message=None, date=None,
330 **kwargs):
330 **kwargs):
331 """
331 """
332 Creates and returns a tag for the given ``commit_id``.
332 Creates and returns a tag for the given ``commit_id``.
333
333
334 :param name: name for new tag
334 :param name: name for new tag
335 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
335 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
336 :param commit_id: commit id for which new tag would be created
336 :param commit_id: commit id for which new tag would be created
337 :param message: message of the tag's commit
337 :param message: message of the tag's commit
338 :param date: date of tag's commit
338 :param date: date of tag's commit
339
339
340 :raises TagAlreadyExistError: if tag with same name already exists
340 :raises TagAlreadyExistError: if tag with same name already exists
341 """
341 """
342 if name in self.tags:
342 if name in self.tags:
343 raise TagAlreadyExistError("Tag %s already exists" % name)
343 raise TagAlreadyExistError("Tag %s already exists" % name)
344 commit = self.get_commit(commit_id=commit_id)
344 commit = self.get_commit(commit_id=commit_id)
345 message = message or "Added tag %s for commit %s" % (
345 message = message or "Added tag %s for commit %s" % (
346 name, commit.raw_id)
346 name, commit.raw_id)
347 self._remote.set_refs('refs/tags/%s' % name, commit._commit['id'])
347 self._remote.set_refs('refs/tags/%s' % name, commit._commit['id'])
348
348
349 self._parsed_refs = self._get_parsed_refs()
349 self._parsed_refs = self._get_parsed_refs()
350 self.tags = self._get_tags()
350 self.tags = self._get_tags()
351 return commit
351 return commit
352
352
353 def remove_tag(self, name, user, message=None, date=None):
353 def remove_tag(self, name, user, message=None, date=None):
354 """
354 """
355 Removes tag with the given ``name``.
355 Removes tag with the given ``name``.
356
356
357 :param name: name of the tag to be removed
357 :param name: name of the tag to be removed
358 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
358 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
359 :param message: message of the tag's removal commit
359 :param message: message of the tag's removal commit
360 :param date: date of tag's removal commit
360 :param date: date of tag's removal commit
361
361
362 :raises TagDoesNotExistError: if tag with given name does not exists
362 :raises TagDoesNotExistError: if tag with given name does not exists
363 """
363 """
364 if name not in self.tags:
364 if name not in self.tags:
365 raise TagDoesNotExistError("Tag %s does not exist" % name)
365 raise TagDoesNotExistError("Tag %s does not exist" % name)
366 tagpath = vcspath.join(
366 tagpath = vcspath.join(
367 self._remote.get_refs_path(), 'refs', 'tags', name)
367 self._remote.get_refs_path(), 'refs', 'tags', name)
368 try:
368 try:
369 os.remove(tagpath)
369 os.remove(tagpath)
370 self._parsed_refs = self._get_parsed_refs()
370 self._parsed_refs = self._get_parsed_refs()
371 self.tags = self._get_tags()
371 self.tags = self._get_tags()
372 except OSError as e:
372 except OSError as e:
373 raise RepositoryError(e.strerror)
373 raise RepositoryError(e.strerror)
374
374
375 @LazyProperty
375 @LazyProperty
376 def _parsed_refs(self):
376 def _parsed_refs(self):
377 return self._get_parsed_refs()
377 return self._get_parsed_refs()
378
378
379 def _get_parsed_refs(self):
379 def _get_parsed_refs(self):
380 # TODO: (oliver) who needs RH; branches?
380 # TODO: (oliver) who needs RH; branches?
381 # Remote Heads were commented out, as they may overwrite local branches
381 # Remote Heads were commented out, as they may overwrite local branches
382 # See the TODO note in rhodecode.lib.vcs.remote.git:get_refs for more
382 # See the TODO note in rhodecode.lib.vcs.remote.git:get_refs for more
383 # details.
383 # details.
384 keys = [('refs/heads/', 'H'),
384 keys = [('refs/heads/', 'H'),
385 #('refs/remotes/origin/', 'RH'),
385 #('refs/remotes/origin/', 'RH'),
386 ('refs/tags/', 'T')]
386 ('refs/tags/', 'T')]
387 return self._remote.get_refs(keys=keys)
387 return self._remote.get_refs(keys=keys)
388
388
389 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
389 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
390 """
390 """
391 Returns `GitCommit` object representing commit from git repository
391 Returns `GitCommit` object representing commit from git repository
392 at the given `commit_id` or head (most recent commit) if None given.
392 at the given `commit_id` or head (most recent commit) if None given.
393 """
393 """
394 if commit_id is not None:
394 if commit_id is not None:
395 self._validate_commit_id(commit_id)
395 self._validate_commit_id(commit_id)
396 elif commit_idx is not None:
396 elif commit_idx is not None:
397 self._validate_commit_idx(commit_idx)
397 self._validate_commit_idx(commit_idx)
398 commit_id = commit_idx
398 commit_id = commit_idx
399 commit_id = self._get_commit_id(commit_id)
399 commit_id = self._get_commit_id(commit_id)
400 try:
400 try:
401 # Need to call remote to translate id for tagging scenario
401 # Need to call remote to translate id for tagging scenario
402 commit_id = self._remote.get_object(commit_id)["commit_id"]
402 commit_id = self._remote.get_object(commit_id)["commit_id"]
403 idx = self._commit_ids[commit_id]
403 idx = self._commit_ids[commit_id]
404 except KeyError:
404 except KeyError:
405 raise RepositoryError("Cannot get object with id %s" % commit_id)
405 raise RepositoryError("Cannot get object with id %s" % commit_id)
406
406
407 return GitCommit(self, commit_id, idx, pre_load=pre_load)
407 return GitCommit(self, commit_id, idx, pre_load=pre_load)
408
408
409 def get_commits(
409 def get_commits(
410 self, start_id=None, end_id=None, start_date=None, end_date=None,
410 self, start_id=None, end_id=None, start_date=None, end_date=None,
411 branch_name=None, pre_load=None):
411 branch_name=None, pre_load=None):
412 """
412 """
413 Returns generator of `GitCommit` objects from start to end (both
413 Returns generator of `GitCommit` objects from start to end (both
414 are inclusive), in ascending date order.
414 are inclusive), in ascending date order.
415
415
416 :param start_id: None, str(commit_id)
416 :param start_id: None, str(commit_id)
417 :param end_id: None, str(commit_id)
417 :param end_id: None, str(commit_id)
418 :param start_date: if specified, commits with commit date less than
418 :param start_date: if specified, commits with commit date less than
419 ``start_date`` would be filtered out from returned set
419 ``start_date`` would be filtered out from returned set
420 :param end_date: if specified, commits with commit date greater than
420 :param end_date: if specified, commits with commit date greater than
421 ``end_date`` would be filtered out from returned set
421 ``end_date`` would be filtered out from returned set
422 :param branch_name: if specified, commits not reachable from given
422 :param branch_name: if specified, commits not reachable from given
423 branch would be filtered out from returned set
423 branch would be filtered out from returned set
424
424
425 :raise BranchDoesNotExistError: If given `branch_name` does not
425 :raise BranchDoesNotExistError: If given `branch_name` does not
426 exist.
426 exist.
427 :raise CommitDoesNotExistError: If commits for given `start` or
427 :raise CommitDoesNotExistError: If commits for given `start` or
428 `end` could not be found.
428 `end` could not be found.
429
429
430 """
430 """
431 if self.is_empty():
431 if self.is_empty():
432 raise EmptyRepositoryError("There are no commits yet")
432 raise EmptyRepositoryError("There are no commits yet")
433 self._validate_branch_name(branch_name)
433 self._validate_branch_name(branch_name)
434
434
435 if start_id is not None:
435 if start_id is not None:
436 self._validate_commit_id(start_id)
436 self._validate_commit_id(start_id)
437 if end_id is not None:
437 if end_id is not None:
438 self._validate_commit_id(end_id)
438 self._validate_commit_id(end_id)
439
439
440 start_raw_id = self._get_commit_id(start_id)
440 start_raw_id = self._get_commit_id(start_id)
441 start_pos = self._commit_ids[start_raw_id] if start_id else None
441 start_pos = self._commit_ids[start_raw_id] if start_id else None
442 end_raw_id = self._get_commit_id(end_id)
442 end_raw_id = self._get_commit_id(end_id)
443 end_pos = max(0, self._commit_ids[end_raw_id]) if end_id else None
443 end_pos = max(0, self._commit_ids[end_raw_id]) if end_id else None
444
444
445 if None not in [start_id, end_id] and start_pos > end_pos:
445 if None not in [start_id, end_id] and start_pos > end_pos:
446 raise RepositoryError(
446 raise RepositoryError(
447 "Start commit '%s' cannot be after end commit '%s'" %
447 "Start commit '%s' cannot be after end commit '%s'" %
448 (start_id, end_id))
448 (start_id, end_id))
449
449
450 if end_pos is not None:
450 if end_pos is not None:
451 end_pos += 1
451 end_pos += 1
452
452
453 filter_ = []
453 filter_ = []
454 if branch_name:
454 if branch_name:
455 filter_.append({'branch_name': branch_name})
455 filter_.append({'branch_name': branch_name})
456 if start_date and not end_date:
456 if start_date and not end_date:
457 filter_.append({'since': start_date})
457 filter_.append({'since': start_date})
458 if end_date and not start_date:
458 if end_date and not start_date:
459 filter_.append({'until': end_date})
459 filter_.append({'until': end_date})
460 if start_date and end_date:
460 if start_date and end_date:
461 filter_.append({'since': start_date})
461 filter_.append({'since': start_date})
462 filter_.append({'until': end_date})
462 filter_.append({'until': end_date})
463
463
464 # if start_pos or end_pos:
464 # if start_pos or end_pos:
465 # filter_.append({'start': start_pos})
465 # filter_.append({'start': start_pos})
466 # filter_.append({'end': end_pos})
466 # filter_.append({'end': end_pos})
467
467
468 if filter_:
468 if filter_:
469 revfilters = {
469 revfilters = {
470 'branch_name': branch_name,
470 'branch_name': branch_name,
471 'since': start_date.strftime('%m/%d/%y %H:%M:%S') if start_date else None,
471 'since': start_date.strftime('%m/%d/%y %H:%M:%S') if start_date else None,
472 'until': end_date.strftime('%m/%d/%y %H:%M:%S') if end_date else None,
472 'until': end_date.strftime('%m/%d/%y %H:%M:%S') if end_date else None,
473 'start': start_pos,
473 'start': start_pos,
474 'end': end_pos,
474 'end': end_pos,
475 }
475 }
476 commit_ids = self._get_all_commit_ids(filters=revfilters)
476 commit_ids = self._get_all_commit_ids(filters=revfilters)
477
477
478 # pure python stuff, it's slow due to walker walking whole repo
478 # pure python stuff, it's slow due to walker walking whole repo
479 # def get_revs(walker):
479 # def get_revs(walker):
480 # for walker_entry in walker:
480 # for walker_entry in walker:
481 # yield walker_entry.commit.id
481 # yield walker_entry.commit.id
482 # revfilters = {}
482 # revfilters = {}
483 # commit_ids = list(reversed(list(get_revs(self._repo.get_walker(**revfilters)))))
483 # commit_ids = list(reversed(list(get_revs(self._repo.get_walker(**revfilters)))))
484 else:
484 else:
485 commit_ids = self.commit_ids
485 commit_ids = self.commit_ids
486
486
487 if start_pos or end_pos:
487 if start_pos or end_pos:
488 commit_ids = commit_ids[start_pos: end_pos]
488 commit_ids = commit_ids[start_pos: end_pos]
489
489
490 return CollectionGenerator(self, commit_ids, pre_load=pre_load)
490 return CollectionGenerator(self, commit_ids, pre_load=pre_load)
491
491
492 def get_diff(
492 def get_diff(
493 self, commit1, commit2, path='', ignore_whitespace=False,
493 self, commit1, commit2, path='', ignore_whitespace=False,
494 context=3, path1=None):
494 context=3, path1=None):
495 """
495 """
496 Returns (git like) *diff*, as plain text. Shows changes introduced by
496 Returns (git like) *diff*, as plain text. Shows changes introduced by
497 ``commit2`` since ``commit1``.
497 ``commit2`` since ``commit1``.
498
498
499 :param commit1: Entry point from which diff is shown. Can be
499 :param commit1: Entry point from which diff is shown. Can be
500 ``self.EMPTY_COMMIT`` - in this case, patch showing all
500 ``self.EMPTY_COMMIT`` - in this case, patch showing all
501 the changes since empty state of the repository until ``commit2``
501 the changes since empty state of the repository until ``commit2``
502 :param commit2: Until which commits changes should be shown.
502 :param commit2: Until which commits changes should be shown.
503 :param ignore_whitespace: If set to ``True``, would not show whitespace
503 :param ignore_whitespace: If set to ``True``, would not show whitespace
504 changes. Defaults to ``False``.
504 changes. Defaults to ``False``.
505 :param context: How many lines before/after changed lines should be
505 :param context: How many lines before/after changed lines should be
506 shown. Defaults to ``3``.
506 shown. Defaults to ``3``.
507 """
507 """
508 self._validate_diff_commits(commit1, commit2)
508 self._validate_diff_commits(commit1, commit2)
509 if path1 is not None and path1 != path:
509 if path1 is not None and path1 != path:
510 raise ValueError("Diff of two different paths not supported.")
510 raise ValueError("Diff of two different paths not supported.")
511
511
512 flags = [
512 flags = [
513 '-U%s' % context, '--full-index', '--binary', '-p',
513 '-U%s' % context, '--full-index', '--binary', '-p',
514 '-M', '--abbrev=40']
514 '-M', '--abbrev=40']
515 if ignore_whitespace:
515 if ignore_whitespace:
516 flags.append('-w')
516 flags.append('-w')
517
517
518 if commit1 == self.EMPTY_COMMIT:
518 if commit1 == self.EMPTY_COMMIT:
519 cmd = ['show'] + flags + [commit2.raw_id]
519 cmd = ['show'] + flags + [commit2.raw_id]
520 else:
520 else:
521 cmd = ['diff'] + flags + [commit1.raw_id, commit2.raw_id]
521 cmd = ['diff'] + flags + [commit1.raw_id, commit2.raw_id]
522
522
523 if path:
523 if path:
524 cmd.extend(['--', path])
524 cmd.extend(['--', path])
525
525
526 stdout, __ = self.run_git_command(cmd)
526 stdout, __ = self.run_git_command(cmd)
527 # If we used 'show' command, strip first few lines (until actual diff
527 # If we used 'show' command, strip first few lines (until actual diff
528 # starts)
528 # starts)
529 if commit1 == self.EMPTY_COMMIT:
529 if commit1 == self.EMPTY_COMMIT:
530 lines = stdout.splitlines()
530 lines = stdout.splitlines()
531 x = 0
531 x = 0
532 for line in lines:
532 for line in lines:
533 if line.startswith('diff'):
533 if line.startswith('diff'):
534 break
534 break
535 x += 1
535 x += 1
536 # Append new line just like 'diff' command do
536 # Append new line just like 'diff' command do
537 stdout = '\n'.join(lines[x:]) + '\n'
537 stdout = '\n'.join(lines[x:]) + '\n'
538 return GitDiff(stdout)
538 return GitDiff(stdout)
539
539
540 def strip(self, commit_id, branch_name):
540 def strip(self, commit_id, branch_name):
541 commit = self.get_commit(commit_id=commit_id)
541 commit = self.get_commit(commit_id=commit_id)
542 if commit.merge:
542 if commit.merge:
543 raise Exception('Cannot reset to merge commit')
543 raise Exception('Cannot reset to merge commit')
544
544
545 # parent is going to be the new head now
545 # parent is going to be the new head now
546 commit = commit.parents[0]
546 commit = commit.parents[0]
547 self._remote.set_refs('refs/heads/%s' % branch_name, commit.raw_id)
547 self._remote.set_refs('refs/heads/%s' % branch_name, commit.raw_id)
548
548
549 self.commit_ids = self._get_all_commit_ids()
549 self.commit_ids = self._get_all_commit_ids()
550 self._rebuild_cache(self.commit_ids)
550 self._rebuild_cache(self.commit_ids)
551
551
552 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
552 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
553 if commit_id1 == commit_id2:
553 if commit_id1 == commit_id2:
554 return commit_id1
554 return commit_id1
555
555
556 if self != repo2:
556 if self != repo2:
557 commits = self._remote.get_missing_revs(
557 commits = self._remote.get_missing_revs(
558 commit_id1, commit_id2, repo2.path)
558 commit_id1, commit_id2, repo2.path)
559 if commits:
559 if commits:
560 commit = repo2.get_commit(commits[-1])
560 commit = repo2.get_commit(commits[-1])
561 if commit.parents:
561 if commit.parents:
562 ancestor_id = commit.parents[0].raw_id
562 ancestor_id = commit.parents[0].raw_id
563 else:
563 else:
564 ancestor_id = None
564 ancestor_id = None
565 else:
565 else:
566 # no commits from other repo, ancestor_id is the commit_id2
566 # no commits from other repo, ancestor_id is the commit_id2
567 ancestor_id = commit_id2
567 ancestor_id = commit_id2
568 else:
568 else:
569 output, __ = self.run_git_command(
569 output, __ = self.run_git_command(
570 ['merge-base', commit_id1, commit_id2])
570 ['merge-base', commit_id1, commit_id2])
571 ancestor_id = re.findall(r'[0-9a-fA-F]{40}', output)[0]
571 ancestor_id = re.findall(r'[0-9a-fA-F]{40}', output)[0]
572
572
573 return ancestor_id
573 return ancestor_id
574
574
575 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
575 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
576 repo1 = self
576 repo1 = self
577 ancestor_id = None
577 ancestor_id = None
578
578
579 if commit_id1 == commit_id2:
579 if commit_id1 == commit_id2:
580 commits = []
580 commits = []
581 elif repo1 != repo2:
581 elif repo1 != repo2:
582 missing_ids = self._remote.get_missing_revs(commit_id1, commit_id2,
582 missing_ids = self._remote.get_missing_revs(commit_id1, commit_id2,
583 repo2.path)
583 repo2.path)
584 commits = [
584 commits = [
585 repo2.get_commit(commit_id=commit_id, pre_load=pre_load)
585 repo2.get_commit(commit_id=commit_id, pre_load=pre_load)
586 for commit_id in reversed(missing_ids)]
586 for commit_id in reversed(missing_ids)]
587 else:
587 else:
588 output, __ = repo1.run_git_command(
588 output, __ = repo1.run_git_command(
589 ['log', '--reverse', '--pretty=format: %H', '-s',
589 ['log', '--reverse', '--pretty=format: %H', '-s',
590 '%s..%s' % (commit_id1, commit_id2)])
590 '%s..%s' % (commit_id1, commit_id2)])
591 commits = [
591 commits = [
592 repo1.get_commit(commit_id=commit_id, pre_load=pre_load)
592 repo1.get_commit(commit_id=commit_id, pre_load=pre_load)
593 for commit_id in re.findall(r'[0-9a-fA-F]{40}', output)]
593 for commit_id in re.findall(r'[0-9a-fA-F]{40}', output)]
594
594
595 return commits
595 return commits
596
596
597 @LazyProperty
597 @LazyProperty
598 def in_memory_commit(self):
598 def in_memory_commit(self):
599 """
599 """
600 Returns ``GitInMemoryCommit`` object for this repository.
600 Returns ``GitInMemoryCommit`` object for this repository.
601 """
601 """
602 return GitInMemoryCommit(self)
602 return GitInMemoryCommit(self)
603
603
604 def clone(self, url, update_after_clone=True, bare=False):
604 def clone(self, url, update_after_clone=True, bare=False):
605 """
605 """
606 Tries to clone commits from external location.
606 Tries to clone commits from external location.
607
607
608 :param update_after_clone: If set to ``False``, git won't checkout
608 :param update_after_clone: If set to ``False``, git won't checkout
609 working directory
609 working directory
610 :param bare: If set to ``True``, repository would be cloned into
610 :param bare: If set to ``True``, repository would be cloned into
611 *bare* git repository (no working directory at all).
611 *bare* git repository (no working directory at all).
612 """
612 """
613 # init_bare and init expect empty dir created to proceed
613 # init_bare and init expect empty dir created to proceed
614 if not os.path.exists(self.path):
614 if not os.path.exists(self.path):
615 os.mkdir(self.path)
615 os.mkdir(self.path)
616
616
617 if bare:
617 if bare:
618 self._remote.init_bare()
618 self._remote.init_bare()
619 else:
619 else:
620 self._remote.init()
620 self._remote.init()
621
621
622 deferred = '^{}'
622 deferred = '^{}'
623 valid_refs = ('refs/heads', 'refs/tags', 'HEAD')
623 valid_refs = ('refs/heads', 'refs/tags', 'HEAD')
624
624
625 return self._remote.clone(
625 return self._remote.clone(
626 url, deferred, valid_refs, update_after_clone)
626 url, deferred, valid_refs, update_after_clone)
627
627
628 def pull(self, url, commit_ids=None):
628 def pull(self, url, commit_ids=None):
629 """
629 """
630 Tries to pull changes from external location. We use fetch here since
630 Tries to pull changes from external location. We use fetch here since
631 pull in get does merges and we want to be compatible with hg backend so
631 pull in get does merges and we want to be compatible with hg backend so
632 pull == fetch in this case
632 pull == fetch in this case
633 """
633 """
634 self.fetch(url, commit_ids=commit_ids)
634 self.fetch(url, commit_ids=commit_ids)
635
635
636 def fetch(self, url, commit_ids=None):
636 def fetch(self, url, commit_ids=None):
637 """
637 """
638 Tries to fetch changes from external location.
638 Tries to fetch changes from external location.
639 """
639 """
640 refs = None
640 refs = None
641
641
642 if commit_ids is not None:
642 if commit_ids is not None:
643 remote_refs = self._remote.get_remote_refs(url)
643 remote_refs = self._remote.get_remote_refs(url)
644 refs = [
644 refs = [
645 ref for ref in remote_refs if remote_refs[ref] in commit_ids]
645 ref for ref in remote_refs if remote_refs[ref] in commit_ids]
646 self._remote.fetch(url, refs=refs)
646 self._remote.fetch(url, refs=refs)
647
647
648 def set_refs(self, ref_name, commit_id):
648 def set_refs(self, ref_name, commit_id):
649 self._remote.set_refs(ref_name, commit_id)
649 self._remote.set_refs(ref_name, commit_id)
650
650
651 def remove_ref(self, ref_name):
651 def remove_ref(self, ref_name):
652 self._remote.remove_ref(ref_name)
652 self._remote.remove_ref(ref_name)
653
653
654 def _update_server_info(self):
654 def _update_server_info(self):
655 """
655 """
656 runs gits update-server-info command in this repo instance
656 runs gits update-server-info command in this repo instance
657 """
657 """
658 self._remote.update_server_info()
658 self._remote.update_server_info()
659
659
660 def _current_branch(self):
660 def _current_branch(self):
661 """
661 """
662 Return the name of the current branch.
662 Return the name of the current branch.
663
663
664 It only works for non bare repositories (i.e. repositories with a
664 It only works for non bare repositories (i.e. repositories with a
665 working copy)
665 working copy)
666 """
666 """
667 if self.bare:
667 if self.bare:
668 raise RepositoryError('Bare git repos do not have active branches')
668 raise RepositoryError('Bare git repos do not have active branches')
669
669
670 if self.is_empty():
670 if self.is_empty():
671 return None
671 return None
672
672
673 stdout, _ = self.run_git_command(['rev-parse', '--abbrev-ref', 'HEAD'])
673 stdout, _ = self.run_git_command(['rev-parse', '--abbrev-ref', 'HEAD'])
674 return stdout.strip()
674 return stdout.strip()
675
675
676 def _checkout(self, branch_name, create=False):
676 def _checkout(self, branch_name, create=False):
677 """
677 """
678 Checkout a branch in the working directory.
678 Checkout a branch in the working directory.
679
679
680 It tries to create the branch if create is True, failing if the branch
680 It tries to create the branch if create is True, failing if the branch
681 already exists.
681 already exists.
682
682
683 It only works for non bare repositories (i.e. repositories with a
683 It only works for non bare repositories (i.e. repositories with a
684 working copy)
684 working copy)
685 """
685 """
686 if self.bare:
686 if self.bare:
687 raise RepositoryError('Cannot checkout branches in a bare git repo')
687 raise RepositoryError('Cannot checkout branches in a bare git repo')
688
688
689 cmd = ['checkout']
689 cmd = ['checkout']
690 if create:
690 if create:
691 cmd.append('-b')
691 cmd.append('-b')
692 cmd.append(branch_name)
692 cmd.append(branch_name)
693 self.run_git_command(cmd, fail_on_stderr=False)
693 self.run_git_command(cmd, fail_on_stderr=False)
694
694
695 def _local_clone(self, clone_path, branch_name):
695 def _local_clone(self, clone_path, branch_name):
696 """
696 """
697 Create a local clone of the current repo.
697 Create a local clone of the current repo.
698 """
698 """
699 # N.B.(skreft): the --branch option is required as otherwise the shallow
699 # N.B.(skreft): the --branch option is required as otherwise the shallow
700 # clone will only fetch the active branch.
700 # clone will only fetch the active branch.
701 cmd = ['clone', '--branch', branch_name, '--single-branch',
701 cmd = ['clone', '--branch', branch_name, '--single-branch',
702 self.path, os.path.abspath(clone_path)]
702 self.path, os.path.abspath(clone_path)]
703 self.run_git_command(cmd, fail_on_stderr=False)
703 self.run_git_command(cmd, fail_on_stderr=False)
704
704
705 def _local_fetch(self, repository_path, branch_name):
705 def _local_fetch(self, repository_path, branch_name):
706 """
706 """
707 Fetch a branch from a local repository.
707 Fetch a branch from a local repository.
708 """
708 """
709 repository_path = os.path.abspath(repository_path)
709 repository_path = os.path.abspath(repository_path)
710 if repository_path == self.path:
710 if repository_path == self.path:
711 raise ValueError('Cannot fetch from the same repository')
711 raise ValueError('Cannot fetch from the same repository')
712
712
713 cmd = ['fetch', '--no-tags', repository_path, branch_name]
713 cmd = ['fetch', '--no-tags', repository_path, branch_name]
714 self.run_git_command(cmd, fail_on_stderr=False)
714 self.run_git_command(cmd, fail_on_stderr=False)
715
715
716 def _last_fetch_heads(self):
716 def _last_fetch_heads(self):
717 """
717 """
718 Return the last fetched heads that need merging.
718 Return the last fetched heads that need merging.
719
719
720 The algorithm is defined at
720 The algorithm is defined at
721 https://github.com/git/git/blob/v2.1.3/git-pull.sh#L283
721 https://github.com/git/git/blob/v2.1.3/git-pull.sh#L283
722 """
722 """
723 if not self.bare:
723 if not self.bare:
724 fetch_heads_path = os.path.join(self.path, '.git', 'FETCH_HEAD')
724 fetch_heads_path = os.path.join(self.path, '.git', 'FETCH_HEAD')
725 else:
725 else:
726 fetch_heads_path = os.path.join(self.path, 'FETCH_HEAD')
726 fetch_heads_path = os.path.join(self.path, 'FETCH_HEAD')
727
727
728 heads = []
728 heads = []
729 with open(fetch_heads_path) as f:
729 with open(fetch_heads_path) as f:
730 for line in f:
730 for line in f:
731 if ' not-for-merge ' in line:
731 if ' not-for-merge ' in line:
732 continue
732 continue
733 line = re.sub('\t.*', '', line, flags=re.DOTALL)
733 line = re.sub('\t.*', '', line, flags=re.DOTALL)
734 heads.append(line)
734 heads.append(line)
735
735
736 return heads
736 return heads
737
737
738 def _local_pull(self, repository_path, branch_name):
738 def _local_pull(self, repository_path, branch_name):
739 """
739 """
740 Pull a branch from a local repository.
740 Pull a branch from a local repository.
741 """
741 """
742 if self.bare:
742 if self.bare:
743 raise RepositoryError('Cannot pull into a bare git repository')
743 raise RepositoryError('Cannot pull into a bare git repository')
744 # N.B.(skreft): The --ff-only option is to make sure this is a
744 # N.B.(skreft): The --ff-only option is to make sure this is a
745 # fast-forward (i.e., we are only pulling new changes and there are no
745 # fast-forward (i.e., we are only pulling new changes and there are no
746 # conflicts with our current branch)
746 # conflicts with our current branch)
747 # Additionally, that option needs to go before --no-tags, otherwise git
747 # Additionally, that option needs to go before --no-tags, otherwise git
748 # pull complains about it being an unknown flag.
748 # pull complains about it being an unknown flag.
749 cmd = ['pull', '--ff-only', '--no-tags', repository_path, branch_name]
749 cmd = ['pull', '--ff-only', '--no-tags', repository_path, branch_name]
750 self.run_git_command(cmd, fail_on_stderr=False)
750 self.run_git_command(cmd, fail_on_stderr=False)
751
751
752 def _local_merge(self, merge_message, user_name, user_email, heads):
752 def _local_merge(self, merge_message, user_name, user_email, heads):
753 """
753 """
754 Merge the given head into the checked out branch.
754 Merge the given head into the checked out branch.
755
755
756 It will force a merge commit.
756 It will force a merge commit.
757
757
758 Currently it raises an error if the repo is empty, as it is not possible
758 Currently it raises an error if the repo is empty, as it is not possible
759 to create a merge commit in an empty repo.
759 to create a merge commit in an empty repo.
760
760
761 :param merge_message: The message to use for the merge commit.
761 :param merge_message: The message to use for the merge commit.
762 :param heads: the heads to merge.
762 :param heads: the heads to merge.
763 """
763 """
764 if self.bare:
764 if self.bare:
765 raise RepositoryError('Cannot merge into a bare git repository')
765 raise RepositoryError('Cannot merge into a bare git repository')
766
766
767 if not heads:
767 if not heads:
768 return
768 return
769
769
770 if self.is_empty():
770 if self.is_empty():
771 # TODO(skreft): do somehting more robust in this case.
771 # TODO(skreft): do somehting more robust in this case.
772 raise RepositoryError(
772 raise RepositoryError(
773 'Do not know how to merge into empty repositories yet')
773 'Do not know how to merge into empty repositories yet')
774
774
775 # N.B.(skreft): the --no-ff option is used to enforce the creation of a
775 # N.B.(skreft): the --no-ff option is used to enforce the creation of a
776 # commit message. We also specify the user who is doing the merge.
776 # commit message. We also specify the user who is doing the merge.
777 cmd = ['-c', 'user.name=%s' % safe_str(user_name),
777 cmd = ['-c', 'user.name=%s' % safe_str(user_name),
778 '-c', 'user.email=%s' % safe_str(user_email),
778 '-c', 'user.email=%s' % safe_str(user_email),
779 'merge', '--no-ff', '-m', safe_str(merge_message)]
779 'merge', '--no-ff', '-m', safe_str(merge_message)]
780 cmd.extend(heads)
780 cmd.extend(heads)
781 try:
781 try:
782 self.run_git_command(cmd, fail_on_stderr=False)
782 self.run_git_command(cmd, fail_on_stderr=False)
783 except RepositoryError:
783 except RepositoryError:
784 # Cleanup any merge leftovers
784 # Cleanup any merge leftovers
785 self.run_git_command(['merge', '--abort'], fail_on_stderr=False)
785 self.run_git_command(['merge', '--abort'], fail_on_stderr=False)
786 raise
786 raise
787
787
788 def _local_push(
788 def _local_push(
789 self, source_branch, repository_path, target_branch,
789 self, source_branch, repository_path, target_branch,
790 enable_hooks=False, rc_scm_data=None):
790 enable_hooks=False, rc_scm_data=None):
791 """
791 """
792 Push the source_branch to the given repository and target_branch.
792 Push the source_branch to the given repository and target_branch.
793
793
794 Currently it if the target_branch is not master and the target repo is
794 Currently it if the target_branch is not master and the target repo is
795 empty, the push will work, but then GitRepository won't be able to find
795 empty, the push will work, but then GitRepository won't be able to find
796 the pushed branch or the commits. As the HEAD will be corrupted (i.e.,
796 the pushed branch or the commits. As the HEAD will be corrupted (i.e.,
797 pointing to master, which does not exist).
797 pointing to master, which does not exist).
798
798
799 It does not run the hooks in the target repo.
799 It does not run the hooks in the target repo.
800 """
800 """
801 # TODO(skreft): deal with the case in which the target repo is empty,
801 # TODO(skreft): deal with the case in which the target repo is empty,
802 # and the target_branch is not master.
802 # and the target_branch is not master.
803 target_repo = GitRepository(repository_path)
803 target_repo = GitRepository(repository_path)
804 if (not target_repo.bare and
804 if (not target_repo.bare and
805 target_repo._current_branch() == target_branch):
805 target_repo._current_branch() == target_branch):
806 # Git prevents pushing to the checked out branch, so simulate it by
806 # Git prevents pushing to the checked out branch, so simulate it by
807 # pulling into the target repository.
807 # pulling into the target repository.
808 target_repo._local_pull(self.path, source_branch)
808 target_repo._local_pull(self.path, source_branch)
809 else:
809 else:
810 cmd = ['push', os.path.abspath(repository_path),
810 cmd = ['push', os.path.abspath(repository_path),
811 '%s:%s' % (source_branch, target_branch)]
811 '%s:%s' % (source_branch, target_branch)]
812 gitenv = {}
812 gitenv = {}
813 if rc_scm_data:
813 if rc_scm_data:
814 gitenv.update({'RC_SCM_DATA': rc_scm_data})
814 gitenv.update({'RC_SCM_DATA': rc_scm_data})
815
815
816 if not enable_hooks:
816 if not enable_hooks:
817 gitenv['RC_SKIP_HOOKS'] = '1'
817 gitenv['RC_SKIP_HOOKS'] = '1'
818 self.run_git_command(cmd, fail_on_stderr=False, extra_env=gitenv)
818 self.run_git_command(cmd, fail_on_stderr=False, extra_env=gitenv)
819
819
820 def _get_new_pr_branch(self, source_branch, target_branch):
820 def _get_new_pr_branch(self, source_branch, target_branch):
821 prefix = 'pr_%s-%s_' % (source_branch, target_branch)
821 prefix = 'pr_%s-%s_' % (source_branch, target_branch)
822 pr_branches = []
822 pr_branches = []
823 for branch in self.branches:
823 for branch in self.branches:
824 if branch.startswith(prefix):
824 if branch.startswith(prefix):
825 pr_branches.append(int(branch[len(prefix):]))
825 pr_branches.append(int(branch[len(prefix):]))
826
826
827 if not pr_branches:
827 if not pr_branches:
828 branch_id = 0
828 branch_id = 0
829 else:
829 else:
830 branch_id = max(pr_branches) + 1
830 branch_id = max(pr_branches) + 1
831
831
832 return '%s%d' % (prefix, branch_id)
832 return '%s%d' % (prefix, branch_id)
833
833
834 def _merge_repo(self, shadow_repository_path, target_ref,
834 def _merge_repo(self, shadow_repository_path, target_ref,
835 source_repo, source_ref, merge_message,
835 source_repo, source_ref, merge_message,
836 merger_name, merger_email, dry_run=False):
836 merger_name, merger_email, dry_run=False):
837 if target_ref.commit_id != self.branches[target_ref.name]:
837 if target_ref.commit_id != self.branches[target_ref.name]:
838 return MergeResponse(
838 return MergeResponse(
839 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD)
839 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD)
840
840
841 shadow_repo = GitRepository(shadow_repository_path)
841 shadow_repo = GitRepository(shadow_repository_path)
842 shadow_repo._checkout(target_ref.name)
842 shadow_repo._checkout(target_ref.name)
843 shadow_repo._local_pull(self.path, target_ref.name)
843 shadow_repo._local_pull(self.path, target_ref.name)
844 # Need to reload repo to invalidate the cache, or otherwise we cannot
844 # Need to reload repo to invalidate the cache, or otherwise we cannot
845 # retrieve the last target commit.
845 # retrieve the last target commit.
846 shadow_repo = GitRepository(shadow_repository_path)
846 shadow_repo = GitRepository(shadow_repository_path)
847 if target_ref.commit_id != shadow_repo.branches[target_ref.name]:
847 if target_ref.commit_id != shadow_repo.branches[target_ref.name]:
848 return MergeResponse(
848 return MergeResponse(
849 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD)
849 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD)
850
850
851 pr_branch = shadow_repo._get_new_pr_branch(
851 pr_branch = shadow_repo._get_new_pr_branch(
852 source_ref.name, target_ref.name)
852 source_ref.name, target_ref.name)
853 shadow_repo._checkout(pr_branch, create=True)
853 shadow_repo._checkout(pr_branch, create=True)
854 try:
854 try:
855 shadow_repo._local_fetch(source_repo.path, source_ref.name)
855 shadow_repo._local_fetch(source_repo.path, source_ref.name)
856 except RepositoryError:
856 except RepositoryError as e:
857 log.exception('Failure when doing local fetch on git shadow repo')
857 return MergeResponse(
858 return MergeResponse(
858 False, False, None, MergeFailureReason.MISSING_COMMIT)
859 False, False, None, MergeFailureReason.MISSING_COMMIT)
859
860
860 merge_commit_id = None
861 merge_commit_id = None
861 merge_failure_reason = MergeFailureReason.NONE
862 merge_failure_reason = MergeFailureReason.NONE
862 try:
863 try:
863 shadow_repo._local_merge(merge_message, merger_name, merger_email,
864 shadow_repo._local_merge(merge_message, merger_name, merger_email,
864 [source_ref.commit_id])
865 [source_ref.commit_id])
865 merge_possible = True
866 merge_possible = True
866 except RepositoryError:
867 except RepositoryError as e:
868 log.exception('Failure when doing local merge on git shadow repo')
867 merge_possible = False
869 merge_possible = False
868 merge_failure_reason = MergeFailureReason.MERGE_FAILED
870 merge_failure_reason = MergeFailureReason.MERGE_FAILED
869
871
870 if merge_possible and not dry_run:
872 if merge_possible and not dry_run:
871 try:
873 try:
872 shadow_repo._local_push(
874 shadow_repo._local_push(
873 pr_branch, self.path, target_ref.name, enable_hooks=True,
875 pr_branch, self.path, target_ref.name, enable_hooks=True,
874 rc_scm_data=self.config.get('rhodecode', 'RC_SCM_DATA'))
876 rc_scm_data=self.config.get('rhodecode', 'RC_SCM_DATA'))
875 merge_succeeded = True
877 merge_succeeded = True
876 # Need to reload repo to invalidate the cache, or otherwise we
878 # Need to reload repo to invalidate the cache, or otherwise we
877 # cannot retrieve the merge commit.
879 # cannot retrieve the merge commit.
878 shadow_repo = GitRepository(shadow_repository_path)
880 shadow_repo = GitRepository(shadow_repository_path)
879 merge_commit_id = shadow_repo.branches[pr_branch]
881 merge_commit_id = shadow_repo.branches[pr_branch]
880 except RepositoryError:
882 except RepositoryError as e:
883 log.exception(
884 'Failure when doing local push on git shadow repo')
881 merge_succeeded = False
885 merge_succeeded = False
882 merge_failure_reason = MergeFailureReason.PUSH_FAILED
886 merge_failure_reason = MergeFailureReason.PUSH_FAILED
883 else:
887 else:
884 merge_succeeded = False
888 merge_succeeded = False
885
889
886 return MergeResponse(
890 return MergeResponse(
887 merge_possible, merge_succeeded, merge_commit_id,
891 merge_possible, merge_succeeded, merge_commit_id,
888 merge_failure_reason)
892 merge_failure_reason)
889
893
890 def _get_shadow_repository_path(self, workspace_id):
894 def _get_shadow_repository_path(self, workspace_id):
891 # The name of the shadow repository must start with '.', so it is
895 # The name of the shadow repository must start with '.', so it is
892 # skipped by 'rhodecode.lib.utils.get_filesystem_repos'.
896 # skipped by 'rhodecode.lib.utils.get_filesystem_repos'.
893 return os.path.join(
897 return os.path.join(
894 os.path.dirname(self.path),
898 os.path.dirname(self.path),
895 '.__shadow_%s_%s' % (os.path.basename(self.path), workspace_id))
899 '.__shadow_%s_%s' % (os.path.basename(self.path), workspace_id))
896
900
897 def _maybe_prepare_merge_workspace(self, workspace_id, target_ref):
901 def _maybe_prepare_merge_workspace(self, workspace_id, target_ref):
898 shadow_repository_path = self._get_shadow_repository_path(workspace_id)
902 shadow_repository_path = self._get_shadow_repository_path(workspace_id)
899 if not os.path.exists(shadow_repository_path):
903 if not os.path.exists(shadow_repository_path):
900 self._local_clone(shadow_repository_path, target_ref.name)
904 self._local_clone(shadow_repository_path, target_ref.name)
901
905
902 return shadow_repository_path
906 return shadow_repository_path
903
907
904 def cleanup_merge_workspace(self, workspace_id):
908 def cleanup_merge_workspace(self, workspace_id):
905 shadow_repository_path = self._get_shadow_repository_path(workspace_id)
909 shadow_repository_path = self._get_shadow_repository_path(workspace_id)
906 shutil.rmtree(shadow_repository_path, ignore_errors=True)
910 shutil.rmtree(shadow_repository_path, ignore_errors=True)
@@ -1,770 +1,774 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2014-2016 RhodeCode GmbH
3 # Copyright (C) 2014-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 HG repository module
22 HG repository module
23 """
23 """
24
24
25 import binascii
25 import binascii
26 import os
26 import os
27 import re
27 import re
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_fromtimestamp, makedate, date_to_timestamp_plus_offset,
35 date_fromtimestamp, makedate, date_to_timestamp_plus_offset,
36 date_astimestamp)
36 date_astimestamp)
37 from rhodecode.lib.utils import safe_unicode, safe_str
37 from rhodecode.lib.utils import safe_unicode, safe_str
38 from rhodecode.lib.vcs import connection
38 from rhodecode.lib.vcs import connection
39 from rhodecode.lib.vcs.backends.base import (
39 from rhodecode.lib.vcs.backends.base import (
40 BaseRepository, CollectionGenerator, Config, MergeResponse,
40 BaseRepository, CollectionGenerator, Config, MergeResponse,
41 MergeFailureReason)
41 MergeFailureReason)
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.conf import settings
45 from rhodecode.lib.vcs.conf import settings
46 from rhodecode.lib.vcs.exceptions import (
46 from rhodecode.lib.vcs.exceptions import (
47 EmptyRepositoryError, RepositoryError, TagAlreadyExistError,
47 EmptyRepositoryError, RepositoryError, TagAlreadyExistError,
48 TagDoesNotExistError, CommitDoesNotExistError)
48 TagDoesNotExistError, CommitDoesNotExistError)
49
49
50 hexlify = binascii.hexlify
50 hexlify = binascii.hexlify
51 nullid = "\0" * 20
51 nullid = "\0" * 20
52
52
53
53
54 class MercurialRepository(BaseRepository):
54 class MercurialRepository(BaseRepository):
55 """
55 """
56 Mercurial repository backend
56 Mercurial repository backend
57 """
57 """
58 DEFAULT_BRANCH_NAME = 'default'
58 DEFAULT_BRANCH_NAME = 'default'
59
59
60 def __init__(self, repo_path, config=None, create=False, src_url=None,
60 def __init__(self, repo_path, config=None, create=False, src_url=None,
61 update_after_clone=False, with_wire=None):
61 update_after_clone=False, with_wire=None):
62 """
62 """
63 Raises RepositoryError if repository could not be find at the given
63 Raises RepositoryError if repository could not be find at the given
64 ``repo_path``.
64 ``repo_path``.
65
65
66 :param repo_path: local path of the repository
66 :param repo_path: local path of the repository
67 :param config: config object containing the repo configuration
67 :param config: config object containing the repo configuration
68 :param create=False: if set to True, would try to create repository if
68 :param create=False: if set to True, would try to create repository if
69 it does not exist rather than raising exception
69 it does not exist rather than raising exception
70 :param src_url=None: would try to clone repository from given location
70 :param src_url=None: would try to clone repository from given location
71 :param update_after_clone=False: sets update of working copy after
71 :param update_after_clone=False: sets update of working copy after
72 making a clone
72 making a clone
73 """
73 """
74 self.path = safe_str(os.path.abspath(repo_path))
74 self.path = safe_str(os.path.abspath(repo_path))
75 self.config = config if config else Config()
75 self.config = config if config else Config()
76 self._remote = connection.Hg(
76 self._remote = connection.Hg(
77 self.path, self.config, with_wire=with_wire)
77 self.path, self.config, with_wire=with_wire)
78
78
79 self._init_repo(create, src_url, update_after_clone)
79 self._init_repo(create, src_url, update_after_clone)
80
80
81 # caches
81 # caches
82 self._commit_ids = {}
82 self._commit_ids = {}
83
83
84 @LazyProperty
84 @LazyProperty
85 def commit_ids(self):
85 def commit_ids(self):
86 """
86 """
87 Returns list of commit ids, in ascending order. Being lazy
87 Returns list of commit ids, in ascending order. Being lazy
88 attribute allows external tools to inject shas from cache.
88 attribute allows external tools to inject shas from cache.
89 """
89 """
90 commit_ids = self._get_all_commit_ids()
90 commit_ids = self._get_all_commit_ids()
91 self._rebuild_cache(commit_ids)
91 self._rebuild_cache(commit_ids)
92 return commit_ids
92 return commit_ids
93
93
94 def _rebuild_cache(self, commit_ids):
94 def _rebuild_cache(self, commit_ids):
95 self._commit_ids = dict((commit_id, index)
95 self._commit_ids = dict((commit_id, index)
96 for index, commit_id in enumerate(commit_ids))
96 for index, commit_id in enumerate(commit_ids))
97
97
98 @LazyProperty
98 @LazyProperty
99 def branches(self):
99 def branches(self):
100 return self._get_branches()
100 return self._get_branches()
101
101
102 @LazyProperty
102 @LazyProperty
103 def branches_closed(self):
103 def branches_closed(self):
104 return self._get_branches(active=False, closed=True)
104 return self._get_branches(active=False, closed=True)
105
105
106 @LazyProperty
106 @LazyProperty
107 def branches_all(self):
107 def branches_all(self):
108 all_branches = {}
108 all_branches = {}
109 all_branches.update(self.branches)
109 all_branches.update(self.branches)
110 all_branches.update(self.branches_closed)
110 all_branches.update(self.branches_closed)
111 return all_branches
111 return all_branches
112
112
113 def _get_branches(self, active=True, closed=False):
113 def _get_branches(self, active=True, closed=False):
114 """
114 """
115 Gets branches for this repository
115 Gets branches for this repository
116 Returns only not closed active branches by default
116 Returns only not closed active branches by default
117
117
118 :param active: return also active branches
118 :param active: return also active branches
119 :param closed: return also closed branches
119 :param closed: return also closed branches
120
120
121 """
121 """
122 if self.is_empty():
122 if self.is_empty():
123 return {}
123 return {}
124
124
125 def get_name(ctx):
125 def get_name(ctx):
126 return ctx[0]
126 return ctx[0]
127
127
128 _branches = [(safe_unicode(n), hexlify(h),) for n, h in
128 _branches = [(safe_unicode(n), hexlify(h),) for n, h in
129 self._remote.branches(active, closed).items()]
129 self._remote.branches(active, closed).items()]
130
130
131 return OrderedDict(sorted(_branches, key=get_name, reverse=False))
131 return OrderedDict(sorted(_branches, key=get_name, reverse=False))
132
132
133 @LazyProperty
133 @LazyProperty
134 def tags(self):
134 def tags(self):
135 """
135 """
136 Gets tags for this repository
136 Gets tags for this repository
137 """
137 """
138 return self._get_tags()
138 return self._get_tags()
139
139
140 def _get_tags(self):
140 def _get_tags(self):
141 if self.is_empty():
141 if self.is_empty():
142 return {}
142 return {}
143
143
144 def get_name(ctx):
144 def get_name(ctx):
145 return ctx[0]
145 return ctx[0]
146
146
147 _tags = [(safe_unicode(n), hexlify(h),) for n, h in
147 _tags = [(safe_unicode(n), hexlify(h),) for n, h in
148 self._remote.tags().items()]
148 self._remote.tags().items()]
149
149
150 return OrderedDict(sorted(_tags, key=get_name, reverse=True))
150 return OrderedDict(sorted(_tags, key=get_name, reverse=True))
151
151
152 def tag(self, name, user, commit_id=None, message=None, date=None,
152 def tag(self, name, user, commit_id=None, message=None, date=None,
153 **kwargs):
153 **kwargs):
154 """
154 """
155 Creates and returns a tag for the given ``commit_id``.
155 Creates and returns a tag for the given ``commit_id``.
156
156
157 :param name: name for new tag
157 :param name: name for new tag
158 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
158 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
159 :param commit_id: commit id for which new tag would be created
159 :param commit_id: commit id for which new tag would be created
160 :param message: message of the tag's commit
160 :param message: message of the tag's commit
161 :param date: date of tag's commit
161 :param date: date of tag's commit
162
162
163 :raises TagAlreadyExistError: if tag with same name already exists
163 :raises TagAlreadyExistError: if tag with same name already exists
164 """
164 """
165 if name in self.tags:
165 if name in self.tags:
166 raise TagAlreadyExistError("Tag %s already exists" % name)
166 raise TagAlreadyExistError("Tag %s already exists" % name)
167 commit = self.get_commit(commit_id=commit_id)
167 commit = self.get_commit(commit_id=commit_id)
168 local = kwargs.setdefault('local', False)
168 local = kwargs.setdefault('local', False)
169
169
170 if message is None:
170 if message is None:
171 message = "Added tag %s for commit %s" % (name, commit.short_id)
171 message = "Added tag %s for commit %s" % (name, commit.short_id)
172
172
173 date, tz = date_to_timestamp_plus_offset(date)
173 date, tz = date_to_timestamp_plus_offset(date)
174
174
175 self._remote.tag(
175 self._remote.tag(
176 name, commit.raw_id, message, local, user, date, tz)
176 name, commit.raw_id, message, local, user, date, tz)
177
177
178 # Reinitialize tags
178 # Reinitialize tags
179 self.tags = self._get_tags()
179 self.tags = self._get_tags()
180 tag_id = self.tags[name]
180 tag_id = self.tags[name]
181
181
182 return self.get_commit(commit_id=tag_id)
182 return self.get_commit(commit_id=tag_id)
183
183
184 def remove_tag(self, name, user, message=None, date=None):
184 def remove_tag(self, name, user, message=None, date=None):
185 """
185 """
186 Removes tag with the given `name`.
186 Removes tag with the given `name`.
187
187
188 :param name: name of the tag to be removed
188 :param name: name of the tag to be removed
189 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
189 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
190 :param message: message of the tag's removal commit
190 :param message: message of the tag's removal commit
191 :param date: date of tag's removal commit
191 :param date: date of tag's removal commit
192
192
193 :raises TagDoesNotExistError: if tag with given name does not exists
193 :raises TagDoesNotExistError: if tag with given name does not exists
194 """
194 """
195 if name not in self.tags:
195 if name not in self.tags:
196 raise TagDoesNotExistError("Tag %s does not exist" % name)
196 raise TagDoesNotExistError("Tag %s does not exist" % name)
197 if message is None:
197 if message is None:
198 message = "Removed tag %s" % name
198 message = "Removed tag %s" % name
199 local = False
199 local = False
200
200
201 date, tz = date_to_timestamp_plus_offset(date)
201 date, tz = date_to_timestamp_plus_offset(date)
202
202
203 self._remote.tag(name, nullid, message, local, user, date, tz)
203 self._remote.tag(name, nullid, message, local, user, date, tz)
204 self.tags = self._get_tags()
204 self.tags = self._get_tags()
205
205
206 @LazyProperty
206 @LazyProperty
207 def bookmarks(self):
207 def bookmarks(self):
208 """
208 """
209 Gets bookmarks for this repository
209 Gets bookmarks for this repository
210 """
210 """
211 return self._get_bookmarks()
211 return self._get_bookmarks()
212
212
213 def _get_bookmarks(self):
213 def _get_bookmarks(self):
214 if self.is_empty():
214 if self.is_empty():
215 return {}
215 return {}
216
216
217 def get_name(ctx):
217 def get_name(ctx):
218 return ctx[0]
218 return ctx[0]
219
219
220 _bookmarks = [
220 _bookmarks = [
221 (safe_unicode(n), hexlify(h)) for n, h in
221 (safe_unicode(n), hexlify(h)) for n, h in
222 self._remote.bookmarks().items()]
222 self._remote.bookmarks().items()]
223
223
224 return OrderedDict(sorted(_bookmarks, key=get_name))
224 return OrderedDict(sorted(_bookmarks, key=get_name))
225
225
226 def _get_all_commit_ids(self):
226 def _get_all_commit_ids(self):
227 return self._remote.get_all_commit_ids('visible')
227 return self._remote.get_all_commit_ids('visible')
228
228
229 def get_diff(
229 def get_diff(
230 self, commit1, commit2, path='', ignore_whitespace=False,
230 self, commit1, commit2, path='', ignore_whitespace=False,
231 context=3, path1=None):
231 context=3, path1=None):
232 """
232 """
233 Returns (git like) *diff*, as plain text. Shows changes introduced by
233 Returns (git like) *diff*, as plain text. Shows changes introduced by
234 `commit2` since `commit1`.
234 `commit2` since `commit1`.
235
235
236 :param commit1: Entry point from which diff is shown. Can be
236 :param commit1: Entry point from which diff is shown. Can be
237 ``self.EMPTY_COMMIT`` - in this case, patch showing all
237 ``self.EMPTY_COMMIT`` - in this case, patch showing all
238 the changes since empty state of the repository until `commit2`
238 the changes since empty state of the repository until `commit2`
239 :param commit2: Until which commit changes should be shown.
239 :param commit2: Until which commit changes should be shown.
240 :param ignore_whitespace: If set to ``True``, would not show whitespace
240 :param ignore_whitespace: If set to ``True``, would not show whitespace
241 changes. Defaults to ``False``.
241 changes. Defaults to ``False``.
242 :param context: How many lines before/after changed lines should be
242 :param context: How many lines before/after changed lines should be
243 shown. Defaults to ``3``.
243 shown. Defaults to ``3``.
244 """
244 """
245 self._validate_diff_commits(commit1, commit2)
245 self._validate_diff_commits(commit1, commit2)
246 if path1 is not None and path1 != path:
246 if path1 is not None and path1 != path:
247 raise ValueError("Diff of two different paths not supported.")
247 raise ValueError("Diff of two different paths not supported.")
248
248
249 if path:
249 if path:
250 file_filter = [self.path, path]
250 file_filter = [self.path, path]
251 else:
251 else:
252 file_filter = None
252 file_filter = None
253
253
254 diff = self._remote.diff(
254 diff = self._remote.diff(
255 commit1.raw_id, commit2.raw_id, file_filter=file_filter,
255 commit1.raw_id, commit2.raw_id, file_filter=file_filter,
256 opt_git=True, opt_ignorews=ignore_whitespace,
256 opt_git=True, opt_ignorews=ignore_whitespace,
257 context=context)
257 context=context)
258 return MercurialDiff(diff)
258 return MercurialDiff(diff)
259
259
260 def strip(self, commit_id, branch=None):
260 def strip(self, commit_id, branch=None):
261 self._remote.strip(commit_id, update=False, backup="none")
261 self._remote.strip(commit_id, update=False, backup="none")
262
262
263 self.commit_ids = self._get_all_commit_ids()
263 self.commit_ids = self._get_all_commit_ids()
264 self._rebuild_cache(self.commit_ids)
264 self._rebuild_cache(self.commit_ids)
265
265
266 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
266 def get_common_ancestor(self, commit_id1, commit_id2, repo2):
267 if commit_id1 == commit_id2:
267 if commit_id1 == commit_id2:
268 return commit_id1
268 return commit_id1
269
269
270 ancestors = self._remote.revs_from_revspec(
270 ancestors = self._remote.revs_from_revspec(
271 "ancestor(id(%s), id(%s))", commit_id1, commit_id2,
271 "ancestor(id(%s), id(%s))", commit_id1, commit_id2,
272 other_path=repo2.path)
272 other_path=repo2.path)
273 return repo2[ancestors[0]].raw_id if ancestors else None
273 return repo2[ancestors[0]].raw_id if ancestors else None
274
274
275 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
275 def compare(self, commit_id1, commit_id2, repo2, merge, pre_load=None):
276 if commit_id1 == commit_id2:
276 if commit_id1 == commit_id2:
277 commits = []
277 commits = []
278 else:
278 else:
279 if merge:
279 if merge:
280 indexes = self._remote.revs_from_revspec(
280 indexes = self._remote.revs_from_revspec(
281 "ancestors(id(%s)) - ancestors(id(%s)) - id(%s)",
281 "ancestors(id(%s)) - ancestors(id(%s)) - id(%s)",
282 commit_id2, commit_id1, commit_id1, other_path=repo2.path)
282 commit_id2, commit_id1, commit_id1, other_path=repo2.path)
283 else:
283 else:
284 indexes = self._remote.revs_from_revspec(
284 indexes = self._remote.revs_from_revspec(
285 "id(%s)..id(%s) - id(%s)", commit_id1, commit_id2,
285 "id(%s)..id(%s) - id(%s)", commit_id1, commit_id2,
286 commit_id1, other_path=repo2.path)
286 commit_id1, other_path=repo2.path)
287
287
288 commits = [repo2.get_commit(commit_idx=idx, pre_load=pre_load)
288 commits = [repo2.get_commit(commit_idx=idx, pre_load=pre_load)
289 for idx in indexes]
289 for idx in indexes]
290
290
291 return commits
291 return commits
292
292
293 @staticmethod
293 @staticmethod
294 def check_url(url, config):
294 def check_url(url, config):
295 """
295 """
296 Function will check given url and try to verify if it's a valid
296 Function will check given url and try to verify if it's a valid
297 link. Sometimes it may happened that mercurial will issue basic
297 link. Sometimes it may happened that mercurial will issue basic
298 auth request that can cause whole API to hang when used from python
298 auth request that can cause whole API to hang when used from python
299 or other external calls.
299 or other external calls.
300
300
301 On failures it'll raise urllib2.HTTPError, exception is also thrown
301 On failures it'll raise urllib2.HTTPError, exception is also thrown
302 when the return code is non 200
302 when the return code is non 200
303 """
303 """
304 # check first if it's not an local url
304 # check first if it's not an local url
305 if os.path.isdir(url) or url.startswith('file:'):
305 if os.path.isdir(url) or url.startswith('file:'):
306 return True
306 return True
307
307
308 # Request the _remote to verify the url
308 # Request the _remote to verify the url
309 return connection.Hg.check_url(url, config.serialize())
309 return connection.Hg.check_url(url, config.serialize())
310
310
311 @staticmethod
311 @staticmethod
312 def is_valid_repository(path):
312 def is_valid_repository(path):
313 return os.path.isdir(os.path.join(path, '.hg'))
313 return os.path.isdir(os.path.join(path, '.hg'))
314
314
315 def _init_repo(self, create, src_url=None, update_after_clone=False):
315 def _init_repo(self, create, src_url=None, update_after_clone=False):
316 """
316 """
317 Function will check for mercurial repository in given path. If there
317 Function will check for mercurial repository in given path. If there
318 is no repository in that path it will raise an exception unless
318 is no repository in that path it will raise an exception unless
319 `create` parameter is set to True - in that case repository would
319 `create` parameter is set to True - in that case repository would
320 be created.
320 be created.
321
321
322 If `src_url` is given, would try to clone repository from the
322 If `src_url` is given, would try to clone repository from the
323 location at given clone_point. Additionally it'll make update to
323 location at given clone_point. Additionally it'll make update to
324 working copy accordingly to `update_after_clone` flag.
324 working copy accordingly to `update_after_clone` flag.
325 """
325 """
326 if create and os.path.exists(self.path):
326 if create and os.path.exists(self.path):
327 raise RepositoryError(
327 raise RepositoryError(
328 "Cannot create repository at %s, location already exist"
328 "Cannot create repository at %s, location already exist"
329 % self.path)
329 % self.path)
330
330
331 if src_url:
331 if src_url:
332 url = str(self._get_url(src_url))
332 url = str(self._get_url(src_url))
333 MercurialRepository.check_url(url, self.config)
333 MercurialRepository.check_url(url, self.config)
334
334
335 self._remote.clone(url, self.path, update_after_clone)
335 self._remote.clone(url, self.path, update_after_clone)
336
336
337 # Don't try to create if we've already cloned repo
337 # Don't try to create if we've already cloned repo
338 create = False
338 create = False
339
339
340 if create:
340 if create:
341 os.makedirs(self.path, mode=0755)
341 os.makedirs(self.path, mode=0755)
342
342
343 self._remote.localrepository(create)
343 self._remote.localrepository(create)
344
344
345 @LazyProperty
345 @LazyProperty
346 def in_memory_commit(self):
346 def in_memory_commit(self):
347 return MercurialInMemoryCommit(self)
347 return MercurialInMemoryCommit(self)
348
348
349 @LazyProperty
349 @LazyProperty
350 def description(self):
350 def description(self):
351 description = self._remote.get_config_value(
351 description = self._remote.get_config_value(
352 'web', 'description', untrusted=True)
352 'web', 'description', untrusted=True)
353 return safe_unicode(description or self.DEFAULT_DESCRIPTION)
353 return safe_unicode(description or self.DEFAULT_DESCRIPTION)
354
354
355 @LazyProperty
355 @LazyProperty
356 def contact(self):
356 def contact(self):
357 contact = (
357 contact = (
358 self._remote.get_config_value("web", "contact") or
358 self._remote.get_config_value("web", "contact") or
359 self._remote.get_config_value("ui", "username"))
359 self._remote.get_config_value("ui", "username"))
360 return safe_unicode(contact or self.DEFAULT_CONTACT)
360 return safe_unicode(contact or self.DEFAULT_CONTACT)
361
361
362 @LazyProperty
362 @LazyProperty
363 def last_change(self):
363 def last_change(self):
364 """
364 """
365 Returns last change made on this repository as
365 Returns last change made on this repository as
366 `datetime.datetime` object
366 `datetime.datetime` object
367 """
367 """
368 return date_fromtimestamp(self._get_mtime(), makedate()[1])
368 return date_fromtimestamp(self._get_mtime(), makedate()[1])
369
369
370 def _get_mtime(self):
370 def _get_mtime(self):
371 try:
371 try:
372 return date_astimestamp(self.get_commit().date)
372 return date_astimestamp(self.get_commit().date)
373 except RepositoryError:
373 except RepositoryError:
374 # fallback to filesystem
374 # fallback to filesystem
375 cl_path = os.path.join(self.path, '.hg', "00changelog.i")
375 cl_path = os.path.join(self.path, '.hg', "00changelog.i")
376 st_path = os.path.join(self.path, '.hg', "store")
376 st_path = os.path.join(self.path, '.hg', "store")
377 if os.path.exists(cl_path):
377 if os.path.exists(cl_path):
378 return os.stat(cl_path).st_mtime
378 return os.stat(cl_path).st_mtime
379 else:
379 else:
380 return os.stat(st_path).st_mtime
380 return os.stat(st_path).st_mtime
381
381
382 def _sanitize_commit_idx(self, idx):
382 def _sanitize_commit_idx(self, idx):
383 # Note: Mercurial has ``int(-1)`` reserved as not existing id_or_idx
383 # Note: Mercurial has ``int(-1)`` reserved as not existing id_or_idx
384 # number. A `long` is treated in the correct way though. So we convert
384 # number. A `long` is treated in the correct way though. So we convert
385 # `int` to `long` here to make sure it is handled correctly.
385 # `int` to `long` here to make sure it is handled correctly.
386 if isinstance(idx, int):
386 if isinstance(idx, int):
387 return long(idx)
387 return long(idx)
388 return idx
388 return idx
389
389
390 def _get_url(self, url):
390 def _get_url(self, url):
391 """
391 """
392 Returns normalized url. If schema is not given, would fall
392 Returns normalized url. If schema is not given, would fall
393 to filesystem
393 to filesystem
394 (``file:///``) schema.
394 (``file:///``) schema.
395 """
395 """
396 url = url.encode('utf8')
396 url = url.encode('utf8')
397 if url != 'default' and '://' not in url:
397 if url != 'default' and '://' not in url:
398 url = "file:" + urllib.pathname2url(url)
398 url = "file:" + urllib.pathname2url(url)
399 return url
399 return url
400
400
401 def get_hook_location(self):
401 def get_hook_location(self):
402 """
402 """
403 returns absolute path to location where hooks are stored
403 returns absolute path to location where hooks are stored
404 """
404 """
405 return os.path.join(self.path, '.hg', '.hgrc')
405 return os.path.join(self.path, '.hg', '.hgrc')
406
406
407 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
407 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
408 """
408 """
409 Returns ``MercurialCommit`` object representing repository's
409 Returns ``MercurialCommit`` object representing repository's
410 commit at the given `commit_id` or `commit_idx`.
410 commit at the given `commit_id` or `commit_idx`.
411 """
411 """
412 if self.is_empty():
412 if self.is_empty():
413 raise EmptyRepositoryError("There are no commits yet")
413 raise EmptyRepositoryError("There are no commits yet")
414
414
415 if commit_id is not None:
415 if commit_id is not None:
416 self._validate_commit_id(commit_id)
416 self._validate_commit_id(commit_id)
417 try:
417 try:
418 idx = self._commit_ids[commit_id]
418 idx = self._commit_ids[commit_id]
419 return MercurialCommit(self, commit_id, idx, pre_load=pre_load)
419 return MercurialCommit(self, commit_id, idx, pre_load=pre_load)
420 except KeyError:
420 except KeyError:
421 pass
421 pass
422 elif commit_idx is not None:
422 elif commit_idx is not None:
423 self._validate_commit_idx(commit_idx)
423 self._validate_commit_idx(commit_idx)
424 commit_idx = self._sanitize_commit_idx(commit_idx)
424 commit_idx = self._sanitize_commit_idx(commit_idx)
425 try:
425 try:
426 id_ = self.commit_ids[commit_idx]
426 id_ = self.commit_ids[commit_idx]
427 if commit_idx < 0:
427 if commit_idx < 0:
428 commit_idx += len(self.commit_ids)
428 commit_idx += len(self.commit_ids)
429 return MercurialCommit(
429 return MercurialCommit(
430 self, id_, commit_idx, pre_load=pre_load)
430 self, id_, commit_idx, pre_load=pre_load)
431 except IndexError:
431 except IndexError:
432 commit_id = commit_idx
432 commit_id = commit_idx
433 else:
433 else:
434 commit_id = "tip"
434 commit_id = "tip"
435
435
436 # TODO Paris: Ugly hack to "serialize" long for msgpack
436 # TODO Paris: Ugly hack to "serialize" long for msgpack
437 if isinstance(commit_id, long):
437 if isinstance(commit_id, long):
438 commit_id = float(commit_id)
438 commit_id = float(commit_id)
439
439
440 if isinstance(commit_id, unicode):
440 if isinstance(commit_id, unicode):
441 commit_id = safe_str(commit_id)
441 commit_id = safe_str(commit_id)
442
442
443 raw_id, idx = self._remote.lookup(commit_id, both=True)
443 raw_id, idx = self._remote.lookup(commit_id, both=True)
444
444
445 return MercurialCommit(self, raw_id, idx, pre_load=pre_load)
445 return MercurialCommit(self, raw_id, idx, pre_load=pre_load)
446
446
447 def get_commits(
447 def get_commits(
448 self, start_id=None, end_id=None, start_date=None, end_date=None,
448 self, start_id=None, end_id=None, start_date=None, end_date=None,
449 branch_name=None, pre_load=None):
449 branch_name=None, pre_load=None):
450 """
450 """
451 Returns generator of ``MercurialCommit`` objects from start to end
451 Returns generator of ``MercurialCommit`` objects from start to end
452 (both are inclusive)
452 (both are inclusive)
453
453
454 :param start_id: None, str(commit_id)
454 :param start_id: None, str(commit_id)
455 :param end_id: None, str(commit_id)
455 :param end_id: None, str(commit_id)
456 :param start_date: if specified, commits with commit date less than
456 :param start_date: if specified, commits with commit date less than
457 ``start_date`` would be filtered out from returned set
457 ``start_date`` would be filtered out from returned set
458 :param end_date: if specified, commits with commit date greater than
458 :param end_date: if specified, commits with commit date greater than
459 ``end_date`` would be filtered out from returned set
459 ``end_date`` would be filtered out from returned set
460 :param branch_name: if specified, commits not reachable from given
460 :param branch_name: if specified, commits not reachable from given
461 branch would be filtered out from returned set
461 branch would be filtered out from returned set
462
462
463 :raise BranchDoesNotExistError: If given ``branch_name`` does not
463 :raise BranchDoesNotExistError: If given ``branch_name`` does not
464 exist.
464 exist.
465 :raise CommitDoesNotExistError: If commit for given ``start`` or
465 :raise CommitDoesNotExistError: If commit for given ``start`` or
466 ``end`` could not be found.
466 ``end`` could not be found.
467 """
467 """
468 # actually we should check now if it's not an empty repo
468 # actually we should check now if it's not an empty repo
469 branch_ancestors = False
469 branch_ancestors = False
470 if self.is_empty():
470 if self.is_empty():
471 raise EmptyRepositoryError("There are no commits yet")
471 raise EmptyRepositoryError("There are no commits yet")
472 self._validate_branch_name(branch_name)
472 self._validate_branch_name(branch_name)
473
473
474 if start_id is not None:
474 if start_id is not None:
475 self._validate_commit_id(start_id)
475 self._validate_commit_id(start_id)
476 c_start = self.get_commit(commit_id=start_id)
476 c_start = self.get_commit(commit_id=start_id)
477 start_pos = self._commit_ids[c_start.raw_id]
477 start_pos = self._commit_ids[c_start.raw_id]
478 else:
478 else:
479 start_pos = None
479 start_pos = None
480
480
481 if end_id is not None:
481 if end_id is not None:
482 self._validate_commit_id(end_id)
482 self._validate_commit_id(end_id)
483 c_end = self.get_commit(commit_id=end_id)
483 c_end = self.get_commit(commit_id=end_id)
484 end_pos = max(0, self._commit_ids[c_end.raw_id])
484 end_pos = max(0, self._commit_ids[c_end.raw_id])
485 else:
485 else:
486 end_pos = None
486 end_pos = None
487
487
488 if None not in [start_id, end_id] and start_pos > end_pos:
488 if None not in [start_id, end_id] and start_pos > end_pos:
489 raise RepositoryError(
489 raise RepositoryError(
490 "Start commit '%s' cannot be after end commit '%s'" %
490 "Start commit '%s' cannot be after end commit '%s'" %
491 (start_id, end_id))
491 (start_id, end_id))
492
492
493 if end_pos is not None:
493 if end_pos is not None:
494 end_pos += 1
494 end_pos += 1
495
495
496 commit_filter = []
496 commit_filter = []
497 if branch_name and not branch_ancestors:
497 if branch_name and not branch_ancestors:
498 commit_filter.append('branch("%s")' % branch_name)
498 commit_filter.append('branch("%s")' % branch_name)
499 elif branch_name and branch_ancestors:
499 elif branch_name and branch_ancestors:
500 commit_filter.append('ancestors(branch("%s"))' % branch_name)
500 commit_filter.append('ancestors(branch("%s"))' % branch_name)
501 if start_date and not end_date:
501 if start_date and not end_date:
502 commit_filter.append('date(">%s")' % start_date)
502 commit_filter.append('date(">%s")' % start_date)
503 if end_date and not start_date:
503 if end_date and not start_date:
504 commit_filter.append('date("<%s")' % end_date)
504 commit_filter.append('date("<%s")' % end_date)
505 if start_date and end_date:
505 if start_date and end_date:
506 commit_filter.append(
506 commit_filter.append(
507 'date(">%s") and date("<%s")' % (start_date, end_date))
507 'date(">%s") and date("<%s")' % (start_date, end_date))
508
508
509 # TODO: johbo: Figure out a simpler way for this solution
509 # TODO: johbo: Figure out a simpler way for this solution
510 collection_generator = CollectionGenerator
510 collection_generator = CollectionGenerator
511 if commit_filter:
511 if commit_filter:
512 commit_filter = map(safe_str, commit_filter)
512 commit_filter = map(safe_str, commit_filter)
513 revisions = self._remote.rev_range(commit_filter)
513 revisions = self._remote.rev_range(commit_filter)
514 collection_generator = MercurialIndexBasedCollectionGenerator
514 collection_generator = MercurialIndexBasedCollectionGenerator
515 else:
515 else:
516 revisions = self.commit_ids
516 revisions = self.commit_ids
517
517
518 if start_pos or end_pos:
518 if start_pos or end_pos:
519 revisions = revisions[start_pos:end_pos]
519 revisions = revisions[start_pos:end_pos]
520
520
521 return collection_generator(self, revisions, pre_load=pre_load)
521 return collection_generator(self, revisions, pre_load=pre_load)
522
522
523 def pull(self, url, commit_ids=None):
523 def pull(self, url, commit_ids=None):
524 """
524 """
525 Tries to pull changes from external location.
525 Tries to pull changes from external location.
526
526
527 :param commit_ids: Optional. Can be set to a list of commit ids
527 :param commit_ids: Optional. Can be set to a list of commit ids
528 which shall be pulled from the other repository.
528 which shall be pulled from the other repository.
529 """
529 """
530 url = self._get_url(url)
530 url = self._get_url(url)
531 self._remote.pull(url, commit_ids=commit_ids)
531 self._remote.pull(url, commit_ids=commit_ids)
532
532
533 def _local_clone(self, clone_path):
533 def _local_clone(self, clone_path):
534 """
534 """
535 Create a local clone of the current repo.
535 Create a local clone of the current repo.
536 """
536 """
537 self._remote.clone(self.path, clone_path, update_after_clone=True,
537 self._remote.clone(self.path, clone_path, update_after_clone=True,
538 hooks=False)
538 hooks=False)
539
539
540 def _update(self, revision, clean=False):
540 def _update(self, revision, clean=False):
541 """
541 """
542 Update the working copty to the specified revision.
542 Update the working copty to the specified revision.
543 """
543 """
544 self._remote.update(revision, clean=clean)
544 self._remote.update(revision, clean=clean)
545
545
546 def _identify(self):
546 def _identify(self):
547 """
547 """
548 Return the current state of the working directory.
548 Return the current state of the working directory.
549 """
549 """
550 return self._remote.identify().strip().rstrip('+')
550 return self._remote.identify().strip().rstrip('+')
551
551
552 def _heads(self, branch=None):
552 def _heads(self, branch=None):
553 """
553 """
554 Return the commit ids of the repository heads.
554 Return the commit ids of the repository heads.
555 """
555 """
556 return self._remote.heads(branch=branch).strip().split(' ')
556 return self._remote.heads(branch=branch).strip().split(' ')
557
557
558 def _ancestor(self, revision1, revision2):
558 def _ancestor(self, revision1, revision2):
559 """
559 """
560 Return the common ancestor of the two revisions.
560 Return the common ancestor of the two revisions.
561 """
561 """
562 return self._remote.ancestor(
562 return self._remote.ancestor(
563 revision1, revision2).strip().split(':')[-1]
563 revision1, revision2).strip().split(':')[-1]
564
564
565 def _local_push(
565 def _local_push(
566 self, revision, repository_path, push_branches=False,
566 self, revision, repository_path, push_branches=False,
567 enable_hooks=False):
567 enable_hooks=False):
568 """
568 """
569 Push the given revision to the specified repository.
569 Push the given revision to the specified repository.
570
570
571 :param push_branches: allow to create branches in the target repo.
571 :param push_branches: allow to create branches in the target repo.
572 """
572 """
573 self._remote.push(
573 self._remote.push(
574 [revision], repository_path, hooks=enable_hooks,
574 [revision], repository_path, hooks=enable_hooks,
575 push_branches=push_branches)
575 push_branches=push_branches)
576
576
577 def _local_merge(self, target_ref, merge_message, user_name, user_email,
577 def _local_merge(self, target_ref, merge_message, user_name, user_email,
578 source_ref):
578 source_ref):
579 """
579 """
580 Merge the given source_revision into the checked out revision.
580 Merge the given source_revision into the checked out revision.
581
581
582 Returns the commit id of the merge and a boolean indicating if the
582 Returns the commit id of the merge and a boolean indicating if the
583 commit needs to be pushed.
583 commit needs to be pushed.
584 """
584 """
585 self._update(target_ref.commit_id)
585 self._update(target_ref.commit_id)
586
586
587 ancestor = self._ancestor(target_ref.commit_id, source_ref.commit_id)
587 ancestor = self._ancestor(target_ref.commit_id, source_ref.commit_id)
588 is_the_same_branch = self._is_the_same_branch(target_ref, source_ref)
588 is_the_same_branch = self._is_the_same_branch(target_ref, source_ref)
589
589
590 if ancestor == source_ref.commit_id:
590 if ancestor == source_ref.commit_id:
591 # Nothing to do, the changes were already integrated
591 # Nothing to do, the changes were already integrated
592 return target_ref.commit_id, False
592 return target_ref.commit_id, False
593
593
594 elif ancestor == target_ref.commit_id and is_the_same_branch:
594 elif ancestor == target_ref.commit_id and is_the_same_branch:
595 # In this case we should force a commit message
595 # In this case we should force a commit message
596 return source_ref.commit_id, True
596 return source_ref.commit_id, True
597
597
598 if settings.HG_USE_REBASE_FOR_MERGING:
598 if settings.HG_USE_REBASE_FOR_MERGING:
599 try:
599 try:
600 bookmark_name = 'rcbook%s%s' % (source_ref.commit_id,
600 bookmark_name = 'rcbook%s%s' % (source_ref.commit_id,
601 target_ref.commit_id)
601 target_ref.commit_id)
602 self.bookmark(bookmark_name, revision=source_ref.commit_id)
602 self.bookmark(bookmark_name, revision=source_ref.commit_id)
603 self._remote.rebase(
603 self._remote.rebase(
604 source=source_ref.commit_id, dest=target_ref.commit_id)
604 source=source_ref.commit_id, dest=target_ref.commit_id)
605 self._update(bookmark_name)
605 self._update(bookmark_name)
606 return self._identify(), True
606 return self._identify(), True
607 except RepositoryError:
607 except RepositoryError:
608 # Cleanup any rebase leftovers
608 # Cleanup any rebase leftovers
609 self._remote.rebase(abort=True)
609 self._remote.rebase(abort=True)
610 self._remote.update(clean=True)
610 self._remote.update(clean=True)
611 raise
611 raise
612 else:
612 else:
613 try:
613 try:
614 self._remote.merge(source_ref.commit_id)
614 self._remote.merge(source_ref.commit_id)
615 self._remote.commit(
615 self._remote.commit(
616 message=safe_str(merge_message),
616 message=safe_str(merge_message),
617 username=safe_str('%s <%s>' % (user_name, user_email)))
617 username=safe_str('%s <%s>' % (user_name, user_email)))
618 return self._identify(), True
618 return self._identify(), True
619 except RepositoryError:
619 except RepositoryError:
620 # Cleanup any merge leftovers
620 # Cleanup any merge leftovers
621 self._remote.update(clean=True)
621 self._remote.update(clean=True)
622 raise
622 raise
623
623
624 def _is_the_same_branch(self, target_ref, source_ref):
624 def _is_the_same_branch(self, target_ref, source_ref):
625 return (
625 return (
626 self._get_branch_name(target_ref) ==
626 self._get_branch_name(target_ref) ==
627 self._get_branch_name(source_ref))
627 self._get_branch_name(source_ref))
628
628
629 def _get_branch_name(self, ref):
629 def _get_branch_name(self, ref):
630 if ref.type == 'branch':
630 if ref.type == 'branch':
631 return ref.name
631 return ref.name
632 return self._remote.ctx_branch(ref.commit_id)
632 return self._remote.ctx_branch(ref.commit_id)
633
633
634 def _get_shadow_repository_path(self, workspace_id):
634 def _get_shadow_repository_path(self, workspace_id):
635 # The name of the shadow repository must start with '.', so it is
635 # The name of the shadow repository must start with '.', so it is
636 # skipped by 'rhodecode.lib.utils.get_filesystem_repos'.
636 # skipped by 'rhodecode.lib.utils.get_filesystem_repos'.
637 return os.path.join(
637 return os.path.join(
638 os.path.dirname(self.path),
638 os.path.dirname(self.path),
639 '.__shadow_%s_%s' % (os.path.basename(self.path), workspace_id))
639 '.__shadow_%s_%s' % (os.path.basename(self.path), workspace_id))
640
640
641 def _maybe_prepare_merge_workspace(self, workspace_id, unused_target_ref):
641 def _maybe_prepare_merge_workspace(self, workspace_id, unused_target_ref):
642 shadow_repository_path = self._get_shadow_repository_path(workspace_id)
642 shadow_repository_path = self._get_shadow_repository_path(workspace_id)
643 if not os.path.exists(shadow_repository_path):
643 if not os.path.exists(shadow_repository_path):
644 self._local_clone(shadow_repository_path)
644 self._local_clone(shadow_repository_path)
645
645
646 return shadow_repository_path
646 return shadow_repository_path
647
647
648 def cleanup_merge_workspace(self, workspace_id):
648 def cleanup_merge_workspace(self, workspace_id):
649 shadow_repository_path = self._get_shadow_repository_path(workspace_id)
649 shadow_repository_path = self._get_shadow_repository_path(workspace_id)
650 shutil.rmtree(shadow_repository_path, ignore_errors=True)
650 shutil.rmtree(shadow_repository_path, ignore_errors=True)
651
651
652 def _merge_repo(self, shadow_repository_path, target_ref,
652 def _merge_repo(self, shadow_repository_path, target_ref,
653 source_repo, source_ref, merge_message,
653 source_repo, source_ref, merge_message,
654 merger_name, merger_email, dry_run=False):
654 merger_name, merger_email, dry_run=False):
655 if target_ref.commit_id not in self._heads():
655 if target_ref.commit_id not in self._heads():
656 return MergeResponse(
656 return MergeResponse(
657 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD)
657 False, False, None, MergeFailureReason.TARGET_IS_NOT_HEAD)
658
658
659 if (target_ref.type == 'branch' and
659 if (target_ref.type == 'branch' and
660 len(self._heads(target_ref.name)) != 1):
660 len(self._heads(target_ref.name)) != 1):
661 return MergeResponse(
661 return MergeResponse(
662 False, False, None,
662 False, False, None,
663 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS)
663 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS)
664
664
665 shadow_repo = self._get_shadow_instance(shadow_repository_path)
665 shadow_repo = self._get_shadow_instance(shadow_repository_path)
666
666
667 self._validate_pull_reference(target_ref)
667 self._validate_pull_reference(target_ref)
668 shadow_repo._local_pull(self.path, target_ref)
668 shadow_repo._local_pull(self.path, target_ref)
669 try:
669 try:
670 source_repo._validate_pull_reference(source_ref)
670 source_repo._validate_pull_reference(source_ref)
671 shadow_repo._local_pull(source_repo.path, source_ref)
671 shadow_repo._local_pull(source_repo.path, source_ref)
672 except CommitDoesNotExistError:
672 except CommitDoesNotExistError as e:
673 log.exception('Failure when doing local pull on hg shadow repo')
673 return MergeResponse(
674 return MergeResponse(
674 False, False, None, MergeFailureReason.MISSING_COMMIT)
675 False, False, None, MergeFailureReason.MISSING_COMMIT)
675
676
676 merge_commit_id = None
677 merge_commit_id = None
677 merge_failure_reason = MergeFailureReason.NONE
678 merge_failure_reason = MergeFailureReason.NONE
678
679
679 try:
680 try:
680 merge_commit_id, needs_push = shadow_repo._local_merge(
681 merge_commit_id, needs_push = shadow_repo._local_merge(
681 target_ref, merge_message, merger_name, merger_email,
682 target_ref, merge_message, merger_name, merger_email,
682 source_ref)
683 source_ref)
683 merge_possible = True
684 merge_possible = True
684 except RepositoryError:
685 except RepositoryError as e:
686 log.exception('Failure when doing local merge on hg shadow repo')
685 merge_possible = False
687 merge_possible = False
686 merge_failure_reason = MergeFailureReason.MERGE_FAILED
688 merge_failure_reason = MergeFailureReason.MERGE_FAILED
687
689
688 if merge_possible and not dry_run:
690 if merge_possible and not dry_run:
689 if needs_push:
691 if needs_push:
690 # In case the target is a bookmark, update it, so after pushing
692 # In case the target is a bookmark, update it, so after pushing
691 # the bookmarks is also updated in the target.
693 # the bookmarks is also updated in the target.
692 if target_ref.type == 'book':
694 if target_ref.type == 'book':
693 shadow_repo.bookmark(
695 shadow_repo.bookmark(
694 target_ref.name, revision=merge_commit_id)
696 target_ref.name, revision=merge_commit_id)
695
697
696 try:
698 try:
697 shadow_repo_with_hooks = self._get_shadow_instance(
699 shadow_repo_with_hooks = self._get_shadow_instance(
698 shadow_repository_path,
700 shadow_repository_path,
699 enable_hooks=True)
701 enable_hooks=True)
700 # Note: the push_branches option will push any new branch
702 # Note: the push_branches option will push any new branch
701 # defined in the source repository to the target. This may
703 # defined in the source repository to the target. This may
702 # be dangerous as branches are permanent in Mercurial.
704 # be dangerous as branches are permanent in Mercurial.
703 # This feature was requested in issue #441.
705 # This feature was requested in issue #441.
704 shadow_repo_with_hooks._local_push(
706 shadow_repo_with_hooks._local_push(
705 merge_commit_id, self.path, push_branches=True,
707 merge_commit_id, self.path, push_branches=True,
706 enable_hooks=True)
708 enable_hooks=True)
707 merge_succeeded = True
709 merge_succeeded = True
708 except RepositoryError:
710 except RepositoryError as e:
711 log.exception(
712 'Failure when doing local push on hg shadow repo')
709 merge_succeeded = False
713 merge_succeeded = False
710 merge_failure_reason = MergeFailureReason.PUSH_FAILED
714 merge_failure_reason = MergeFailureReason.PUSH_FAILED
711 else:
715 else:
712 merge_succeeded = True
716 merge_succeeded = True
713 else:
717 else:
714 merge_succeeded = False
718 merge_succeeded = False
715
719
716 if dry_run:
720 if dry_run:
717 merge_commit_id = None
721 merge_commit_id = None
718
722
719 return MergeResponse(
723 return MergeResponse(
720 merge_possible, merge_succeeded, merge_commit_id,
724 merge_possible, merge_succeeded, merge_commit_id,
721 merge_failure_reason)
725 merge_failure_reason)
722
726
723 def _get_shadow_instance(
727 def _get_shadow_instance(
724 self, shadow_repository_path, enable_hooks=False):
728 self, shadow_repository_path, enable_hooks=False):
725 config = self.config.copy()
729 config = self.config.copy()
726 if not enable_hooks:
730 if not enable_hooks:
727 config.clear_section('hooks')
731 config.clear_section('hooks')
728 return MercurialRepository(shadow_repository_path, config)
732 return MercurialRepository(shadow_repository_path, config)
729
733
730 def _validate_pull_reference(self, reference):
734 def _validate_pull_reference(self, reference):
731 if not (reference.name in self.bookmarks or
735 if not (reference.name in self.bookmarks or
732 reference.name in self.branches or
736 reference.name in self.branches or
733 self.get_commit(reference.commit_id)):
737 self.get_commit(reference.commit_id)):
734 raise CommitDoesNotExistError(
738 raise CommitDoesNotExistError(
735 'Unknown branch, bookmark or commit id')
739 'Unknown branch, bookmark or commit id')
736
740
737 def _local_pull(self, repository_path, reference):
741 def _local_pull(self, repository_path, reference):
738 """
742 """
739 Fetch a branch, bookmark or commit from a local repository.
743 Fetch a branch, bookmark or commit from a local repository.
740 """
744 """
741 repository_path = os.path.abspath(repository_path)
745 repository_path = os.path.abspath(repository_path)
742 if repository_path == self.path:
746 if repository_path == self.path:
743 raise ValueError('Cannot pull from the same repository')
747 raise ValueError('Cannot pull from the same repository')
744
748
745 reference_type_to_option_name = {
749 reference_type_to_option_name = {
746 'book': 'bookmark',
750 'book': 'bookmark',
747 'branch': 'branch',
751 'branch': 'branch',
748 }
752 }
749 option_name = reference_type_to_option_name.get(
753 option_name = reference_type_to_option_name.get(
750 reference.type, 'revision')
754 reference.type, 'revision')
751
755
752 if option_name == 'revision':
756 if option_name == 'revision':
753 ref = reference.commit_id
757 ref = reference.commit_id
754 else:
758 else:
755 ref = reference.name
759 ref = reference.name
756
760
757 options = {option_name: [ref]}
761 options = {option_name: [ref]}
758 self._remote.pull_cmd(repository_path, hooks=False, **options)
762 self._remote.pull_cmd(repository_path, hooks=False, **options)
759
763
760 def bookmark(self, bookmark, revision=None):
764 def bookmark(self, bookmark, revision=None):
761 if isinstance(bookmark, unicode):
765 if isinstance(bookmark, unicode):
762 bookmark = safe_str(bookmark)
766 bookmark = safe_str(bookmark)
763 self._remote.bookmark(bookmark, revision=revision)
767 self._remote.bookmark(bookmark, revision=revision)
764
768
765
769
766 class MercurialIndexBasedCollectionGenerator(CollectionGenerator):
770 class MercurialIndexBasedCollectionGenerator(CollectionGenerator):
767
771
768 def _commit_factory(self, commit_id):
772 def _commit_factory(self, commit_id):
769 return self.repo.get_commit(
773 return self.repo.get_commit(
770 commit_idx=commit_id, pre_load=self.pre_load)
774 commit_idx=commit_id, pre_load=self.pre_load)
General Comments 0
You need to be logged in to leave comments. Login now