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