Changeset - 779d43be59c6
[Not reviewed]
default
0 3 0
Mads Kiilerich - 10 years ago 2016-01-20 01:47:11
madski@unity3d.com
pullrequests: make it possible to delete pull requests instead of commenting

Deleting a pull request might however be a bad idea - the reviews on the PR
will be lost.
3 files changed with 55 insertions and 10 deletions:
0 comments (0 inline, 0 general)
kallithea/controllers/pullrequests.py
Show inline comments
 
@@ -329,464 +329,481 @@ class PullrequestsController(BaseRepoCon
 
         other_ref_name,
 
         other_rev) = other_ref.split(':')
 

	
 
        cs_ranges, _cs_ranges_not, ancestor_rev = \
 
            CompareController._get_changesets(org_repo.scm_instance.alias,
 
                                              other_repo.scm_instance, other_rev, # org and other "swapped"
 
                                              org_repo.scm_instance, org_rev,
 
                                              )
 
        if ancestor_rev is None:
 
            ancestor_rev = org_repo.scm_instance.EMPTY_CHANGESET
 
        revisions = [cs_.raw_id for cs_ in cs_ranges]
 

	
 
        # hack: ancestor_rev is not an other_rev but we want to show the
 
        # requested destination and have the exact ancestor
 
        other_ref = '%s:%s:%s' % (other_ref_type, other_ref_name, ancestor_rev)
 

	
 
        reviewers = _form['review_members']
 

	
 
        title = _form['pullrequest_title']
 
        if not title:
 
            if org_repo_name == other_repo_name:
 
                title = '%s to %s' % (h.short_ref(org_ref_type, org_ref_name),
 
                                      h.short_ref(other_ref_type, other_ref_name))
 
            else:
 
                title = '%s#%s to %s#%s' % (org_repo_name, h.short_ref(org_ref_type, org_ref_name),
 
                                            other_repo_name, h.short_ref(other_ref_type, other_ref_name))
 
        description = _form['pullrequest_desc'].strip() or _('No description')
 
        try:
 
            pull_request = PullRequestModel().create(
 
                self.authuser.user_id, org_repo_name, org_ref, other_repo_name,
 
                other_ref, revisions, reviewers, title, description
 
            )
 
            Session().commit()
 
            h.flash(_('Successfully opened new pull request'),
 
                    category='success')
 
        except UserInvalidException as u:
 
            h.flash(_('Invalid reviewer "%s" specified') % u, category='error')
 
            raise HTTPBadRequest()
 
        except Exception:
 
            h.flash(_('Error occurred while creating pull request'),
 
                    category='error')
 
            log.error(traceback.format_exc())
 
            raise HTTPFound(location=url('pullrequest_home', repo_name=repo_name))
 

	
 
        raise HTTPFound(location=pull_request.url())
 

	
 
    def create_update(self, old_pull_request, updaterev, title, description, reviewers_ids):
 
        org_repo = RepoModel()._get_repo(old_pull_request.org_repo.repo_name)
 
        org_ref_type, org_ref_name, org_rev = old_pull_request.org_ref.split(':')
 
        new_org_rev = self._get_ref_rev(org_repo, 'rev', updaterev)
 

	
 
        other_repo = RepoModel()._get_repo(old_pull_request.other_repo.repo_name)
 
        other_ref_type, other_ref_name, other_rev = old_pull_request.other_ref.split(':') # other_rev is ancestor
 
        #assert other_ref_type == 'branch', other_ref_type # TODO: what if not?
 
        new_other_rev = self._get_ref_rev(other_repo, other_ref_type, other_ref_name)
 

	
 
        cs_ranges, _cs_ranges_not, ancestor_rev = CompareController._get_changesets(org_repo.scm_instance.alias,
 
            other_repo.scm_instance, new_other_rev, # org and other "swapped"
 
            org_repo.scm_instance, new_org_rev)
 

	
 
        old_revisions = set(old_pull_request.revisions)
 
        revisions = [cs.raw_id for cs in cs_ranges]
 
        new_revisions = [r for r in revisions if r not in old_revisions]
 
        lost = old_revisions.difference(revisions)
 

	
 
        infos = ['This is an update of %s "%s".' %
 
                 (h.canonical_url('pullrequest_show', repo_name=old_pull_request.other_repo.repo_name,
 
                      pull_request_id=old_pull_request.pull_request_id),
 
                  old_pull_request.title)]
 

	
 
        if lost:
 
            infos.append(_('Missing changesets since the previous pull request:'))
 
            for r in old_pull_request.revisions:
 
                if r in lost:
 
                    rev_desc = org_repo.get_changeset(r).message.split('\n')[0]
 
                    infos.append('  %s "%s"' % (h.short_id(r), rev_desc))
 

	
 
        if new_revisions:
 
            infos.append(_('New changesets on %s %s since the previous pull request:') % (org_ref_type, org_ref_name))
 
            for r in reversed(revisions):
 
                if r in new_revisions:
 
                    rev_desc = org_repo.get_changeset(r).message.split('\n')[0]
 
                    infos.append('  %s %s' % (h.short_id(r), h.shorter(rev_desc, 80)))
 

	
 
            if ancestor_rev == other_rev:
 
                infos.append(_("Ancestor didn't change - show diff since previous version:"))
 
                infos.append(h.canonical_url('compare_url',
 
                                 repo_name=org_repo.repo_name, # other_repo is always same as repo_name
 
                                 org_ref_type='rev', org_ref_name=h.short_id(org_rev), # use old org_rev as base
 
                                 other_ref_type='rev', other_ref_name=h.short_id(new_org_rev),
 
                                 )) # note: linear diff, merge or not doesn't matter
 
            else:
 
                infos.append(_('This pull request is based on another %s revision and there is no simple diff.') % other_ref_name)
 
        else:
 
           infos.append(_('No changes found on %s %s since previous version.') % (org_ref_type, org_ref_name))
 
           # TODO: fail?
 

	
 
        # hack: ancestor_rev is not an other_ref but we want to show the
 
        # requested destination and have the exact ancestor
 
        new_other_ref = '%s:%s:%s' % (other_ref_type, other_ref_name, ancestor_rev)
 
        new_org_ref = '%s:%s:%s' % (org_ref_type, org_ref_name, new_org_rev)
 

	
 
        try:
 
            title, old_v = re.match(r'(.*)\(v(\d+)\)\s*$', title).groups()
 
            v = int(old_v) + 1
 
        except (AttributeError, ValueError):
 
            v = 2
 
        title = '%s (v%s)' % (title.strip(), v)
 

	
 
        # using a mail-like separator, insert new update info at the top of the list
 
        descriptions = description.replace('\r\n', '\n').split('\n-- \n', 1)
 
        description = descriptions[0].strip() + '\n\n-- \n' + '\n'.join(infos)
 
        if len(descriptions) > 1:
 
            description += '\n\n' + descriptions[1].strip()
 

	
 
        try:
 
            pull_request = PullRequestModel().create(
 
                self.authuser.user_id,
 
                old_pull_request.org_repo.repo_name, new_org_ref,
 
                old_pull_request.other_repo.repo_name, new_other_ref,
 
                revisions, reviewers_ids, title, description
 
            )
 
        except UserInvalidException as u:
 
            h.flash(_('Invalid reviewer "%s" specified') % u, category='error')
 
            raise HTTPBadRequest()
 
        except Exception:
 
            h.flash(_('Error occurred while creating pull request'),
 
                    category='error')
 
            log.error(traceback.format_exc())
 
            raise HTTPFound(location=old_pull_request.url())
 

	
 
        ChangesetCommentsModel().create(
 
            text=_('Closed, replaced by %s .') % pull_request.url(canonical=True),
 
            repo=old_pull_request.other_repo.repo_id,
 
            user=c.authuser.user_id,
 
            pull_request=old_pull_request.pull_request_id,
 
            closing_pr=True)
 
        PullRequestModel().close_pull_request(old_pull_request.pull_request_id)
 

	
 
        Session().commit()
 
        h.flash(_('Pull request update created'),
 
                category='success')
 

	
 
        raise HTTPFound(location=pull_request.url())
 

	
 
    # pullrequest_post for PR editing
 
    @LoginRequired()
 
    @NotAnonymous()
 
    @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
 
                                   'repository.admin')
 
    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.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')
 
        pull_request.owner = User.get_by_username(_form['owner'])
 
        user = User.get(c.authuser.user_id)
 
        try:
 
            PullRequestModel().mention_from_description(user, pull_request, old_description)
 
            PullRequestModel().update_reviewers(user, pull_request_id, reviewers_ids)
 
        except UserInvalidException as u:
 
            h.flash(_('Invalid reviewer "%s" specified') % u, category='error')
 
            raise HTTPBadRequest()
 

	
 
        Session().commit()
 
        h.flash(_('Pull request updated'), category='success')
 

	
 
        raise HTTPFound(location=pull_request.url())
 

	
 
    @LoginRequired()
 
    @NotAnonymous()
 
    @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
 
                                   'repository.admin')
 
    @jsonify
 
    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()
 

	
 
    @LoginRequired()
 
    @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
 
                                   'repository.admin')
 
    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
 
        c.cs_ranges = [org_scm_instance.get_changeset(x) for x in c.pull_request.revisions]
 
        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 = ""
 
        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 & !::%s & !::%s',
 
                                           avail_revs, revs[0], targethead))
 
                    c.update_msg = _('This pull request can be updated with changes 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.update_msg = _("Git pull requests don't support updates yet.")
 

	
 
        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)
 
        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)
 

	
 
        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')
 

	
 
    @LoginRequired()
 
    @NotAnonymous()
 
    @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
 
                                   'repository.admin')
 
    @jsonify
 
    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) and (f_path or line_no):
 
        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
 

	
 
    @LoginRequired()
 
    @NotAnonymous()
 
    @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
 
                                   'repository.admin')
 
    @jsonify
 
    def delete_comment(self, repo_name, comment_id):
 
        co = ChangesetComment.get(comment_id)
 
        if co.pull_request.is_closed():
 
            #don't allow deleting comments on closed pull request
 
            raise HTTPForbidden()
 

	
 
        owner = co.author.user_id == c.authuser.user_id
 
        repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
 
        if h.HasPermissionAny('hg.admin') or repo_admin or owner:
 
            ChangesetCommentsModel().delete(comment=co)
 
            Session().commit()
 
            return True
 
        else:
 
            raise HTTPForbidden()
