def add_welcome_comment( pr: MergeRequest, autorespond_text: str='Hi! This is GitMate v2.0!' ): """ Adds a welcome comment to pull requests. """ sign = TimestampSigner().sign(autorespond_text) msg = ('{}\n\n(Powered by [GitMate.io](https://gitmate.io))\n\n' '<!-- Timestamp signature `{}` -->'.format(autorespond_text, sign)) pr.add_comment(msg)
def gitmate_ack(pr: MergeRequest, comment: Comment, ack_strs: str = 'ack, reack', unack_strs: str = 'unack'): """ A responder to ack and unack commits """ body = comment.body.lower() commits = pr.commits perm_level = pr.repository.get_permission_level(comment.author) comment_slices = map_comment_parts_to_keywords(ack_strs, unack_strs, body) has_commit_sha = any( sha_compiled.search(string) for _list in comment_slices.values() for string in _list) # return right away if the comment isn't related to ack / unack command if not any(comment_slices) or not has_commit_sha: return elif perm_level.value < AccessLevel.CAN_WRITE.value: msg = ('Sorry @{}, you do not have the necessary permission ' 'levels to perform the action.'.format(comment.author.username)) pr.add_comment(msg) return db_pr, created = MergeRequestModel.objects.get_or_create( repo=Repository.from_igitt_repo(pr.repository), number=pr.number, defaults={'acks': dict()}) if created: # GitMate was integrated to the repo after syncing the pull request add_review_status(pr) db_pr.refresh_from_db() for commit in commits: for substring in comment_slices['unack']: if commit.sha[:6] in substring: db_pr.acks[_get_commit_hash(commit)] = _status_to_dict( unack(commit)) for substring in comment_slices['ack']: if commit.sha[:6] in substring: db_pr.acks[_get_commit_hash(commit)] = _status_to_dict( ack(commit)) db_pr.save() pr.head.set_status(db_pr.ack_state)
def apply_command_on_merge_request( pr: MergeRequest, comment: Comment, enable_rebase: bool = False, enable_merge: bool = False, enable_fastforward: bool = False, merge_admin_only: bool = True, fastforward_admin_only: bool = True, ): """ Performs a merge, fastforward or rebase of a merge request when an authorized user posts a command mentioning the keywords ``merge``, ``fastforward``/``ff`` or ``rebase`` respectively. e.g. ``@gitmate-bot rebase`` rebases the pull request with master. """ username = Repository.from_igitt_repo(pr.repository).user.username cmd, cmd_past = get_matched_command(comment.body, username) enabled_cmd = { 'rebase': enable_rebase, 'merge': enable_merge, 'fastforward': enable_fastforward }.get(cmd) if enabled_cmd: if not verify_command_access(comment, merge_admin_only, fastforward_admin_only, cmd): pr.add_comment( f'Hey @{comment.author.username}, you do not have the access ' f'to perform the {cmd} action with [GitMate.io]' '(https://gitmate.io). Please ask a maintainer to give you ' 'access. :warning:') return pr.add_comment( f'Hey! I\'m [GitMate.io](https://gitmate.io)! This pull request is' f' being {cmd_past} automatically. Please **DO NOT** push while ' f'{cmd} is in progress or your changes would be lost permanently ' ':warning:') head_clone_url = pr.source_repository.clone_url base_clone_url = pr.target_repository.clone_url output = run_in_container(settings.REBASER_IMAGE, 'python', 'run.py', cmd, head_clone_url, base_clone_url, pr.head_branch_name, pr.base_branch_name) output = json.loads(output) if output['status'] == 'success': pr.add_comment( f'Automated {cmd} with [GitMate.io](https://gitmate.io) was ' 'successful! :tada:') elif 'error' in output: # hiding oauth token for safeguarding user privacy error = output['error'].replace(head_clone_url, '<hidden_oauth_token>') error = error.replace(base_clone_url, '<hidden_oauth_token>') pr.add_comment(f'Automated {cmd} failed! Please {cmd} your pull ' 'request manually via the command line.\n\n' 'Reason:\n```\n{}\n```'.format(error))
def add_labels_based_on_size(pr: MergeRequest, size_scheme: str = 'size/{size}'): """ Labels the pull request with size labels according to the amount of code touched, commits and files involved. Helps plan the review in advance. """ sizes = {'XXL', 'XL', 'L', 'M', 'S', 'XS'} with lock_igitt_object('label mr', pr): labels = pr.labels.difference( {size_scheme.format(size=size) for size in sizes}) lines_added, lines_deleted = pr.diffstat commit_score = 4 * len(pr.commits) file_score = 4 * len(pr.affected_files) if commit_score + file_score + lines_added + lines_deleted <= 100: pr.labels = {size_scheme.format(size='XS')}.union(labels) elif commit_score + file_score + lines_added + lines_deleted <= 250: pr.labels = {size_scheme.format(size='S')}.union(labels) elif commit_score + file_score + lines_added + lines_deleted <= 500: pr.labels = {size_scheme.format(size='M')}.union(labels) elif commit_score + file_score + lines_added + lines_deleted <= 1000: pr.labels = {size_scheme.format(size='L')}.union(labels) elif commit_score + file_score + lines_added + lines_deleted <= 1500: pr.labels = {size_scheme.format(size='XL')}.union(labels) else: pr.labels = {size_scheme.format(size='XXL')}.union(labels)
class TestMergeRequest(IGittTestCase): def setUp(self): self.mr = MergeRequest() self.repo = Repository() @patch.object(Repository, 'hoster', new_callable=PropertyMock) @patch.object(Repository, 'full_name', new_callable=PropertyMock) @patch.object(MergeRequest, 'repository', new_callable=PropertyMock) def test_get_keywords_issues(self, mock_repository, mock_full_name, mock_hoster): mock_hoster.return_value = 'github' mock_full_name.return_value = 'gitmate-test-user/test' mock_repository.return_value = self.repo test_cases = [ ({('123', 'gitmate-test-user/test')}, ['https://github.com/gitmate-test-user/test/issues/123']), ({('234', 'gitmate-test-user/test/repo')}, ['gitmate-test-user/test/repo#234']), ({('345', 'gitmate-test-user/test')}, ['gitmate-test-user/test#345']), ({('456', 'gitmate-test-user/test')}, ['#456']), ({('345', 'gitmate-test-user/test')}, [ 'hey there [#123](https://github.com/gitmate-test-user/test/issues/345)' ]) ] for expected, body in test_cases: self.assertEqual(self.mr._get_keywords_issues(r'', body), expected) bad = [ '[#123]', '#123ds', 'https://saucelabs.com/beta/tests/18c6aed24ed143d3bd1d1096498f34ac/commands#178', ] for body in bad: self.assertEqual(self.mr._get_keywords_issues(r'', body), set())
def sync_updated_pr_with_issue(pr: MergeRequest, sync_assignees: bool='Synchronize Assignees'): issues = pr.closes_issues repo = Repository.from_igitt_repo(pr.repository) pr_obj = MergeRequestModel.objects.get_or_create( repo=repo, number=pr.number)[0] data = defaultdict(dict) with lock_igitt_object('label mr', pr): labels = pr.labels for issue in issues: labels = issue.labels | labels pr.labels = labels if sync_assignees: with lock_igitt_object('assign mr', pr): assignees = pr.assignees for issue in issues: assignees |= issue.assignees data[str(issue.number)]['assignees'] = True pr.assignees = assignees pr_obj.closes_issues = data pr_obj.save()
def remove_stale_label_from_merge_requests( pr: MergeRequest, *args, stale_label: str = 'Label to be used for marking stale' ): """ Unassigns the chosen label from pull requests when they are updated again. """ if len(args) > 0 and args[0] == stale_label: # LABELED and UNLABELED events return the label used, skip action if # the label was ``stale_label`` return with lock_igitt_object('label mr', pr): pr.labels = pr.labels - {stale_label}
def mark_pending_review_or_wip_accordingly( pr: MergeRequest, wip_label: str = 'Work in progress', pending_review_label: str = 'Review pending'): """ Labels the pull request as pending review and removes work in progress on every changed PR accordingly. But retains work in progress label, if title of the pull request begins with "wip". """ with lock_igitt_object('label mr', pr): labels = pr.labels # Allows [wip] and WIP: if not 'wip' in pr.title.lower()[:4]: labels.add(pending_review_label) labels.discard(wip_label) else: labels.add(wip_label) labels.discard(pending_review_label) pr.labels = labels
def setUp(self): self.mr = MergeRequest() self.repo = Repository()