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