def show_my(self): c.closed = request.GET.get('closed') or '' c.my_pull_requests = PullRequest.query( include_closed=c.closed, sorted=True, ).filter_by(owner_id=request.authuser.user_id).all() c.participate_in_pull_requests = [] c.participate_in_pull_requests_todo = [] done_status = set( [ChangesetStatus.STATUS_APPROVED, ChangesetStatus.STATUS_REJECTED]) for pr in PullRequest.query( include_closed=c.closed, reviewer_id=request.authuser.user_id, sorted=True, ): status = pr.user_review_status( request.authuser.user_id) # very inefficient!!! if status in done_status: c.participate_in_pull_requests.append(pr) else: c.participate_in_pull_requests_todo.append(pr) return render('/pullrequests/pullrequest_show_my.html')
def post(self, repo_name, pull_request_id): pull_request = PullRequest.get_or_404(pull_request_id) if pull_request.is_closed(): raise HTTPForbidden() assert pull_request.other_repo.repo_name == repo_name #only owner or admin can update it owner = pull_request.author.user_id == c.authuser.user_id repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name) if not (h.HasPermissionAny('hg.admin') or repo_admin or owner): raise HTTPForbidden() _form = PullRequestPostForm()().to_python(request.POST) reviewers_ids = [int(s) for s in _form['review_members']] if _form['updaterev']: return self.create_update(pull_request, _form['updaterev'], _form['pullrequest_title'], _form['pullrequest_desc'], reviewers_ids) old_description = pull_request.description pull_request.title = _form['pullrequest_title'] pull_request.description = _form['pullrequest_desc'].strip() or _( 'No description') PullRequestModel().mention_from_description(pull_request, old_description) PullRequestModel().update_reviewers(pull_request_id, reviewers_ids) Session().commit() h.flash(_('Pull request updated'), category='success') return redirect(pull_request.url())
def post(self, repo_name, pull_request_id): pull_request = PullRequest.get_or_404(pull_request_id) if pull_request.is_closed(): raise HTTPForbidden() assert pull_request.other_repo.repo_name == repo_name #only owner or admin can update it owner = pull_request.author.user_id == c.authuser.user_id repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name) if not (h.HasPermissionAny('hg.admin') or repo_admin or owner): raise HTTPForbidden() _form = PullRequestPostForm()().to_python(request.POST) reviewers_ids = [int(s) for s in _form['review_members']] if _form['updaterev']: return self.create_update(pull_request, _form['updaterev'], _form['pullrequest_title'], _form['pullrequest_desc'], reviewers_ids) old_description = pull_request.description pull_request.title = _form['pullrequest_title'] pull_request.description = _form['pullrequest_desc'].strip() or _('No description') PullRequestModel().mention_from_description(pull_request, old_description) PullRequestModel().update_reviewers(pull_request_id, reviewers_ids) Session().commit() h.flash(_('Pull request updated'), category='success') return redirect(pull_request.url())
def _get_comments(self, repo_id, revision=None, pull_request=None, inline=False): """ Gets comments for either revision or pull_request_id, either inline or general. """ q = Session().query(ChangesetComment) if inline: q = q.filter(ChangesetComment.line_no != None) \ .filter(ChangesetComment.f_path != None) else: q = q.filter(ChangesetComment.line_no == None) \ .filter(ChangesetComment.f_path == None) if revision is not None: q = q.filter(ChangesetComment.revision == revision) \ .filter(ChangesetComment.repo_id == repo_id) elif pull_request is not None: pull_request = PullRequest.guess_instance(pull_request) q = q.filter(ChangesetComment.pull_request == pull_request) else: raise Exception('Please specify either revision or pull_request') return q.order_by(ChangesetComment.created_on).all()
def test_close_pr(self): self.log_user() pr_id = self._create_pr() text = 'general comment on pullrequest' params = { 'text': text, 'save_close': 'close', '_session_csrf_secret_token': self.session_csrf_secret_token() } response = self.app.post(base.url(controller='pullrequests', action='comment', repo_name=base.HG_REPO, pull_request_id=pr_id), params=params, extra_environ={'HTTP_X_PARTIAL_XHR': '1'}) # Test response... assert response.status == '200 OK' response = self.app.get( base.url(controller='pullrequests', action='show', repo_name=base.HG_REPO, pull_request_id=pr_id, extra='')) response.mustcontain('''title (Closed)''') response.mustcontain(text) # test DB assert PullRequest.get(pr_id).status == PullRequest.STATUS_CLOSED
def test_delete_pr(self): self.log_user() pr_id = self._create_pr() text = 'general comment on pullrequest' params = { 'text': text, 'save_delete': 'delete', '_session_csrf_secret_token': self.session_csrf_secret_token() } response = self.app.post(base.url(controller='pullrequests', action='comment', repo_name=base.HG_REPO, pull_request_id=pr_id), params=params, extra_environ={'HTTP_X_PARTIAL_XHR': '1'}) # Test response... assert response.status == '200 OK' response = self.app.get(base.url(controller='pullrequests', action='show', repo_name=base.HG_REPO, pull_request_id=pr_id, extra=''), status=404) # test DB assert PullRequest.get(pr_id) is None
def test_delete_closed_pr(self): self.log_user() pr_id = self._create_pr() # first close text = 'general comment on pullrequest' params = { 'text': text, 'save_close': 'close', '_session_csrf_secret_token': self.session_csrf_secret_token() } response = self.app.post(base.url(controller='pullrequests', action='comment', repo_name=base.HG_REPO, pull_request_id=pr_id), params=params, extra_environ={'HTTP_X_PARTIAL_XHR': '1'}) assert response.status == '200 OK' # attempt delete, should fail params = { 'text': text, 'save_delete': 'delete', '_session_csrf_secret_token': self.session_csrf_secret_token() } response = self.app.post(base.url(controller='pullrequests', action='comment', repo_name=base.HG_REPO, pull_request_id=pr_id), params=params, extra_environ={'HTTP_X_PARTIAL_XHR': '1'}, status=403) # verify that PR still exists, in closed state assert PullRequest.get(pr_id).status == PullRequest.STATUS_CLOSED
def set_status(self, repo, status, user, comment, revision=None, pull_request=None, dont_allow_on_closed_pull_request=False): """ Creates new status for changeset or updates the old ones bumping their version, leaving the current status at the value of 'status'. :param repo: :param status: :param user: :param comment: :param revision: :param pull_request: :param dont_allow_on_closed_pull_request: don't allow a status change if last status was for pull request and it's closed. We shouldn't mess around this manually """ repo = Repository.guess_instance(repo) q = ChangesetStatus.query() if revision is not None: assert pull_request is None q = q.filter(ChangesetStatus.repo == repo) q = q.filter(ChangesetStatus.revision == revision) revisions = [revision] else: assert pull_request is not None pull_request = PullRequest.guess_instance(pull_request) repo = pull_request.org_repo q = q.filter(ChangesetStatus.repo == repo) q = q.filter(ChangesetStatus.revision.in_(pull_request.revisions)) revisions = pull_request.revisions cur_statuses = q.all() #if statuses exists and last is associated with a closed pull request # we need to check if we can allow this status change if (dont_allow_on_closed_pull_request and cur_statuses and getattr(cur_statuses[0].pull_request, 'status', '') == PullRequest.STATUS_CLOSED): raise StatusChangeOnClosedPullRequestError( 'Changing status on closed pull request is not allowed' ) #update all current statuses with older version for st in cur_statuses: st.version += 1 new_statuses = [] for rev in revisions: new_status = ChangesetStatus() new_status.version = 0 # default new_status.author = User.guess_instance(user) new_status.repo = Repository.guess_instance(repo) new_status.status = status new_status.comment = comment new_status.revision = rev new_status.pull_request = pull_request new_statuses.append(new_status) Session().add(new_status) return new_statuses
def comment(self, repo_name, pull_request_id): pull_request = PullRequest.get_or_404(pull_request_id) allowed_to_change_status = self._is_allowed_to_change_status( pull_request) return create_cs_pr_comment( repo_name, pull_request=pull_request, allowed_to_change_status=allowed_to_change_status)
def post(self, repo_name, pull_request_id): pull_request = PullRequest.get_or_404(pull_request_id) if pull_request.is_closed(): raise HTTPForbidden() assert pull_request.other_repo.repo_name == repo_name # only owner or admin can update it owner = pull_request.owner_id == request.authuser.user_id repo_admin = h.HasRepoPermissionLevel('admin')(c.repo_name) if not (h.HasPermissionAny('hg.admin')() or repo_admin or owner): raise HTTPForbidden() _form = PullRequestPostForm()().to_python(request.POST) cur_reviewers = set(pull_request.get_reviewer_users()) new_reviewers = set(_get_reviewer(s) for s in _form['review_members']) old_reviewers = set( _get_reviewer(s) for s in _form['org_review_members']) other_added = cur_reviewers - old_reviewers other_removed = old_reviewers - cur_reviewers if other_added: h.flash( _('Meanwhile, the following reviewers have been added: %s') % (', '.join(u.username for u in other_added)), category='warning') if other_removed: h.flash( _('Meanwhile, the following reviewers have been removed: %s') % (', '.join(u.username for u in other_removed)), category='warning') if _form['updaterev']: return self.create_new_iteration(pull_request, _form['updaterev'], _form['pullrequest_title'], _form['pullrequest_desc'], new_reviewers) added_reviewers = new_reviewers - old_reviewers - cur_reviewers removed_reviewers = (old_reviewers - new_reviewers) & cur_reviewers old_description = pull_request.description pull_request.title = _form['pullrequest_title'] pull_request.description = _form['pullrequest_desc'].strip() or _( 'No description') pull_request.owner = User.get_by_username(_form['owner']) user = User.get(request.authuser.user_id) PullRequestModel().mention_from_description(user, pull_request, old_description) PullRequestModel().add_reviewers(user, pull_request, added_reviewers) PullRequestModel().remove_reviewers(user, pull_request, removed_reviewers) Session().commit() h.flash(_('Pull request updated'), category='success') raise HTTPFound(location=pull_request.url())
def delete(self, pull_request): pull_request = PullRequest.guess_instance(pull_request) Session().delete(pull_request) if pull_request.org_repo.scm_instance.alias == 'git': # remove a ref under refs/pull/ so that commits can be garbage-collected try: del pull_request.org_repo.scm_instance._repo["refs/pull/%d/head" % pull_request.pull_request_id] except KeyError: pass
def delete(self, repo_name, pull_request_id): pull_request = PullRequest.get_or_404(pull_request_id) # only owner can delete it ! if pull_request.owner_id == request.authuser.user_id: PullRequestModel().delete(pull_request) Session().commit() h.flash(_('Successfully deleted pull request'), category='success') raise HTTPFound(location=url('my_pullrequests')) raise HTTPForbidden()
def _before(self, *args, **kwargs): """ _before is called before controller methods and after __call__ """ c.kallithea_version = __version__ rc_config = Setting.get_app_settings() # Visual options c.visual = AttributeDict({}) ## DB stored c.visual.show_public_icon = str2bool(rc_config.get('show_public_icon')) c.visual.show_private_icon = str2bool(rc_config.get('show_private_icon')) c.visual.stylify_metatags = str2bool(rc_config.get('stylify_metatags')) c.visual.page_size = safe_int(rc_config.get('dashboard_items', 100)) c.visual.admin_grid_items = safe_int(rc_config.get('admin_grid_items', 100)) c.visual.repository_fields = str2bool(rc_config.get('repository_fields')) c.visual.show_version = str2bool(rc_config.get('show_version')) c.visual.use_gravatar = str2bool(rc_config.get('use_gravatar')) c.visual.gravatar_url = rc_config.get('gravatar_url') c.ga_code = rc_config.get('ga_code') # TODO: replace undocumented backwards compatibility hack with db upgrade and rename ga_code if c.ga_code and '<' not in c.ga_code: c.ga_code = '''<script type="text/javascript"> var _gaq = _gaq || []; _gaq.push(['_setAccount', '%s']); _gaq.push(['_trackPageview']); (function() { var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true; ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'; var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); })(); </script>''' % c.ga_code c.site_name = rc_config.get('title') c.clone_uri_tmpl = rc_config.get('clone_uri_tmpl') ## INI stored c.visual.allow_repo_location_change = str2bool(config.get('allow_repo_location_change', True)) c.visual.allow_custom_hooks_settings = str2bool(config.get('allow_custom_hooks_settings', True)) c.instance_id = config.get('instance_id') c.issues_url = config.get('bugtracker', url('issues_url')) # END CONFIG VARS c.repo_name = get_repo_slug(request) # can be empty c.backends = BACKENDS.keys() c.unread_notifications = NotificationModel() \ .get_unread_cnt_for_user(request.authuser.user_id) self.cut_off_limit = safe_int(config.get('cut_off_limit')) c.my_pr_count = PullRequest.query(reviewer_id=request.authuser.user_id, include_closed=False).count() self.scm_model = ScmModel()
def delete(self, repo_name, pull_request_id): pull_request = PullRequest.get_or_404(pull_request_id) #only owner can delete it ! if pull_request.owner.user_id == c.authuser.user_id: PullRequestModel().delete(pull_request) Session().commit() h.flash(_('Successfully deleted pull request'), category='success') raise HTTPFound(location=url('my_pullrequests')) raise HTTPForbidden()
def create(self, created_by, org_repo, org_ref, other_repo, other_ref, revisions, reviewers, title, description=None): from kallithea.model.changeset_status import ChangesetStatusModel created_by_user = self._get_user(created_by) org_repo = self._get_repo(org_repo) other_repo = self._get_repo(other_repo) new = PullRequest() new.org_repo = org_repo new.org_ref = org_ref new.other_repo = other_repo new.other_ref = other_ref new.revisions = revisions new.title = title new.description = description new.author = created_by_user Session().add(new) Session().flush() #reset state to under-review from kallithea.model.comment import ChangesetCommentsModel comment = ChangesetCommentsModel().create( text=u'Auto status change to %s' % (ChangesetStatus.get_status_lbl(ChangesetStatus.STATUS_UNDER_REVIEW)), repo=org_repo, user=new.author, pull_request=new, send_email=False ) ChangesetStatusModel().set_status( org_repo, ChangesetStatus.STATUS_UNDER_REVIEW, new.author, comment, pull_request=new ) mention_recipients = set(User.get_by_username(username, case_insensitive=True) for username in extract_mentioned_users(new.description)) self.__add_reviewers(new, reviewers, mention_recipients) return new
def execute(self): created_by = User.get(request.authuser.user_id) pr = PullRequest() pr.org_repo = self.org_repo pr.org_ref = self.org_ref pr.other_repo = self.other_repo pr.other_ref = self.other_ref pr.revisions = self.revisions pr.title = self.title pr.description = self.description pr.owner = self.owner Session().add(pr) Session().flush() # make database assign pull_request_id if self.org_repo.scm_instance.alias == 'git': # create a ref under refs/pull/ so that commits don't get garbage-collected self.org_repo.scm_instance._repo["refs/pull/%d/head" % pr.pull_request_id] = safe_str(self.org_rev) #reset state to under-review from kallithea.model.changeset_status import ChangesetStatusModel from kallithea.model.comment import ChangesetCommentsModel comment = ChangesetCommentsModel().create( text=u'', repo=self.org_repo, author=created_by, pull_request=pr, send_email=False, status_change=ChangesetStatus.STATUS_UNDER_REVIEW, ) ChangesetStatusModel().set_status( self.org_repo, ChangesetStatus.STATUS_UNDER_REVIEW, created_by, comment, pull_request=pr, ) mention_recipients = extract_mentioned_users(self.description) PullRequestModel().add_reviewers(created_by, pr, self.reviewers, mention_recipients) return pr
def show_my(self): c.closed = request.GET.get('closed') or '' def _filter(pr): s = sorted(pr, key=lambda o: o.created_on, reverse=True) if not c.closed: s = filter(lambda p: p.status != PullRequest.STATUS_CLOSED, s) return s c.my_pull_requests = _filter(PullRequest.query()\ .filter(PullRequest.user_id == self.authuser.user_id)\ .all()) c.participate_in_pull_requests = _filter(PullRequest.query()\ .join(PullRequestReviewers)\ .filter(PullRequestReviewers.user_id == self.authuser.user_id)\ ) return render('/pullrequests/pullrequest_show_my.html')
def post(self, repo_name, pull_request_id): pull_request = PullRequest.get_or_404(pull_request_id) if pull_request.is_closed(): raise HTTPForbidden() assert pull_request.other_repo.repo_name == repo_name #only owner or admin can update it owner = pull_request.owner_id == request.authuser.user_id repo_admin = h.HasRepoPermissionLevel('admin')(c.repo_name) if not (h.HasPermissionAny('hg.admin')() or repo_admin or owner): raise HTTPForbidden() _form = PullRequestPostForm()().to_python(request.POST) cur_reviewers = set(pull_request.get_reviewer_users()) new_reviewers = set(_get_reviewer(s) for s in _form['review_members']) old_reviewers = set(_get_reviewer(s) for s in _form['org_review_members']) other_added = cur_reviewers - old_reviewers other_removed = old_reviewers - cur_reviewers if other_added: h.flash(_('Meanwhile, the following reviewers have been added: %s') % (', '.join(u.username for u in other_added)), category='warning') if other_removed: h.flash(_('Meanwhile, the following reviewers have been removed: %s') % (', '.join(u.username for u in other_removed)), category='warning') if _form['updaterev']: return self.create_new_iteration(pull_request, _form['updaterev'], _form['pullrequest_title'], _form['pullrequest_desc'], new_reviewers) added_reviewers = new_reviewers - old_reviewers - cur_reviewers removed_reviewers = (old_reviewers - new_reviewers) & cur_reviewers old_description = pull_request.description pull_request.title = _form['pullrequest_title'] pull_request.description = _form['pullrequest_desc'].strip() or _('No description') pull_request.owner = User.get_by_username(_form['owner']) user = User.get(request.authuser.user_id) PullRequestModel().mention_from_description(user, pull_request, old_description) PullRequestModel().add_reviewers(user, pull_request, added_reviewers) PullRequestModel().remove_reviewers(user, pull_request, removed_reviewers) Session().commit() h.flash(_('Pull request updated'), category='success') raise HTTPFound(location=pull_request.url())
def show_my(self): c.closed = request.GET.get('closed') or '' def _filter(pr): s = sorted(pr, key=lambda o: o.created_on, reverse=True) if not c.closed: s = filter(lambda p: p.status != PullRequest.STATUS_CLOSED, s) return s c.my_pull_requests = _filter(PullRequest.query() \ .filter(PullRequest.user_id == self.authuser.user_id) \ .all()) c.participate_in_pull_requests = _filter(PullRequest.query() \ .join(PullRequestReviewers) \ .filter(PullRequestReviewers.user_id == self.authuser.user_id) \ ) return render('/pullrequests/pullrequest_show_my.html')
def set_status(self, repo, status, user, comment, revision=None, pull_request=None): """ Creates new status for changeset or updates the old ones bumping their version, leaving the current status at the value of 'status'. :param repo: :param status: :param user: :param comment: :param revision: :param pull_request: """ repo = Repository.guess_instance(repo) q = ChangesetStatus.query() if revision is not None: assert pull_request is None q = q.filter(ChangesetStatus.repo == repo) q = q.filter(ChangesetStatus.revision == revision) revisions = [revision] else: assert pull_request is not None pull_request = PullRequest.guess_instance(pull_request) repo = pull_request.org_repo q = q.filter(ChangesetStatus.repo == repo) q = q.filter(ChangesetStatus.revision.in_(pull_request.revisions)) revisions = pull_request.revisions cur_statuses = q.all() # update all current statuses with older version for st in cur_statuses: st.version += 1 new_statuses = [] for rev in revisions: new_status = ChangesetStatus() new_status.version = 0 # default new_status.author = User.guess_instance(user) new_status.repo = Repository.guess_instance(repo) new_status.status = status new_status.comment = comment new_status.revision = rev new_status.pull_request = pull_request new_statuses.append(new_status) Session().add(new_status) return new_statuses
def get_all(self, repo_name, from_=False, closed=False): """Get all PRs for repo. Default is all PRs to the repo, PRs from the repo if from_. Closed PRs are only included if closed is true.""" repo = self._get_repo(repo_name) q = PullRequest.query() if from_: q = q.filter(PullRequest.org_repo == repo) else: q = q.filter(PullRequest.other_repo == repo) if not closed: q = q.filter(PullRequest.status != PullRequest.STATUS_CLOSED) return q.order_by(PullRequest.created_on.desc()).all()
def create(self, created_by, org_repo, org_ref, other_repo, other_ref, revisions, reviewers, title, description=None): from kallithea.model.changeset_status import ChangesetStatusModel created_by_user = self._get_user(created_by) org_repo = self._get_repo(org_repo) other_repo = self._get_repo(other_repo) new = PullRequest() new.org_repo = org_repo new.org_ref = org_ref new.other_repo = other_repo new.other_ref = other_ref new.revisions = revisions new.title = title new.description = description new.owner = created_by_user Session().add(new) Session().flush() #reset state to under-review from kallithea.model.comment import ChangesetCommentsModel comment = ChangesetCommentsModel().create( text=u'', repo=org_repo, user=new.owner, pull_request=new, send_email=False, status_change=ChangesetStatus.STATUS_UNDER_REVIEW, ) ChangesetStatusModel().set_status( org_repo, ChangesetStatus.STATUS_UNDER_REVIEW, new.owner, comment, pull_request=new ) mention_recipients = set(User.get_by_username(username, case_insensitive=True) for username in extract_mentioned_users(new.description)) self.__add_reviewers(created_by_user, new, reviewers, mention_recipients) return new
def get_pull_request(): from kallithea.model.db import PullRequest pull_request_id = action_params nice_id = PullRequest.make_nice_id(pull_request_id) deleted = user_log.repository is None if deleted: repo_name = user_log.repository_name else: repo_name = user_log.repository.repo_name return link_to(_('Pull request %s') % nice_id, url('pullrequest_show', repo_name=repo_name, pull_request_id=pull_request_id))
def show_my(self): c.closed = request.GET.get('closed') or '' c.my_pull_requests = PullRequest.query( include_closed=c.closed, sorted=True, ).filter_by(owner_id=request.authuser.user_id).all() c.participate_in_pull_requests = [] c.participate_in_pull_requests_todo = [] done_status = set([ChangesetStatus.STATUS_APPROVED, ChangesetStatus.STATUS_REJECTED]) for pr in PullRequest.query( include_closed=c.closed, reviewer_id=request.authuser.user_id, sorted=True, ): status = pr.user_review_status(request.authuser.user_id) # very inefficient!!! if status in done_status: c.participate_in_pull_requests.append(pr) else: c.participate_in_pull_requests_todo.append(pr) return render('/pullrequests/pullrequest_show_my.html')
def show_all(self, repo_name): c.from_ = request.GET.get('from_') or '' c.closed = request.GET.get('closed') or '' p = safe_int(request.GET.get('page'), 1) q = PullRequest.query(include_closed=c.closed, sorted=True) if c.from_: q = q.filter_by(org_repo=c.db_repo) else: q = q.filter_by(other_repo=c.db_repo) c.pull_requests = q.all() c.pullrequests_pager = Page(c.pull_requests, page=p, items_per_page=100) return render('/pullrequests/pullrequest_show_all.html')
def _get_comments(self, repo_id, revision=None, pull_request=None, inline=False, f_path=None, line_no=None): """ Gets comments for either revision or pull_request_id, either inline or general. If a file path and optionally line number are given, return only the matching inline comments. """ if f_path is None and line_no is not None: raise Exception("line_no only makes sense if f_path is given.") if inline is None and f_path is not None: raise Exception("f_path only makes sense for inline comments.") q = Session().query(ChangesetComment) if inline: if f_path is not None: # inline comments for a given file... q = q.filter(ChangesetComment.f_path == f_path) if line_no is None: # ... on any line q = q.filter(ChangesetComment.line_no != None) else: # ... on specific line q = q.filter(ChangesetComment.line_no == line_no) else: # all inline comments q = q.filter(ChangesetComment.line_no != None) \ .filter(ChangesetComment.f_path != None) else: # all general comments q = q.filter(ChangesetComment.line_no == None) \ .filter(ChangesetComment.f_path == None) if revision is not None: q = q.filter(ChangesetComment.revision == revision) \ .filter(ChangesetComment.repo_id == repo_id) elif pull_request is not None: pull_request = PullRequest.guess_instance(pull_request) q = q.filter(ChangesetComment.pull_request == pull_request) else: raise Exception('Please specify either revision or pull_request') return q.order_by(ChangesetComment.created_on).all()
def _get_status_query(self, repo, revision, pull_request, with_revisions=False): repo = Repository.guess_instance(repo) q = ChangesetStatus.query() \ .filter(ChangesetStatus.repo == repo) if not with_revisions: # only report the latest vote across all users! TODO: be smarter! q = q.filter(ChangesetStatus.version == 0) if revision: q = q.filter(ChangesetStatus.revision == revision) elif pull_request: pull_request = PullRequest.guess_instance(pull_request) q = q.filter(ChangesetStatus.pull_request == pull_request) else: raise Exception('Please specify revision or pull_request') q = q.order_by(ChangesetStatus.version.asc()) return q
def test_iteration_refs(self): # Repo graph excerpt: # o fb95b340e0d0 webvcs # /: # o : 41d2568309a0 default # : : # : o 5ec21f21aafe webvcs # : : # : o 9e6119747791 webvcs # : : # o : 3d1091ee5a53 default # :/ # o 948da46b29c1 default self.log_user() # create initial PR response = self.app.post( url(controller='pullrequests', action='create', repo_name=HG_REPO), { 'org_repo': HG_REPO, 'org_ref': 'rev:9e6119747791:9e6119747791ff886a5abe1193a730b6bf874e1c', 'other_repo': HG_REPO, 'other_ref': 'branch:default:3d1091ee5a533b1f4577ec7d8a226bb315fb1336', 'pullrequest_title': 'title', 'pullrequest_desc': 'description', '_authentication_token': self.authentication_token(), }, status=302) pr1_id = int( re.search('/pull-request/(\d+)/', response.location).group(1)) pr1 = PullRequest.get(pr1_id) assert pr1.org_ref == 'branch:webvcs:9e6119747791ff886a5abe1193a730b6bf874e1c' assert pr1.other_ref == 'branch:default:948da46b29c125838a717f6a8496eb409717078d' Session().rollback( ) # invalidate loaded PR objects before issuing next request. # create PR 2 (new iteration with same ancestor) response = self.app.post( url(controller='pullrequests', action='post', repo_name=HG_REPO, pull_request_id=pr1_id), { 'updaterev': '5ec21f21aafe95220f1fc4843a4a57c378498b71', 'pullrequest_title': 'title', 'pullrequest_desc': 'description', 'owner': TEST_USER_REGULAR_LOGIN, '_authentication_token': self.authentication_token(), }, status=302) pr2_id = int( re.search('/pull-request/(\d+)/', response.location).group(1)) pr1 = PullRequest.get(pr1_id) pr2 = PullRequest.get(pr2_id) assert pr2_id != pr1_id assert pr1.status == PullRequest.STATUS_CLOSED assert pr2.org_ref == 'branch:webvcs:5ec21f21aafe95220f1fc4843a4a57c378498b71' assert pr2.other_ref == pr1.other_ref Session().rollback( ) # invalidate loaded PR objects before issuing next request. # create PR 3 (new iteration with new ancestor) response = self.app.post( url(controller='pullrequests', action='post', repo_name=HG_REPO, pull_request_id=pr2_id), { 'updaterev': 'fb95b340e0d03fa51f33c56c991c08077c99303e', 'pullrequest_title': 'title', 'pullrequest_desc': 'description', 'owner': TEST_USER_REGULAR_LOGIN, '_authentication_token': self.authentication_token(), }, status=302) pr3_id = int( re.search('/pull-request/(\d+)/', response.location).group(1)) pr2 = PullRequest.get(pr2_id) pr3 = PullRequest.get(pr3_id) assert pr3_id != pr2_id assert pr2.status == PullRequest.STATUS_CLOSED assert pr3.org_ref == 'branch:webvcs:fb95b340e0d03fa51f33c56c991c08077c99303e' assert pr3.other_ref == 'branch:default:41d2568309a05f422cffb8008e599d385f8af439'
def comment(self, repo_name, pull_request_id): pull_request = PullRequest.get_or_404(pull_request_id) status = 0 close_pr = False allowed_to_change_status = self._get_is_allowed_change_status( pull_request) if allowed_to_change_status: status = request.POST.get('changeset_status') close_pr = request.POST.get('save_close') text = request.POST.get('text', '').strip() or _('No comments.') if close_pr: text = _('Closing.') + '\n' + text comm = ChangesetCommentsModel().create( text=text, repo=c.db_repo.repo_id, user=c.authuser.user_id, pull_request=pull_request_id, f_path=request.POST.get('f_path'), line_no=request.POST.get('line'), status_change=(ChangesetStatus.get_status_lbl(status) if status and allowed_to_change_status else None), closing_pr=close_pr) action_logger(self.authuser, 'user_commented_pull_request:%s' % pull_request_id, c.db_repo, self.ip_addr, self.sa) if allowed_to_change_status: # get status if set ! if status: ChangesetStatusModel().set_status(c.db_repo.repo_id, status, c.authuser.user_id, comm, pull_request=pull_request_id) if close_pr: PullRequestModel().close_pull_request(pull_request_id) action_logger(self.authuser, 'user_closed_pull_request:%s' % pull_request_id, c.db_repo, self.ip_addr, self.sa) Session().commit() if not request.environ.get('HTTP_X_PARTIAL_XHR'): return redirect(pull_request.url()) data = { 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))), } if comm: c.co = comm data.update(comm.get_dict()) data.update({ 'rendered_text': render('changeset/changeset_comment_block.html') }) return data
def show(self, repo_name, pull_request_id, extra=None): repo_model = RepoModel() c.users_array = repo_model.get_users_js() c.user_groups_array = repo_model.get_user_groups_js() c.pull_request = PullRequest.get_or_404(pull_request_id) c.allowed_to_change_status = self._get_is_allowed_change_status(c.pull_request) cc_model = ChangesetCommentsModel() cs_model = ChangesetStatusModel() # pull_requests repo_name we opened it against # ie. other_repo must match if repo_name != c.pull_request.other_repo.repo_name: raise HTTPNotFound # load compare data into template context c.cs_repo = c.pull_request.org_repo (c.cs_ref_type, c.cs_ref_name, c.cs_rev) = c.pull_request.org_ref.split(':') c.a_repo = c.pull_request.other_repo (c.a_ref_type, c.a_ref_name, c.a_rev) = c.pull_request.other_ref.split(':') # a_rev is ancestor org_scm_instance = c.cs_repo.scm_instance # property with expensive cache invalidation check!!! try: c.cs_ranges = [org_scm_instance.get_changeset(x) for x in c.pull_request.revisions] except ChangesetDoesNotExistError: c.cs_ranges = [] h.flash(_('Revision %s not found in %s') % (x, c.cs_repo.repo_name), 'error') c.cs_ranges_org = None # not stored and not important and moving target - could be calculated ... revs = [ctx.revision for ctx in reversed(c.cs_ranges)] c.jsdata = graph_data(org_scm_instance, revs) c.is_range = False try: if c.a_ref_type == 'rev': # this looks like a free range where target is ancestor cs_a = org_scm_instance.get_changeset(c.a_rev) root_parents = c.cs_ranges[0].parents c.is_range = cs_a in root_parents #c.merge_root = len(root_parents) > 1 # a range starting with a merge might deserve a warning except ChangesetDoesNotExistError: # probably because c.a_rev not found pass except IndexError: # probably because c.cs_ranges is empty, probably because revisions are missing pass avail_revs = set() avail_show = [] c.cs_branch_name = c.cs_ref_name c.a_branch_name = None other_scm_instance = c.a_repo.scm_instance c.update_msg = "" c.update_msg_other = "" try: if not c.cs_ranges: c.update_msg = _('Error: changesets not found when displaying pull request from %s.') % c.cs_rev elif org_scm_instance.alias == 'hg' and c.a_ref_name != 'ancestor': if c.cs_ref_type != 'branch': c.cs_branch_name = org_scm_instance.get_changeset(c.cs_ref_name).branch # use ref_type ? c.a_branch_name = c.a_ref_name if c.a_ref_type != 'branch': try: c.a_branch_name = other_scm_instance.get_changeset(c.a_ref_name).branch # use ref_type ? except EmptyRepositoryError: c.a_branch_name = 'null' # not a branch name ... but close enough # candidates: descendants of old head that are on the right branch # and not are the old head itself ... # and nothing at all if old head is a descendant of target ref name if not c.is_range and other_scm_instance._repo.revs('present(%s)::&%s', c.cs_ranges[-1].raw_id, c.a_branch_name): c.update_msg = _('This pull request has already been merged to %s.') % c.a_branch_name elif c.pull_request.is_closed(): c.update_msg = _('This pull request has been closed and can not be updated.') else: # look for descendants of PR head on source branch in org repo avail_revs = org_scm_instance._repo.revs('%s:: & branch(%s)', revs[0], c.cs_branch_name) if len(avail_revs) > 1: # more than just revs[0] # also show changesets that not are descendants but would be merged in targethead = other_scm_instance.get_changeset(c.a_branch_name).raw_id if org_scm_instance.path != other_scm_instance.path: # Note: org_scm_instance.path must come first so all # valid revision numbers are 100% org_scm compatible # - both for avail_revs and for revset results hgrepo = unionrepo.unionrepository(org_scm_instance.baseui, org_scm_instance.path, other_scm_instance.path) else: hgrepo = org_scm_instance._repo show = set(hgrepo.revs('::%ld & !::parents(%s) & !::%s', avail_revs, revs[0], targethead)) c.update_msg = _('The following additional changes are available on %s:') % c.cs_branch_name else: show = set() avail_revs = set() # drop revs[0] c.update_msg = _('No additional changesets found for iterating on this pull request.') # TODO: handle branch heads that not are tip-most brevs = org_scm_instance._repo.revs('%s - %ld - %s', c.cs_branch_name, avail_revs, revs[0]) if brevs: # also show changesets that are on branch but neither ancestors nor descendants show.update(org_scm_instance._repo.revs('::%ld - ::%ld - ::%s', brevs, avail_revs, c.a_branch_name)) show.add(revs[0]) # make sure graph shows this so we can see how they relate c.update_msg_other = _('Note: Branch %s has another head: %s.') % (c.cs_branch_name, h.short_id(org_scm_instance.get_changeset((max(brevs))).raw_id)) avail_show = sorted(show, reverse=True) elif org_scm_instance.alias == 'git': c.cs_repo.scm_instance.get_changeset(c.cs_rev) # check it exists - raise ChangesetDoesNotExistError if not c.update_msg = _("Git pull requests don't support iterating yet.") except ChangesetDoesNotExistError: c.update_msg = _('Error: some changesets not found when displaying pull request from %s.') % c.cs_rev c.avail_revs = avail_revs c.avail_cs = [org_scm_instance.get_changeset(r) for r in avail_show] c.avail_jsdata = graph_data(org_scm_instance, avail_show) raw_ids = [x.raw_id for x in c.cs_ranges] c.cs_comments = c.cs_repo.get_comments(raw_ids) c.statuses = c.cs_repo.statuses(raw_ids) ignore_whitespace = request.GET.get('ignorews') == '1' line_context = safe_int(request.GET.get('context'), 3) c.ignorews_url = _ignorews_url c.context_url = _context_url c.fulldiff = request.GET.get('fulldiff') diff_limit = self.cut_off_limit if not c.fulldiff else None # we swap org/other ref since we run a simple diff on one repo log.debug('running diff between %s and %s in %s', c.a_rev, c.cs_rev, org_scm_instance.path) try: txtdiff = org_scm_instance.get_diff(rev1=safe_str(c.a_rev), rev2=safe_str(c.cs_rev), ignore_whitespace=ignore_whitespace, context=line_context) except ChangesetDoesNotExistError: txtdiff = _("The diff can't be shown - the PR revisions could not be found.") diff_processor = diffs.DiffProcessor(txtdiff or '', format='gitdiff', diff_limit=diff_limit) _parsed = diff_processor.prepare() c.limited_diff = False if isinstance(_parsed, LimitedDiffContainer): c.limited_diff = True c.file_diff_data = [] c.lines_added = 0 c.lines_deleted = 0 for f in _parsed: st = f['stats'] c.lines_added += st['added'] c.lines_deleted += st['deleted'] filename = f['filename'] fid = h.FID('', filename) diff = diff_processor.as_html(enable_comments=True, parsed_lines=[f]) c.file_diff_data.append((fid, None, f['operation'], f['old_filename'], filename, diff, st)) # inline comments c.inline_cnt = 0 c.inline_comments = cc_model.get_inline_comments( c.db_repo.repo_id, pull_request=pull_request_id) # count inline comments for __, lines in c.inline_comments: for comments in lines.values(): c.inline_cnt += len(comments) # comments c.comments = cc_model.get_comments(c.db_repo.repo_id, pull_request=pull_request_id) # (badly named) pull-request status calculation based on reviewer votes (c.pull_request_reviewers, c.pull_request_pending_reviewers, c.current_voting_result, ) = cs_model.calculate_pull_request_result(c.pull_request) c.changeset_statuses = ChangesetStatus.STATUSES c.is_ajax_preview = False c.ancestors = None # [c.a_rev] ... but that is shown in an other way return render('/pullrequests/pullrequest_show.html')
def comment(self, repo_name, pull_request_id): pull_request = PullRequest.get_or_404(pull_request_id) status = request.POST.get('changeset_status') close_pr = request.POST.get('save_close') delete = request.POST.get('save_delete') f_path = request.POST.get('f_path') line_no = request.POST.get('line') if (status or close_pr or delete) and (f_path or line_no): # status votes and closing is only possible in general comments raise HTTPBadRequest() allowed_to_change_status = self._get_is_allowed_change_status(pull_request) if not allowed_to_change_status: if status or close_pr: h.flash(_('No permission to change pull request status'), 'error') raise HTTPForbidden() if delete == "delete": if (pull_request.owner_id == request.authuser.user_id or h.HasPermissionAny('hg.admin')() or h.HasRepoPermissionLevel('admin')(pull_request.org_repo.repo_name) or h.HasRepoPermissionLevel('admin')(pull_request.other_repo.repo_name) ) and not pull_request.is_closed(): PullRequestModel().delete(pull_request) Session().commit() h.flash(_('Successfully deleted pull request %s') % pull_request_id, category='success') return { 'location': url('my_pullrequests'), # or repo pr list? } raise HTTPFound(location=url('my_pullrequests')) # or repo pr list? raise HTTPForbidden() text = request.POST.get('text', '').strip() comment = create_comment( text, status, pull_request_id=pull_request_id, f_path=f_path, line_no=line_no, closing_pr=close_pr, ) action_logger(request.authuser, 'user_commented_pull_request:%s' % pull_request_id, c.db_repo, request.ip_addr) if status: ChangesetStatusModel().set_status( c.db_repo.repo_id, status, request.authuser.user_id, comment, pull_request=pull_request_id ) if close_pr: PullRequestModel().close_pull_request(pull_request_id) action_logger(request.authuser, 'user_closed_pull_request:%s' % pull_request_id, c.db_repo, request.ip_addr) Session().commit() if not request.environ.get('HTTP_X_PARTIAL_XHR'): raise HTTPFound(location=pull_request.url()) data = { 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))), } if comment is not None: c.comment = comment data.update(comment.get_dict()) data.update({'rendered_text': render('changeset/changeset_comment_block.html')}) return data
def comment(self, repo_name, pull_request_id): pull_request = PullRequest.get_or_404(pull_request_id) status = 0 close_pr = False allowed_to_change_status = self._get_is_allowed_change_status(pull_request) if allowed_to_change_status: status = request.POST.get('changeset_status') close_pr = request.POST.get('save_close') text = request.POST.get('text', '').strip() or _('No comments.') if close_pr: text = _('Closing.') + '\n' + text comm = ChangesetCommentsModel().create( text=text, repo=c.db_repo.repo_id, user=c.authuser.user_id, pull_request=pull_request_id, f_path=request.POST.get('f_path'), line_no=request.POST.get('line'), status_change=(ChangesetStatus.get_status_lbl(status) if status and allowed_to_change_status else None), closing_pr=close_pr ) action_logger(self.authuser, 'user_commented_pull_request:%s' % pull_request_id, c.db_repo, self.ip_addr, self.sa) if allowed_to_change_status: # get status if set ! if status: ChangesetStatusModel().set_status( c.db_repo.repo_id, status, c.authuser.user_id, comm, pull_request=pull_request_id ) if close_pr: PullRequestModel().close_pull_request(pull_request_id) action_logger(self.authuser, 'user_closed_pull_request:%s' % pull_request_id, c.db_repo, self.ip_addr, self.sa) Session().commit() if not request.environ.get('HTTP_X_PARTIAL_XHR'): return redirect(pull_request.url()) data = { 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))), } if comm: c.co = comm data.update(comm.get_dict()) data.update({'rendered_text': render('changeset/changeset_comment_block.html')}) return data
def create(self, text, repo, author, revision=None, pull_request=None, f_path=None, line_no=None, status_change=None, closing_pr=False, send_email=True): """ Creates a new comment for either a changeset or a pull request. status_change and closing_pr is only for the optional email. Returns the created comment. """ if not status_change and not text: log.warning('Missing text for comment, skipping...') return None repo = Repository.guess_instance(repo) author = User.guess_instance(author) comment = ChangesetComment() comment.repo = repo comment.author = author comment.text = text comment.f_path = f_path comment.line_no = line_no if revision is not None: comment.revision = revision elif pull_request is not None: pull_request = PullRequest.guess_instance(pull_request) comment.pull_request = pull_request else: raise Exception('Please specify revision or pull_request_id') Session().add(comment) Session().flush() if send_email: (subj, body, recipients, notification_type, email_kwargs) = self._get_notification_data( repo, comment, author, comment_text=text, line_no=line_no, revision=revision, pull_request=pull_request, status_change=status_change, closing_pr=closing_pr) email_kwargs['is_mention'] = False # create notification objects, and emails NotificationModel().create( created_by=author, subject=subj, body=body, recipients=recipients, type_=notification_type, email_kwargs=email_kwargs, ) mention_recipients = extract_mentioned_users(body).difference( recipients) if mention_recipients: email_kwargs['is_mention'] = True subj = _('[Mention]') + ' ' + subj # FIXME: this subject is wrong and unused! NotificationModel().create(created_by=author, subject=subj, body=body, recipients=mention_recipients, type_=notification_type, email_kwargs=email_kwargs) return comment
def _before(self, *args, **kwargs): """ _before is called before controller methods and after __call__ """ if request.needs_csrf_check: # CSRF protection: Whenever a request has ambient authority (whether # through a session cookie or its origin IP address), it must include # the correct token, unless the HTTP method is GET or HEAD (and thus # guaranteed to be side effect free. In practice, the only situation # where we allow side effects without ambient authority is when the # authority comes from an API key; and that is handled above. from kallithea.lib import helpers as h token = request.POST.get(h.session_csrf_secret_name) if not token or token != h.session_csrf_secret_token(): log.error('CSRF check failed') raise webob.exc.HTTPForbidden() c.kallithea_version = __version__ rc_config = Setting.get_app_settings() # Visual options c.visual = AttributeDict({}) ## DB stored c.visual.show_public_icon = str2bool(rc_config.get('show_public_icon')) c.visual.show_private_icon = str2bool( rc_config.get('show_private_icon')) c.visual.stylify_metalabels = str2bool( rc_config.get('stylify_metalabels')) c.visual.page_size = safe_int(rc_config.get('dashboard_items', 100)) c.visual.admin_grid_items = safe_int( rc_config.get('admin_grid_items', 100)) c.visual.repository_fields = str2bool( rc_config.get('repository_fields')) c.visual.show_version = str2bool(rc_config.get('show_version')) c.visual.use_gravatar = str2bool(rc_config.get('use_gravatar')) c.visual.gravatar_url = rc_config.get('gravatar_url') c.ga_code = rc_config.get('ga_code') # TODO: replace undocumented backwards compatibility hack with db upgrade and rename ga_code if c.ga_code and '<' not in c.ga_code: c.ga_code = '''<script type="text/javascript"> var _gaq = _gaq || []; _gaq.push(['_setAccount', '%s']); _gaq.push(['_trackPageview']); (function() { var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true; ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'; var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); })(); </script>''' % c.ga_code c.site_name = rc_config.get('title') c.clone_uri_tmpl = rc_config.get( 'clone_uri_tmpl') or Repository.DEFAULT_CLONE_URI c.clone_ssh_tmpl = rc_config.get( 'clone_ssh_tmpl') or Repository.DEFAULT_CLONE_SSH ## INI stored c.visual.allow_repo_location_change = str2bool( config.get('allow_repo_location_change', True)) c.visual.allow_custom_hooks_settings = str2bool( config.get('allow_custom_hooks_settings', True)) c.ssh_enabled = str2bool(config.get('ssh_enabled', False)) c.instance_id = config.get('instance_id') c.issues_url = config.get('bugtracker', url('issues_url')) # END CONFIG VARS c.repo_name = get_repo_slug(request) # can be empty c.backends = list(BACKENDS) self.cut_off_limit = safe_int(config.get('cut_off_limit')) c.my_pr_count = PullRequest.query(reviewer_id=request.authuser.user_id, include_closed=False).count() self.scm_model = ScmModel()
def close_pull_request(self, pull_request): pull_request = PullRequest.guess_instance(pull_request) pull_request.status = PullRequest.STATUS_CLOSED pull_request.updated_on = datetime.datetime.now()
def export(self, repo_name, pull_request_id, fname, **kwargs): ext = fname.split('.')[1] export_name = '{repo}-{pr_id}.{ext}'.format( repo=safe_str(repo_name.replace('/', '_')), pr_id=safe_str(pull_request_id), ext=safe_str(ext)) fd, export_path = mkstemp() log.debug( 'Creating new temp export in {path}'.format(path=export_path)) try: pr = PullRequest.get(pull_request_id) if repo_name != pr.other_repo.repo_name: raise RepositoryError except Exception as e: log.error(e) return _('Pull request #{id} not found').format(id=pull_request_id) cc_model = ChangesetCommentsModel() inline_comments = cc_model.get_inline_comments( pr.org_repo_id, pull_request=pull_request_id) file_comments = {} for f_path, lines in inline_comments: file_comments[f_path] = lines sorted_file_comments_by_name = sorted(file_comments.items(), key=lambda x: x[0], reverse=False) general_comments = cc_model.get_comments(pr.org_repo_id, pull_request=pull_request_id) wb = Workbook() ws = wb.create_sheet(_('comments'), 0) ws['A1'].value = _('File path') ws.column_dimensions['A'].width = 3.0 ws['B1'].value = _('Comment ID') ws['C1'].value = _('Line no (old)') ws['D1'].value = _('Line no (new)') ws['E1'].value = _('Author') ws['F1'].value = _('Status') ws['G1'].value = _('Comment') ws.column_dimensions['G'].width = 60.0 ws['H1'].value = _('Opinion') ws.column_dimensions['H'].width = 60.0 ws['I1'].value = _('Retouch') ws['J1'].value = _('Priority') ws['K1'].value = _('Deadline') align_rot_90 = Alignment(text_rotation=90) align_wrap = Alignment(wrap_text=True, vertical='top') rows = 2 for f_path, lines in sorted_file_comments_by_name: sorted_inline_comments_by_lineno = sorted( lines.iteritems(), key=lambda (line_no, comments): int(line_no[1:]), reverse=False) base_rows = rows for line_no, comments in sorted_inline_comments_by_lineno: top_comments = {} reply_comments = {} for co in comments: m = re.match(r'`replyto comment-(?P<replyto>[\d]+)', co.text) if m: replyto = int(m.group('replyto')) if reply_comments.get(replyto, None) is None: reply_comments[replyto] = [] reply_comments[replyto].append(co) else: top_comments[co.comment_id] = co def _make_threaded_message(comment_id, stops=0): message = '\n' if stops > 0 else '' for reply in reply_comments.get(comment_id, []): text = re.sub( r'`replyto comment-(?:[\d]+) <#comment-(?:[\d]+)>`_ :', '', reply.text) indent = ' ' * 2 * stops message += '{indent}commented by {author}:\n'.format( indent=indent, author=reply.author.username) message += '\n'.join(indent + line for line in text.splitlines() if len(line) > 0) message += '\n{indent}----'.format(indent=indent) message += _make_threaded_message( reply.comment_id, stops + 1) return message for comment_id, co in top_comments.items(): link = pr.url( canonical=True, anchor='comment-{id}'.format(id=co.comment_id)) ws['B{row}'.format(row=rows)].value = co.comment_id ws['B{row}'.format(row=rows)].hyperlink = link if co.line_no.startswith('o'): ws['C{row}'.format(row=rows)].value = co.line_no[1:] else: ws['D{row}'.format(row=rows)].value = co.line_no[1:] ws['E{row}'.format(row=rows)].value = co.author.username if co.status_change: ws['F{row}'.format(row=rows)].value = str( h.changeset_status_lbl(co.status_change[0].status)) ws['G{row}'.format(row=rows)].value = co.text.replace( '@', '(at)') ws['G{row}'.format(row=rows)].alignment = align_wrap ws['H{row}'.format( row=rows)].value = _make_threaded_message( comment_id).replace('@', '(at)') ws['H{row}'.format(row=rows)].alignment = align_wrap rows += 1 ws.merge_cells('A{start}:A{end}'.format(start=base_rows, end=rows - 1)) for i in range(rows - base_rows): ws['A{row}'.format(row=base_rows + i)].value = f_path ws['A{start}'.format(start=base_rows)].alignment = align_rot_90 ws['A{row}'.format(row=rows)].value = 'General' base_rows = rows top_comments = {} reply_comments = {} for co in general_comments: m = re.match(r'`replyto comment-(?P<replyto>[\d]+)', co.text) if m: replyto = int(m.group('replyto')) if reply_comments.get(replyto, None) is None: reply_comments[replyto] = [] reply_comments[replyto].append(co) else: top_comments[co.comment_id] = co def _make_threaded_message(comment_id, stops=0): message = '\n' if stops > 0 else '' for reply in reply_comments.get(comment_id, []): text = re.sub( r'`replyto comment-(?:[\d]+) <#comment-(?:[\d]+)>`_ :', '', reply.text) indent = ' ' * 2 * stops message += '{indent}commented by {author}:\n'.format( indent=indent, author=reply.author.username) message += '\n'.join(indent + line for line in text.splitlines() if len(line) > 0) message += '\n{indent}----'.format(indent=indent) message += _make_threaded_message(reply.comment_id, stops + 1) return message for comment_id, co in top_comments.items(): link = pr.url(canonical=True, anchor='comment-{id}'.format(id=co.comment_id)) ws['B{row}'.format(row=rows)].value = co.comment_id ws['B{row}'.format(row=rows)].hyperlink = link ws['E{row}'.format(row=rows)].value = co.author.username if co.status_change: ws['F{row}'.format(row=rows)].value = str( h.changeset_status_lbl(co.status_change[0].status)) ws['G{row}'.format(row=rows)].value = co.text.replace('@', '(at)') ws['G{row}'.format(row=rows)].alignment = align_wrap ws['H{row}'.format( row=rows)].value = _make_threaded_message(comment_id).replace( '@', '(at)') ws['H{row}'.format(row=rows)].alignment = align_wrap rows += 1 ws.merge_cells('A{start}:A{end}'.format(start=base_rows, end=rows - 1)) for i in range(rows - base_rows): ws['A{row}'.format(row=base_rows + i)].value = 'General' ws['A{start}'.format(start=base_rows)].alignment = align_rot_90 with os.fdopen(fd, 'wb') as s: s.write(save_virtual_workbook(wb)) def get_chunked_export(export_path): stream = open(export_path, 'rb') while True: data = stream.read(16 * 1024) if not data: break yield data stream.close() log.debug('Destroying temp export %s', export_path) os.remove(export_path) response.content_disposition = str('attachment; filename=%s' % (export_name)) response.content_type = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' return get_chunked_export(export_path)
def show(self, repo_name, pull_request_id, extra=None): repo_model = RepoModel() c.users_array = repo_model.get_users_js() c.user_groups_array = repo_model.get_user_groups_js() c.pull_request = PullRequest.get_or_404(pull_request_id) c.allowed_to_change_status = self._get_is_allowed_change_status(c.pull_request) cc_model = ChangesetCommentsModel() cs_model = ChangesetStatusModel() # pull_requests repo_name we opened it against # ie. other_repo must match if repo_name != c.pull_request.other_repo.repo_name: raise HTTPNotFound # load compare data into template context c.cs_repo = c.pull_request.org_repo (c.cs_ref_type, c.cs_ref_name, c.cs_rev) = c.pull_request.org_ref.split(':') c.a_repo = c.pull_request.other_repo (c.a_ref_type, c.a_ref_name, c.a_rev) = c.pull_request.other_ref.split(':') # other_rev is ancestor org_scm_instance = c.cs_repo.scm_instance # property with expensive cache invalidation check!!! c.cs_repo = c.cs_repo try: c.cs_ranges = [org_scm_instance.get_changeset(x) for x in c.pull_request.revisions] except ChangesetDoesNotExistError: c.cs_ranges = [] c.cs_ranges_org = None # not stored and not important and moving target - could be calculated ... revs = [ctx.revision for ctx in reversed(c.cs_ranges)] c.jsdata = json.dumps(graph_data(org_scm_instance, revs)) c.is_range = False if c.a_ref_type == 'rev': # this looks like a free range where target is ancestor cs_a = org_scm_instance.get_changeset(c.a_rev) root_parents = c.cs_ranges[0].parents c.is_range = cs_a in root_parents #c.merge_root = len(root_parents) > 1 # a range starting with a merge might deserve a warning avail_revs = set() avail_show = [] c.cs_branch_name = c.cs_ref_name other_scm_instance = c.a_repo.scm_instance c.update_msg = "" c.update_msg_other = "" try: if org_scm_instance.alias == 'hg' and c.a_ref_name != 'ancestor': if c.cs_ref_type != 'branch': c.cs_branch_name = org_scm_instance.get_changeset(c.cs_ref_name).branch # use ref_type ? c.a_branch_name = c.a_ref_name if c.a_ref_type != 'branch': try: c.a_branch_name = other_scm_instance.get_changeset(c.a_ref_name).branch # use ref_type ? except EmptyRepositoryError: c.a_branch_name = 'null' # not a branch name ... but close enough # candidates: descendants of old head that are on the right branch # and not are the old head itself ... # and nothing at all if old head is a descendant of target ref name if not c.is_range and other_scm_instance._repo.revs('present(%s)::&%s', c.cs_ranges[-1].raw_id, c.a_branch_name): c.update_msg = _('This pull request has already been merged to %s.') % c.a_branch_name elif c.pull_request.is_closed(): c.update_msg = _('This pull request has been closed and can not be updated.') else: # look for descendants of PR head on source branch in org repo avail_revs = org_scm_instance._repo.revs('%s:: & branch(%s)', revs[0], c.cs_branch_name) if len(avail_revs) > 1: # more than just revs[0] # also show changesets that not are descendants but would be merged in targethead = other_scm_instance.get_changeset(c.a_branch_name).raw_id if org_scm_instance.path != other_scm_instance.path: # Note: org_scm_instance.path must come first so all # valid revision numbers are 100% org_scm compatible # - both for avail_revs and for revset results hgrepo = unionrepo.unionrepository(org_scm_instance.baseui, org_scm_instance.path, other_scm_instance.path) else: hgrepo = org_scm_instance._repo show = set(hgrepo.revs('::%ld & !::parents(%s) & !::%s', avail_revs, revs[0], targethead)) c.update_msg = _('The following changes are available on %s:') % c.cs_branch_name else: show = set() avail_revs = set() # drop revs[0] c.update_msg = _('No changesets found for updating this pull request.') # TODO: handle branch heads that not are tip-most brevs = org_scm_instance._repo.revs('%s - %ld - %s', c.cs_branch_name, avail_revs, revs[0]) if brevs: # also show changesets that are on branch but neither ancestors nor descendants show.update(org_scm_instance._repo.revs('::%ld - ::%ld - ::%s', brevs, avail_revs, c.a_branch_name)) show.add(revs[0]) # make sure graph shows this so we can see how they relate c.update_msg_other = _('Note: Branch %s has another head: %s.') % (c.cs_branch_name, h.short_id(org_scm_instance.get_changeset((max(brevs))).raw_id)) avail_show = sorted(show, reverse=True) elif org_scm_instance.alias == 'git': c.cs_repo.scm_instance.get_changeset(c.cs_rev) # check it exists - raise ChangesetDoesNotExistError if not c.update_msg = _("Git pull requests don't support updates yet.") except ChangesetDoesNotExistError: c.update_msg = _('Error: revision %s was not found. Please create a new pull request!') % c.cs_rev c.avail_revs = avail_revs c.avail_cs = [org_scm_instance.get_changeset(r) for r in avail_show] c.avail_jsdata = json.dumps(graph_data(org_scm_instance, avail_show)) raw_ids = [x.raw_id for x in c.cs_ranges] c.cs_comments = c.cs_repo.get_comments(raw_ids) c.statuses = c.cs_repo.statuses(raw_ids) ignore_whitespace = request.GET.get('ignorews') == '1' line_context = request.GET.get('context', 3) c.ignorews_url = _ignorews_url c.context_url = _context_url c.fulldiff = request.GET.get('fulldiff') diff_limit = self.cut_off_limit if not c.fulldiff else None # we swap org/other ref since we run a simple diff on one repo log.debug('running diff between %s and %s in %s', c.a_rev, c.cs_rev, org_scm_instance.path) try: txtdiff = org_scm_instance.get_diff(rev1=safe_str(c.a_rev), rev2=safe_str(c.cs_rev), ignore_whitespace=ignore_whitespace, context=line_context) except ChangesetDoesNotExistError: txtdiff = _("The diff can't be shown - the PR revisions could not be found.") diff_processor = diffs.DiffProcessor(txtdiff or '', format='gitdiff', diff_limit=diff_limit) _parsed = diff_processor.prepare() c.limited_diff = False if isinstance(_parsed, LimitedDiffContainer): c.limited_diff = True c.files = [] c.changes = {} c.lines_added = 0 c.lines_deleted = 0 for f in _parsed: st = f['stats'] c.lines_added += st['added'] c.lines_deleted += st['deleted'] fid = h.FID('', f['filename']) c.files.append([fid, f['operation'], f['filename'], f['stats']]) htmldiff = diff_processor.as_html(enable_comments=True, parsed_lines=[f]) c.changes[fid] = [f['operation'], f['filename'], htmldiff] # inline comments c.inline_cnt = 0 c.inline_comments = cc_model.get_inline_comments( c.db_repo.repo_id, pull_request=pull_request_id) # count inline comments for __, lines in c.inline_comments: for comments in lines.values(): c.inline_cnt += len(comments) # comments c.comments = cc_model.get_comments(c.db_repo.repo_id, pull_request=pull_request_id) # (badly named) pull-request status calculation based on reviewer votes (c.pull_request_reviewers, c.pull_request_pending_reviewers, c.current_voting_result, ) = cs_model.calculate_pull_request_result(c.pull_request) c.changeset_statuses = ChangesetStatus.STATUSES c.as_form = False c.ancestor = None # there is one - but right here we don't know which return render('/pullrequests/pullrequest_show.html')
def comment(self, repo_name, pull_request_id): pull_request = PullRequest.get_or_404(pull_request_id) status = request.POST.get('changeset_status') close_pr = request.POST.get('save_close') delete = request.POST.get('save_delete') f_path = request.POST.get('f_path') line_no = request.POST.get('line') if (status or close_pr or delete) and (f_path or line_no): # status votes and closing is only possible in general comments raise HTTPBadRequest() allowed_to_change_status = self._get_is_allowed_change_status(pull_request) if not allowed_to_change_status: if status or close_pr: h.flash(_('No permission to change pull request status'), 'error') raise HTTPForbidden() if delete == "delete": if (pull_request.owner.user_id == c.authuser.user_id or h.HasPermissionAny('hg.admin')() or h.HasRepoPermissionAny('repository.admin')(pull_request.org_repo.repo_name) or h.HasRepoPermissionAny('repository.admin')(pull_request.other_repo.repo_name) ) and not pull_request.is_closed(): PullRequestModel().delete(pull_request) Session().commit() h.flash(_('Successfully deleted pull request %s') % pull_request_id, category='success') return { 'location': url('my_pullrequests'), # or repo pr list? } raise HTTPFound(location=url('my_pullrequests')) # or repo pr list? raise HTTPForbidden() text = request.POST.get('text', '').strip() if close_pr: text = _('Closing.') + '\n' + text comment = create_comment( text, status, pull_request_id=pull_request_id, f_path=f_path, line_no=line_no, closing_pr=close_pr, ) action_logger(self.authuser, 'user_commented_pull_request:%s' % pull_request_id, c.db_repo, self.ip_addr, self.sa) if status: ChangesetStatusModel().set_status( c.db_repo.repo_id, status, c.authuser.user_id, comment, pull_request=pull_request_id ) if close_pr: PullRequestModel().close_pull_request(pull_request_id) action_logger(self.authuser, 'user_closed_pull_request:%s' % pull_request_id, c.db_repo, self.ip_addr, self.sa) Session().commit() if not request.environ.get('HTTP_X_PARTIAL_XHR'): raise HTTPFound(location=pull_request.url()) data = { 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))), } if comment is not None: c.comment = comment data.update(comment.get_dict()) data.update({'rendered_text': render('changeset/changeset_comment_block.html')}) return data
def get_pullrequest_cnt_for_user(self, user): return PullRequest.query()\ .join(PullRequestReviewers)\ .filter(PullRequestReviewers.user_id == user)\ .filter(PullRequest.status != PullRequest.STATUS_CLOSED)\ .count()
def create(self, text, repo, author, revision=None, pull_request=None, f_path=None, line_no=None, status_change=None, closing_pr=False, send_email=True): """ Creates a new comment for either a changeset or a pull request. status_change and closing_pr is only for the optional email. Returns the created comment. """ if not status_change and not text: log.warning('Missing text for comment, skipping...') return None repo = Repository.guess_instance(repo) author = User.guess_instance(author) comment = ChangesetComment() comment.repo = repo comment.author = author comment.text = text comment.f_path = f_path comment.line_no = line_no if revision is not None: comment.revision = revision elif pull_request is not None: pull_request = PullRequest.guess_instance(pull_request) comment.pull_request = pull_request else: raise Exception('Please specify revision or pull_request_id') Session().add(comment) Session().flush() if send_email: (subj, body, recipients, notification_type, email_kwargs) = self._get_notification_data( repo, comment, author, comment_text=text, line_no=line_no, revision=revision, pull_request=pull_request, status_change=status_change, closing_pr=closing_pr) email_kwargs['is_mention'] = False # create notification objects, and emails NotificationModel().create( created_by=author, subject=subj, body=body, recipients=recipients, type_=notification_type, email_kwargs=email_kwargs, ) mention_recipients = extract_mentioned_users(body).difference(recipients) if mention_recipients: email_kwargs['is_mention'] = True subj = _('[Mention]') + ' ' + subj # FIXME: this subject is wrong and unused! NotificationModel().create( created_by=author, subject=subj, body=body, recipients=mention_recipients, type_=notification_type, email_kwargs=email_kwargs ) return comment
def get_pull_requests(self, repo): repo = Repository.guess_instance(repo) return PullRequest.query() \ .filter(PullRequest.other_repo == repo) \ .filter(PullRequest.status != PullRequest.STATUS_CLOSED).count()