kallithea/public/js/base.js
Show inline comments
 
@@ -319,791 +319,812 @@ var YUE = YAHOO.util.Event;
 

	
 
/* Invoke all functions in callbacks */
 
var _run_callbacks = function(callbacks){
 
    if (callbacks !== undefined){
 
        var _l = callbacks.length;
 
        for (var i=0;i<_l;i++){
 
            var func = callbacks[i];
 
            if(typeof(func)=='function'){
 
                try{
 
                    func();
 
                }catch (err){};
 
            }
 
        }
 
    }
 
}
 

	
 
/**
 
 * turns objects into GET query string
 
 */
 
var _toQueryString = function(o) {
 
    if(typeof o !== 'object') {
 
        return false;
 
    }
 
    var _p, _qs = [];
 
    for(_p in o) {
 
        _qs.push(encodeURIComponent(_p) + '=' + encodeURIComponent(o[_p]));
 
    }
 
    return _qs.join('&');
 
};
 

	
 
/**
 
 * Load HTML into DOM using Ajax
 
 *
 
 * @param $target: load html async and place it (or an error message) here
 
 * @param success: success callback function
 
 * @param args: query parameters to pass to url
 
 */
 
function asynchtml(url, $target, success, args){
 
    if(args===undefined){
 
        args=null;
 
    }
 
    $target.html(_TM['Loading ...']).css('opacity','0.3');
 

	
 
    return $.ajax({url: url, data: args, headers: {'X-PARTIAL-XHR': '1'}, cache: false, dataType: 'html'})
 
        .done(function(html) {
 
                $target.html(html);
 
                $target.css('opacity','1.0');
 
                //execute the given original callback
 
                if (success !== undefined && success) {
 
                    success();
 
                }
 
            })
 
        .fail(function(jqXHR, textStatus, errorThrown) {
 
                if (textStatus == "abort")
 
                    return;
 
                $target.html('<span class="error_red">ERROR: {0}</span>'.format(textStatus));
 
                $target.css('opacity','1.0');
 
            })
 
        ;
 
};
 

	
 
