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