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