##// END OF EJS Templates
git: use smarter way for checking if repo is empty. This doesn't spam logs with some dulwich exceptions, we shouldn't really care about here.`
marcink -
r2955:7eb44380 default
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

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