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