var ajaxGET = function(url, success, failure) {
 
    if(failure === undefined) {
 
        failure = function(jqXHR, textStatus, errorThrown) {
 
                if (textStatus != "abort")
 
                    alert("Ajax GET error: " + textStatus);
 
            };
 
    }
 
    return $.ajax({url: url, headers: {'X-PARTIAL-XHR': '1'}, cache: false})
 
        .done(success)
 
        .fail(failure);
 
};
 

	
 
var ajaxPOST = function(url, postData, success, failure) {
 
    postData['_authentication_token'] = _authentication_token;
 
    var postData = _toQueryString(postData);
 
    if(failure === undefined) {
 
        failure = function(jqXHR, textStatus, errorThrown) {
 
                if (textStatus != "abort")
 
                    alert("Error posting to server: " + textStatus);
 
            };
 
    }
 
    return $.ajax({url: url, data: postData, type: 'POST', headers: {'X-PARTIAL-XHR': '1'}, cache: false})
 
        .done(success)
 
        .fail(failure);
 
};
 

	
 

	
 
/**
 
 * activate .show_more links
 
 * the .show_more must have an id that is the the id of an element to hide prefixed with _
 
 * the parentnode will be displayed
 
 */
 
var show_more_event = function(){
 
    $('.show_more').click(function(e){
 
        var el = e.currentTarget;
 
        $('#' + el.id.substring(1)).hide();
 
        $(el.parentNode).show();
 
    });
 
};
 

	
 
/**
 
 * activate .lazy-cs mouseover for showing changeset tooltip
 
 */
 
var show_changeset_tooltip = function(){
 
    $('.lazy-cs').mouseover(function(e){
 
        var $target = $(e.currentTarget);
 
        var rid = $target.attr('raw_id');
 
        var repo_name = $target.attr('repo_name');
 
        if(rid && !$target.hasClass('tooltip')){
 
            _show_tooltip(e, _TM['loading ...']);
 
            var url = pyroutes.url('changeset_info', {"repo_name": repo_name, "revision": rid});
 
            ajaxGET(url, function(json){
 
                    $target.addClass('tooltip');
 
                    _show_tooltip(e, json['message']);
 
                    _activate_tooltip($target);
 
                });
 
        }
 
    });
 
};
 

	
 
var _onSuccessFollow = function(target){
 
    var $target = $(target);
 
    var $f_cnt = $('#current_followers_count');
 
    if ($target.hasClass('follow')) {
 
        $target.removeClass('follow').addClass('following');
 
        $target.prop('title', _TM['Stop following this repository']);
 
        if ($f_cnt.html()) {
 
            var cnt = Number($f_cnt.html())+1;
 
            $f_cnt.html(cnt);
 
        }
 
    } else {
 
        $target.removeClass('following').addClass('follow');
 
        $target.prop('title', _TM['Start following this repository']);
 
        if ($f_cnt.html()) {
 
            var cnt = Number($f_cnt.html())-1;
 
            $f_cnt.html(cnt);
 
        }
 
    }
 
}
 

	
 
var toggleFollowingRepo = function(target, follows_repo_id){
 
    var args = 'follows_repo_id=' + follows_repo_id;
 
    args += '&amp;_authentication_token=' + _authentication_token;
 
    $.post(TOGGLE_FOLLOW_URL, args, function(data){
 
            _onSuccessFollow(target);
 
        });
 
    return false;
 
};
 

	
 
var showRepoSize = function(target, repo_name){
 
    var args = '_authentication_token=' + _authentication_token;
 

	
 
    if(!$("#" + target).hasClass('loaded')){
 
        $("#" + target).html(_TM['Loading ...']);
 
        var url = pyroutes.url('repo_size', {"repo_name":repo_name});
 
        $.post(url, args, function(data) {
 
            $("#" + target).html(data);
 
            $("#" + target).addClass('loaded');
 
        });
 
    }
 
    return false;
 
};
 

	
 
/**
 
 * tooltips
 
 */
 

	
 
var tooltip_activate = function(){
 
    $(document).ready(_init_tooltip);
 
};
 

	
 
var _activate_tooltip = function($tt){
 
    $tt.mouseover(_show_tooltip);
 
    $tt.mousemove(_move_tooltip);
 
    $tt.mouseout(_close_tooltip);
 
};
 

	
 
var _init_tooltip = function(){
 
    var $tipBox = $('#tip-box');
 
    if(!$tipBox.length){
 
        $tipBox = $('<div id="tip-box"></div>');
 
        $(document.body).append($tipBox);
 
    }
 

	
 
    $tipBox.hide();
 
    $tipBox.css('position', 'absolute');
 
    $tipBox.css('max-width', '600px');
 

	
 
    _activate_tooltip($('.tooltip'));
 
};
 

	
 
var _show_tooltip = function(e, tipText, safe){
 
    e.stopImmediatePropagation();
 
    var el = e.currentTarget;
 
    var $el = $(el);
 
    if(tipText){
 
        // just use it
 
    } else if(el.tagName.toLowerCase() === 'img'){
 
        tipText = el.alt ? el.alt : '';
 
    } else {
 
        tipText = el.title ? el.title : '';
 
        safe = safe || $el.hasClass("safe-html-title");
 
    }
 

	
 
    if(tipText !== ''){
 
        // save org title
 
        $el.attr('tt_title', tipText);
 
        // reset title to not show org tooltips
 
        $el.prop('title', '');
 

	
 
        var $tipBox = $('#tip-box');
 
        if (safe) {
 
            $tipBox.html(tipText);
 
        } else {
 
            $tipBox.text(tipText);
 
        }
 
        $tipBox.css('display', 'block');
 
    }
 
};
 

	
 
