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