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