var _move_tooltip = function(e){
 
    e.stopImmediatePropagation();
 
    var $tipBox = $('#tip-box');
 
    $tipBox.css('top', (e.pageY + 15) + 'px');
 
    $tipBox.css('left', (e.pageX + 15) + 'px');
 
};
 

	
 
var _close_tooltip = function(e){
 
    e.stopImmediatePropagation();
 
    var $tipBox = $('#tip-box');
 
    $tipBox.hide();
 
    var el = e.currentTarget;
 
    $(el).prop('title', $(el).attr('tt_title'));
 
};
 

	
 
/**
 
 * Quick filter widget
 
 *
 
 * @param target: filter input target
 
 * @param nodes: list of nodes in html we want to filter.
 
 * @param display_element function that takes current node from nodes and
 
 *    does hide or show based on the node
 
 */
 
var q_filter = (function() {
 
    var _namespace = {};
 
    var namespace = function (target) {
 
        if (!(target in _namespace)) {
 
            _namespace[target] = {};
 
        }
 
        return _namespace[target];
 
    };
 
    return function (target, $nodes, display_element) {
 
        var $nodes = $nodes;
 
        var $q_filter_field = $('#' + target);
 
        var F = namespace(target);
 

	
 
        $q_filter_field.keyup(function (e) {
 
            clearTimeout(F.filterTimeout);
 
            F.filterTimeout = setTimeout(F.updateFilter, 600);
 
        });
 

	
 
        F.filterTimeout = null;
 

	
 
        F.updateFilter = function () {
 
            // Reset timeout
 
            F.filterTimeout = null;
 

	
 
            var obsolete = [];
 

	
 
            var req = $q_filter_field.val().toLowerCase();
 

	
 
            var showing = 0;
 
            $nodes.each(function () {
 
                var n = this;
 
                var target_element = display_element(n);
 
                if (req && n.innerHTML.toLowerCase().indexOf(req) == -1) {
 
                    $(target_element).hide();
 
                }
 
                else {
 
                    $(target_element).show();
 
                    showing += 1;
 
                }
 
            });
 

	
 
            $('#repo_count').html(showing);
 
            /* FIXME: don't hardcode */
 
        }
 
    }
 
})();
 

	
 

	
 
/**
 
 * Comment handling
 
 */
 

	
 
// move comments to their right location, inside new trs
 
function move_comments($anchorcomments) {
 
    $anchorcomments.each(function(i, anchorcomment) {
 
        var $anchorcomment = $(anchorcomment);
 
        var target_id = $anchorcomment.data('target-id');
 
        var $comment_div = _get_add_comment_div(target_id);
 
        var f_path = $anchorcomment.data('f_path');
 
        var line_no = $anchorcomment.data('line_no');
 
        if ($comment_div[0]) {
 
            $comment_div.append($anchorcomment.children());
 
            if (f_path && line_no) {
 
                _comment_div_append_add($comment_div, f_path, line_no);
 
            } else {
 
                _comment_div_append_form($comment_div, f_path, line_no);
 
            }
 
        } else {
 
            $anchorcomment.before("Comment to {0} line {1} which is outside the diff context:".format(f_path || '?', line_no || '?'));
 
        }
 
    });
 
    linkInlineComments($('.firstlink'), $('.comment:first-child'));
 
}
 

	
 
// comment bubble was clicked - insert new tr and show form
 
function show_comment_form($bubble) {
 
    var children = $bubble.closest('tr.line').children('[id]');
 
    var line_td_id = children[children.length - 1].id;
 
    var $comment_div = _get_add_comment_div(line_td_id);
 
    var f_path = $bubble.closest('div.full_f_path').data('f_path');
 
    var parts = line_td_id.split('_');
 
    var line_no = parts[parts.length-1];
 
    comment_div_state($comment_div, f_path, line_no, true);
 
}
 

	
 
// return comment div for target_id - add it if it doesn't exist yet
 
function _get_add_comment_div(target_id) {
 
    var comments_box_id = 'comments-' + target_id;
 
    var $comments_box = $('#' + comments_box_id);
 
    if (!$comments_box.length) {
 
        var html = '<tr><td id="{0}" colspan="3" class="inline-comments"></td></tr>'.format(comments_box_id);
 
        $('#' + target_id).closest('tr').after(html);
 
        $comments_box = $('#' + comments_box_id);
 
    }
 
    return $comments_box;
 
}
 

	
 
// Set $comment_div state - showing or not showing form and Add button.
 
// An Add button is shown on non-empty forms when no form is shown.
 
// The form is controlled by show_form - if undefined, form is only shown for general comments.
 
function comment_div_state($comment_div, f_path, line_no, show_form_opt) {
 
    var show_form = show_form_opt !== undefined ? show_form_opt : !f_path && !line_no;
 
    var $forms = $comment_div.children('.comment-inline-form');
 
    var $buttonrow = $comment_div.children('.add-button-row');
 
    var $comments = $comment_div.children('.comment');
 
    $forms.remove();
 
    $buttonrow.remove();
 
    if (show_form) {
 
        _comment_div_append_form($comment_div, f_path, line_no);
 
    } else if ($comments.length) {
 
        _comment_div_append_add($comment_div, f_path, line_no);
 
    }
 
}
 

	
 
// append an Add button to $comment_div and hook it up to show form
 
function _comment_div_append_add($comment_div, f_path, line_no) {
 
    var addlabel = TRANSLATION_MAP['Add Another Comment'];
 
    var $add = $('<div class="add-button-row"><span class="btn btn-mini add-button">{0}</span></div>'.format(addlabel));
 
    $comment_div.append($add);
 
    $add.children('.add-button').click(function(e) {
 
        comment_div_state($comment_div, f_path, line_no, true);
 
    });
 
}
 

	
 
// append a comment form to $comment_div
 
