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