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