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