function _comment_div_append_form($comment_div, f_path, line_no) {
 
    var $form_div = $('#comment-inline-form-template').children()
 
        .clone()
 
        .addClass('comment-inline-form');
 
    $comment_div.append($form_div);
 
    var $form = $comment_div.find("form");
 
    var $textarea = $form.find('textarea');
 
    var $mentions_container = $form.find('div.mentions-container');
 

	
 
    $form.submit(function(e) {
 
        e.preventDefault();
 

	
 
        var text = $textarea.val();
 
        var review_status = $form.find('input:radio[name=changeset_status]:checked').val();
 
        var pr_close = $form.find('input:checkbox[name=save_close]:checked').length ? 'on' : '';
 
        var pr_delete = $form.find('input:checkbox[name=save_delete]:checked').length ? 'delete' : '';
 

	
 
        if (!text && !review_status && !pr_close) {
 
        if (!text && !review_status && !pr_close && !pr_delete) {
 
            alert("Please provide a comment");
 
            return false;
 
        }
 

	
 
        if (pr_delete) {
 
            if (text || review_status || pr_close) {
 
                alert('Cannot delete pull request while making other changes');
 
                return false;
 
            }
 
            if (!confirm('Confirm to delete this pull request')) {
 
                return false;
 
            }
 
            var comments = $('.comment').size();
 
            if (comments > 0 &&
 
                !confirm('Confirm again to delete this pull request with {0} comments'.format(comments))) {
 
                return false;
 
            }
 
        }
 

	
 
        $form.find('.submitting-overlay').show();
 

	
 
        var postData = {
 
            'text': text,
 
            'f_path': f_path,
 
            'line': line_no,
 
            'changeset_status': review_status,
 
            'save_close': pr_close
 
            'save_close': pr_close,
 
            'save_delete': pr_delete
 
        };
 
        var success = function(json_data) {
 
            $comment_div.append(json_data['rendered_text']);
 
            comment_div_state($comment_div, f_path, line_no);
 
            linkInlineComments($('.firstlink'), $('.comment:first-child'));
 
            if ((review_status || pr_close) && !f_path && !line_no) {
 
                // Page changed a lot - reload it after closing the submitted form
 
                comment_div_state($comment_div, f_path, line_no, false);
 
                location.reload(true);
 
            if (pr_delete) {
 
                location = json_data['location'];
 
            } else {
 
                $comment_div.append(json_data['rendered_text']);
 
                comment_div_state($comment_div, f_path, line_no);
 
                linkInlineComments($('.firstlink'), $('.comment:first-child'));
 
                if ((review_status || pr_close) && !f_path && !line_no) {
 
                    // Page changed a lot - reload it after closing the submitted form
 
                    comment_div_state($comment_div, f_path, line_no, false);
 
                    location.reload(true);
 
                }
 
            }
 
        };
 
        ajaxPOST(AJAX_COMMENT_URL, postData, success);
 
    });
 

	
 
    // create event for hide button
 
    $form.find('.hide-inline-form').click(function(e) {
 
        comment_div_state($comment_div, f_path, line_no);
 
    });
 

	
 
    tooltip_activate();
 
    MentionsAutoComplete($textarea, $mentions_container, _USERS_AC_DATA);
 
    if (f_path) {
 
        $textarea.focus();
 
    }
 
}
 

	
 

	
 
function deleteComment(comment_id) {
 
    var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
 
    var postData = {'_method': 'delete'};
 
    var success = function(o) {
 
        $('#comment-'+comment_id).remove();
 
        // Ignore that this might leave a stray Add button (or have a pending form with another comment) ...
 
    }
 
    ajaxPOST(url, postData, success);
 
}
 

	
 

	
 
/**
 
 * Double link comments
 
 */
 
var linkInlineComments = function($firstlinks, $comments){
 
    if ($comments.length > 0) {
 
        $firstlinks.html('<a href="#{0}">First comment</a>'.format($comments.prop('id')));
 
    }
 
    if ($comments.length <= 1) {
 
        return;
 
    }
 

	
 
    $comments.each(function(i, e){
 
            var prev = '';
 
            if (i > 0){
 
                var prev_anchor = $($comments.get(i-1)).prop('id');
 
                prev = '<a href="#{0}">Previous comment</a>'.format(prev_anchor);
 
            }
 
            var next = '';
 
            if (i+1 < $comments.length){
 
                var next_anchor = $($comments.get(i+1)).prop('id');
 
                next = '<a href="#{0}">Next comment</a>'.format(next_anchor);
 
            }
 
            $(this).find('.comment-prev-next-links').html(
 
                '<div class="prev-comment">{0}</div>'.format(prev) +
 
                '<div class="next-comment">{0}</div>'.format(next));
 
        });
 
}
 

	
 
/* activate files.html stuff */
 
var fileBrowserListeners = function(current_url, node_list_url, url_base){
 
    var current_url_branch = "?branch=__BRANCH__";
 

	
 
    $('#stay_at_branch').on('click',function(e){
 
        if(e.currentTarget.checked){
 
            var uri = current_url_branch;
 
            uri = uri.replace('__BRANCH__',e.currentTarget.value);
 
            window.location = uri;
 
        }
 
        else{
 
            window.location = current_url;
 
        }
 
    });
 

	
 
    var $node_filter = $('#node_filter');
 

	
 
    var filterTimeout = null;
 
    var nodes = null;
 

	
 
    var initFilter = function(){
 
        $('#node_filter_box_loading').show();
 
        $('#search_activate_id').hide();
 
        $('#add_node_id').hide();
 
        $.ajax({url: node_list_url, headers: {'X-PARTIAL-XHR': '1'}, cache: false})
 
            .done(function(json) {
 
                    nodes = json.nodes;
 
                    $('#node_filter_box_loading').hide();
 
                    $('#node_filter_box').show();
 
                    $node_filter.focus();
 
                    if($node_filter.hasClass('init')){
 
                        $node_filter.val('');
 
                        $node_filter.removeClass('init');
 
                    }
 
                })
 
            .fail(function() {
 
                    console.log('fileBrowserListeners initFilter failed to load');
 
                })
 
        ;
 
    }
 

	
 
    var updateFilter = function(e) {
 
        return function(){
 
            // Reset timeout
 
            filterTimeout = null;
 
            var query = e.currentTarget.value.toLowerCase();
 
            var match = [];
 
            var matches = 0;
 
            var matches_max = 20;
 
            if (query != ""){
 
                for(var i=0;i<nodes.length;i++){
 
                    var pos = nodes[i].name.toLowerCase().indexOf(query);
 
                    if(query && pos != -1){
 
                        matches++
 
                        //show only certain amount to not kill browser
 
                        if (matches > matches_max){
 
                            break;
 
                        }
 

	
 
                        var n = nodes[i].name;
 
                        var t = nodes[i].type;
 
                        var n_hl = n.substring(0,pos)
 
                            + "<b>{0}</b>".format(n.substring(pos,pos+query.length))
 
                            + n.substring(pos+query.length);
 
                        var new_url = url_base.replace('__FPATH__',n);
 
                        match.push('<tr><td><a class="browser-{0}" href="{1}">{2}</a></td><td colspan="5"></td></tr>'.format(t,new_url,n_hl));
 
                    }
 
                    if(match.length >= matches_max){
 
                        match.push('<tr><td>{0}</td><td colspan="5"></td></tr>'.format(_TM['Search truncated']));
 
                        break;
 
                    }
 
                }
 
            }
 
            if(query != ""){
 
                $('#tbody').hide();
 
                $('#tbody_filtered').show();
 

	
 
                if (match.length==0){
 
                  match.push('<tr><td>{0}</td><td colspan="5"></td></tr>'.format(_TM['No matching files']));
 
                }
 

	
 
                $('#tbody_filtered').html(match.join(""));
 
            }
 
            else{
 
                $('#tbody').show();
 
                $('#tbody_filtered').hide();
 
            }
 
        }
 
    };
 

	
 
    $('#filter_activate').click(function(){
 
            initFilter();
 
        });
 
    $node_filter.click(function(){
 
            if($node_filter.hasClass('init')){
 
                $node_filter.val('');
 
                $node_filter.removeClass('init');
 
            }
 
        });
 
    $node_filter.keyup(function(e){
 
            clearTimeout(filterTimeout);
 
            filterTimeout = setTimeout(updateFilter(e),600);
 
        });
 
};
 

	
 

	
 
var initCodeMirror = function(textarea_id, baseUrl, resetUrl){
 
    var myCodeMirror = CodeMirror.fromTextArea($('#' + textarea_id)[0], {
 
            mode: "null",
 
            lineNumbers: true,
 
            indentUnit: 4,
 
            autofocus: true
 
        });
 
    CodeMirror.modeURL = baseUrl + "/codemirror/mode/%N/%N.js";
 

	
 
    $('#reset').click(function(e){
 
            window.location=resetUrl;
 
        });
 

	
 
    $('#file_enable').click(function(){
 
            $('#editor_container').show();
 
            $('#upload_file_container').hide();
 
            $('#filename_container').show();
 
            $('#mimetype_header').show();
 
        });
 

	
 
    $('#upload_file_enable').click(function(){
 
            $('#editor_container').hide();
 
            $('#upload_file_container').show();
 
            $('#filename_container').hide();
 
            $('#mimetype_header').hide();
 
        });
 

	
 
    return myCodeMirror
 
};
 

	
 
var setCodeMirrorMode = function(codeMirrorInstance, mode) {
 
    CodeMirror.autoLoadMode(codeMirrorInstance, mode);
 
}
 

	
 

	
 
var _getIdentNode = function(n){
 
    //iterate thrugh nodes until matching interesting node
 

	
 
    if (typeof n == 'undefined'){
 
        return -1
 
    }
 

	
 
    if(typeof n.id != "undefined" && n.id.match('L[0-9]+')){
 
        return n
 
    }
 
    else{
 
        return _getIdentNode(n.parentNode);
 
    }
 
};
 

	
 
/* generate links for multi line selects that can be shown by files.html page_highlights.
 
 * This is a mouseup handler for hlcode from CodeHtmlFormatter and pygmentize */
 
var getSelectionLink = function(e) {
 
    //get selection from start/to nodes
 
    if (typeof window.getSelection != "undefined") {
 
        var s = window.getSelection();
 

	
 
        var from = _getIdentNode(s.anchorNode);
 
        var till = _getIdentNode(s.focusNode);
 

	
 
        var f_int = parseInt(from.id.replace('L',''));
 
        var t_int = parseInt(till.id.replace('L',''));
 

	
 
        var yoffset = 35;
 
        var ranges = [parseInt(from.id.replace('L','')), parseInt(till.id.replace('L',''))];
 
        if (ranges[0] > ranges[1]){
 
            //highlight from bottom
 
            yoffset = -yoffset;
 
            ranges = [ranges[1], ranges[0]];
 
        }
 
        var $hl_div = $('div#linktt');
 
        // if we select more than 2 lines
 
        if (ranges[0] != ranges[1]){
 
            if ($hl_div.length) {
 
                $hl_div.html('');
 
            } else {
 
                $hl_div = $('<div id="linktt" class="hl-tip-box">');
 
                $('body').prepend($hl_div);
 
            }
 

	
 
            $hl_div.append($('<a>').html(_TM['Selection Link']).prop('href', location.href.substring(0, location.href.indexOf('#')) + '#L' + ranges[0] + '-'+ranges[1]));
 
            var xy = $(till).offset();
 
            $hl_div.css('top', (xy.top + yoffset) + 'px').css('left', xy.left + 'px');
 
            $hl_div.show();
 
        }
 
        else{
 
            $hl_div.hide();
 
        }
 
    }
 
};
 

	
 
var deleteNotification = function(url, notification_id, callbacks){
 
    var success = function(o){
 
            $("#notification_"+notification_id).remove();
 
            _run_callbacks(callbacks);
 
        };
 
    var failure = function(o){
 
            alert("deleteNotification failure");
 
        };
 
    var postData = {'_method': 'delete'};
 
    var sUrl = url.replace('__NOTIFICATION_ID__',notification_id);
 
    ajaxPOST(sUrl, postData, success, failure);
 
};
 

	
 
var readNotification = function(url, notification_id, callbacks){
 
    var success = function(o){
 
            var $obj = $("#notification_"+notification_id);
 
            $obj.removeClass('unread');
 
            $obj.find('.read-notification').remove();
 
            _run_callbacks(callbacks);
 
        };
 
    var failure = function(o){
 
            alert("readNotification failure");
 
        };
 
    var postData = {'_method': 'put'};
 
    var sUrl = url.replace('__NOTIFICATION_ID__',notification_id);
 
    ajaxPOST(sUrl, postData, success, failure);
 
};
 

	
 
/**
 
 * Autocomplete functionality
 
 */
 

	
 
// Custom search function for the DataSource of users
 
var autocompleteMatchUsers = function (sQuery, myUsers) {
 
    // Case insensitive matching
 
    var query = sQuery.toLowerCase();
 
    var i = 0;
 
    var l = myUsers.length;
 
    var matches = [];
 

	
 
    // Match against each name of each contact
 
    for (; i < l; i++) {
 
        var contact = myUsers[i];
 
        if (((contact.fname+"").toLowerCase().indexOf(query) > -1) ||
 
             ((contact.lname+"").toLowerCase().indexOf(query) > -1) ||
 
             ((contact.nname) && ((contact.nname).toLowerCase().indexOf(query) > -1))) {
 
            matches[matches.length] = contact;
 
        }
 
    }
 
    return matches;
 
};
 

	
 
// Custom search function for the DataSource of userGroups
 
var autocompleteMatchGroups = function (sQuery, myGroups) {
 
    // Case insensitive matching
 
    var query = sQuery.toLowerCase();
 
    var i = 0;
 
    var l = myGroups.length;
 
    var matches = [];
 

	
 
    // Match against each name of each group
 
    for (; i < l; i++) {
 
        var matched_group = myGroups[i];
 
        if (matched_group.grname.toLowerCase().indexOf(query) > -1) {
 
            matches[matches.length] = matched_group;
 
        }
 
    }
 
    return matches;
 
};
 

	
 
// Helper highlight function for the formatter
 
var autocompleteHighlightMatch = function (full, snippet, matchindex) {
 
    return full.substring(0, matchindex)
 
        + "<span class='match'>"
 
        + full.substr(matchindex, snippet.length)
 
        + "</span>" + full.substring(matchindex + snippet.length);
 
};
 

	
 
var gravatar = function(link, size, cssclass) {
 
    var elem = '<img alt="" class="{2}" style="width: {0}px; height: {0}px" src="{1}"/>'.format(size, link, cssclass);
 
    if (!link) {
 
        elem = '<i class="icon-user {1}" style="font-size: {0}px;"></i>'.format(size, cssclass);
 
    }
 
    return elem;
 
}
 

	
 
var autocompleteGravatar = function(res, link, size, group) {
 
    var elem = gravatar(link, size, "perm-gravatar-ac");
 
    if (group !== undefined) {
 
        elem = '<i class="perm-gravatar-ac icon-users"></i>';
 
    }
 
    return '<div class="ac-container-wrap">{0}{1}</div>'.format(elem, res);
 
}
 

	
 
// Custom formatter to highlight the matching letters
 
var autocompleteFormatter = function (oResultData, sQuery, sResultMatch) {
 
    var query = sQuery.toLowerCase();
 

	
 
    // group
 
    if (oResultData.grname != undefined) {
 
        var grname = oResultData.grname;
 
        var grmembers = oResultData.grmembers;
 
        var grnameMatchIndex = grname.toLowerCase().indexOf(query);
 
        var grprefix = "{0}: ".format(_TM['Group']);
 
        var grsuffix = " ({0} {1})".format(grmembers, _TM['members']);
 

	
 
        if (grnameMatchIndex > -1) {
 
            return autocompleteGravatar(grprefix + autocompleteHighlightMatch(grname, query, grnameMatchIndex) + grsuffix, null, null, true);
 
        }
 
        return autocompleteGravatar(grprefix + oResultData.grname + grsuffix, null, null, true);
 

	
 
    // users
 
    } else if (oResultData.nname != undefined) {
 
        var fname = oResultData.fname || "";
 
        var lname = oResultData.lname || "";
 
        var nname = oResultData.nname;
 

	
 
        // Guard against null value
 
        var fnameMatchIndex = fname.toLowerCase().indexOf(query),
 
            lnameMatchIndex = lname.toLowerCase().indexOf(query),
 
            nnameMatchIndex = nname.toLowerCase().indexOf(query),
 
            displayfname, displaylname, displaynname, displayname;
 

	
 
        if (fnameMatchIndex > -1) {
 
            displayfname = autocompleteHighlightMatch(fname, query, fnameMatchIndex);
 
        } else {
 
            displayfname = fname;
 
        }
 

	
 
        if (lnameMatchIndex > -1) {
kallithea/templates/changeset/changeset_file_comment.html
Show inline comments
 
## -*- coding: utf-8 -*-
 
## usage:
 
## <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
 
## ${comment.comment_block(co)}
 
##
 
<%def name="comment_block(co)">
 
  <div class="comment" id="comment-${co.comment_id}" line="${co.line_no}">
 
    <div class="comment-prev-next-links"></div>
 
    <div class="comment-wrapp">
 
      <div class="meta">
 
          <div style="float:left">
 
               ${h.gravatar(co.author.email, size=20)}
 
          </div>
 
          <div class="user">
 
              ${co.author.full_name_and_username}
 
          </div>
 

	
 
          <span>
 
              ${h.age(co.modified_at)}
 
              %if co.pull_request:
 
                ${_('on pull request')}
 
                <a href="${co.pull_request.url()}">"${co.pull_request.title or _("No title")}"</a>
 
              %else:
 
                ${_('on this changeset')}
 
              %endif
 
              <a class="permalink" href="${co.url()}">&para;</a>
 
          </span>
 

	
 
          %if h.HasPermissionAny('hg.admin')() or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or co.author.user_id == c.authuser.user_id:
 
            <div onClick="confirm('${_("Delete comment?")}') && deleteComment(${co.comment_id})" class="buttons delete-comment btn btn-mini">${_('Delete')}</div>
 
          %endif
 
      </div>
 
      <div class="text">
 
        %if co.status_change:
 
           <div class="automatic-comment">
 
             <p>
 
               <span title="${_('Changeset status')}" class="changeset-status-lbl">${_("Status change")}: ${co.status_change[0].status_lbl}</span>
 
               <span class="changeset-status-ico"><i class="icon-circle changeset-status-${co.status_change[0].status}"></i></span>
 
             </p>
 
           </div>
 
        %endif
 
        %if co.text:
 
          ${h.render_w_mentions(co.text, c.repo_name)|n}
 
        %endif
 
      </div>
 
    </div>
 
  </div>
 
</%def>
 

	
 

	
 
<%def name="comment_inline_form()">
 
<div id='comment-inline-form-template' style="display:none">
 
  <div class="ac">
 
  %if c.authuser.username != 'default':
 
    ${h.form('#', class_='inline-form')}
 
      <div class="clearfix">
 
        <div class="comment-help">${_('Commenting on line.')}
 
          <span style="color:#577632" class="tooltip">${_('Comments are in plain text. Use @username inside this text to notify another user.')|n}</span>
 
        </div>
 
        <div class="mentions-container"></div>
 
        <textarea name="text" class="comment-block-ta yui-ac-input"></textarea>
 

	
 
        <div id="status_block_container" class="status-block general-only hidden">
 
                %if c.pull_request is None:
 
                  ${_('Set changeset status')}:
 
                %else:
 
                  ${_('Vote for pull request status')}:
 
                %endif
 
                <span class="general-only cs-only">
 
                </span>
 
                <input type="radio" class="status_change_radio" name="changeset_status" id="changeset_status_unchanged" value="" checked="checked" />
 
                <label for="changeset_status_unchanged">
 
                  ${_('No change')}
 
                </label>
 
                %for status, lbl in c.changeset_statuses:
 
                    <label>
 
                      <input type="radio" class="status_change_radio" name="changeset_status" id="${status}" value="${status}">
 
                      ${lbl}<i class="icon-circle changeset-status-${status}" /></i>
 
                    </label>
 
                %endfor
 

	
 
                %if c.pull_request is not None and ( \
 
                    h.HasPermissionAny('hg.admin')() or h.HasRepoPermissionAny('repository.admin')(c.repo_name) \
 
                    or c.pull_request.owner.user_id == c.authuser.user_id):
 
                <div>
 
                  ${_('Finish pull request')}:
 
                  <label>
 
                    <input id="save_close" type="checkbox" name="save_close">
 
                    ${_("Close")}
 
                  </label>
 
                  <label>
 
                    <input id="save_delete" type="checkbox" name="save_delete" value="delete">
 
                    ${_("Delete")}
 
                  </label>
 
                </div>
 
                %endif
 
        </div>
 

	
 
      </div>
 
      <div class="comment-button">
 
        <div class="submitting-overlay">${_('Submitting ...')}</div>
 
        ${h.submit('save', _('Comment'), class_='btn btn-small save-inline-form')}
 
        ${h.reset('hide-inline-form', _('Cancel'), class_='btn btn-small hide-inline-form')}
 
      </div>
 
    ${h.end_form()}
 
  %else:
 
      ${h.form('')}
 
      <div class="clearfix">
 
          <div class="comment-help">
 
            ${_('You need to be logged in to comment.')} <a href="${h.url('login_home', came_from=request.path_qs)}">${_('Login now')}</a>
 
          </div>
 
      </div>
 
      <div class="comment-button">
 
      ${h.reset('hide-inline-form', _('Hide'), class_='btn btn-small hide-inline-form')}
 
      </div>
 
      ${h.end_form()}
 
  %endif
 
  </div>
 
</div>
 
</%def>
 

	
 

	
 
## show comment count as "x comments (y inline, z general)"
 
<%def name="comment_count(inline_cnt, general_cnt)">
 
    ${'%s (%s, %s)' % (
 
        ungettext("%d comment", "%d comments", inline_cnt + general_cnt) % (inline_cnt + general_cnt),
 
        ungettext("%d inline", "%d inline", inline_cnt) % inline_cnt,
 
        ungettext("%d general", "%d general", general_cnt) % general_cnt
 
    )}
 
    <span class="firstlink"></span>
 
</%def>
 

	
 

	
 
## generate inline comments and the main ones
 
<%def name="generate_comments()">
 
## original location of comments ... but the ones outside diff context remains here
 
<div class="comments inline-comments">
 
  %for f_path, lines in c.inline_comments:
 
    %for line_no, comments in lines.iteritems():
 
      <div class="comments-list-chunk" data-f_path="${f_path}" data-line_no="${line_no}" data-target-id="${h.safeid(h.safe_unicode(f_path))}_${line_no}">
 
        %for co in comments:
 
            ${comment_block(co)}
 
        %endfor
 
      </div>
 
    %endfor
 
  %endfor
 

	
 
      <div class="comments-list-chunk" data-f_path="" data-line_no="" data-target-id="general-comments">
 
        %for co in c.comments:
 
            ${comment_block(co)}
 
        %endfor
 
      </div>
 
</div>
 
<div class="comments-number">
 
    ${comment_count(c.inline_cnt, len(c.comments))}
 
</div>
 
</%def>
 

	
 
## MAIN COMMENT FORM
 
<%def name="comments(change_status=True)">
 

	
 
## global, shared for all edit boxes
 
<div class="mentions-container" id="mentions_container"></div>
 

	
 
<div class="inline-comments inline-comments-general
 
            ${'show-general-status' if change_status else ''}">
 
  <div id="comments-general-comments" class="">
 
  ## comment_div for general comments
 
  </div>
 
</div>
 

	
 
<script>
 

	
 
$(document).ready(function () {
 

	
 
   $(window).on('beforeunload', function(){
 
      var $textareas = $('.comment-inline-form textarea[name=text]');
 
      if($textareas.size() > 1 ||
 
         $textareas.val()) {
 
         // this message will not be displayed on all browsers
 
         // (e.g. some versions of Firefox), but the user will still be warned
 
         return 'There are uncommitted comments.';
 
      }
 
   });
 

	
 
});
 
</script>
 
</%def>
0 comments (0 inline, 0 general)