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