# -*- coding: utf-8 -*- """ (c) 2014-2016 - Copyright Red Hat Inc Authors: Pierre-Yves Chibon """ # pylint: disable=too-many-branches # pylint: disable=too-many-arguments # pylint: disable=too-many-locals # pylint: disable=too-many-statements # pylint: disable=too-many-lines try: import simplejson as json except ImportError: import json import datetime import markdown import os import shutil import tempfile import urlparse import uuid import bleach import redis import six import sqlalchemy import sqlalchemy.schema from datetime import timedelta from sqlalchemy import func from sqlalchemy import asc from sqlalchemy.orm import aliased from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import scoped_session from sqlalchemy.orm.exc import NoResultFound from sqlalchemy.exc import SQLAlchemyError import pygit2 import pagure.exceptions import pagure.lib.git import pagure.lib.login import pagure.lib.notify import pagure.pfmarkdown from pagure.lib import model REDIS = None PAGURE_CI = None def set_redis(host, port, dbname): """ Set the redis connection with the specified information. """ global REDIS pool = redis.ConnectionPool(host=host, port=port, db=dbname) REDIS = redis.StrictRedis(connection_pool=pool) def set_pagure_ci(services): """ Set the list of CI services supported by this pagure instance. """ global PAGURE_CI PAGURE_CI = services def get_user(session, key): """ Searches for a user in the database for a given username or email. """ user_obj = search_user(session, username=key) if not user_obj: user_obj = search_user(session, email=key) if not user_obj: raise pagure.exceptions.PagureException( 'No user "%s" found' % key ) return user_obj def create_session(db_url, debug=False, pool_recycle=3600): ''' Create the Session object to use to query the database. :arg db_url: URL used to connect to the database. The URL contains information with regards to the database engine, the host to connect to, the user and password and the database name. ie: ://:@/ :kwarg debug: a boolean specifying wether we should have the verbose output of sqlalchemy or not. :return a Session that can be used to query the database. ''' if db_url.startswith('sqlite'): engine = sqlalchemy.create_engine( db_url, echo=debug, pool_recycle=pool_recycle) else: engine = sqlalchemy.create_engine( db_url, echo=debug, pool_recycle=pool_recycle, client_encoding='utf8') scopedsession = scoped_session(sessionmaker(bind=engine)) model.BASE.metadata.bind = scopedsession return scopedsession def get_next_id(session, projectid): """ Returns the next identifier of a project ticket or pull-request based on the identifier already in the database. """ query1 = session.query( func.max(model.Issue.id) ).filter( model.Issue.project_id == projectid ) query2 = session.query( func.max(model.PullRequest.id) ).filter( model.PullRequest.project_id == projectid ) nid = max([el[0] for el in query1.union(query2).all()]) or 0 return nid + 1 def search_user(session, username=None, email=None, token=None, pattern=None): ''' Searches the database for the user or users matching the given criterias. :arg session: the session to use to connect to the database. :kwarg username: the username of the user to look for. :type username: string or None :kwarg email: the email or one of the email of the user to look for :type email: string or None :kwarg token: the token of the user to look for :type token: string or None :kwarg pattern: a pattern to search the users with. :type pattern: string or None :return: A single User object if any of username, email or token is specified, a list of User objects otherwise. :rtype: User or [User] ''' query = session.query( model.User ).order_by( model.User.user ) if username is not None: query = query.filter( model.User.user == username ) if email is not None: query = query.filter( model.UserEmail.user_id == model.User.id ).filter( model.UserEmail.email == email ) if token is not None: query = query.filter( model.User.token == token ) if pattern: pattern = pattern.replace('*', '%') query = query.filter( model.User.user.like(pattern) ) if any([username, email, token]): output = query.first() else: output = query.all() return output def create_user_ssh_keys_on_disk(user, gitolite_keydir): ''' Create the ssh keys for the user on the specific folder. This is the method allowing to have multiple ssh keys per user. ''' if gitolite_keydir: # First remove any old keyfiles for the user # Assumption: we populated the keydir. This means that files # will be in 0/.pub, ..., and not in any deeper # directory structures. Also, this means that if a user # had 5 lines, they will be up to at most keys_4/.pub, # meaning that if a user is not in keys_/.pub, with # i being any integer, the user is most certainly not in # keys_/.pub. i = 0 keyline_file = os.path.join(gitolite_keydir, 'keys_%i' % i, '%s.pub' % user.user) while os.path.exists(keyline_file): os.unlink(keyline_file) i += 1 keyline_file = os.path.join(gitolite_keydir, 'keys_%i' % i, '%s.pub' % user.user) # Now let's create new keyfiles for the user keys = user.public_ssh_key.split('\n') for i in range(len(keys)): if not keys[i]: continue keyline_dir = os.path.join(gitolite_keydir, 'keys_%i' % i) if not os.path.exists(keyline_dir): os.mkdir(keyline_dir) keyfile = os.path.join(keyline_dir, '%s.pub' % user.user) with open(keyfile, 'w') as stream: stream.write(keys[i].strip().encode('UTF-8')) def add_issue_comment(session, issue, comment, user, ticketfolder, notify=True, date_created=None, notification=False): ''' Add a comment to an issue. ''' user_obj = get_user(session, user) issue_comment = model.IssueComment( issue_uid=issue.uid, comment=comment, user_id=user_obj.id, date_created=date_created, notification=notification, ) session.add(issue_comment) # Make sure we won't have SQLAlchemy error before we continue session.commit() pagure.lib.git.update_git( issue, repo=issue.project, repofolder=ticketfolder) if notify: pagure.lib.notify.notify_new_comment(issue_comment, user=user_obj) if not issue.private: pagure.lib.notify.log( issue.project, topic='issue.comment.added', msg=dict( issue=issue.to_json(public=True), project=issue.project.to_json(public=True), agent=user_obj.username, ), redis=REDIS, ) if REDIS: if issue.private: REDIS.publish('pagure.%s' % issue.uid, json.dumps({ 'issue': 'private', 'comment_id': issue_comment.id, })) else: REDIS.publish('pagure.%s' % issue.uid, json.dumps({ 'comment_id': issue_comment.id, 'issue_id': issue.id, 'project': issue.project.fullname, 'comment_added': text2markdown(issue_comment.comment), 'comment_user': issue_comment.user.user, 'avatar_url': avatar_url_from_openid( issue_comment.user.default_email, size=16), 'comment_date': issue_comment.date_created.strftime( '%Y-%m-%d %H:%M:%S'), 'notification': notification, })) return 'Comment added' def add_tag_obj(session, obj, tags, user, ticketfolder): ''' Add a tag to an object (either an issue or a project). ''' user_obj = get_user(session, user) if isinstance(tags, basestring): tags = [tags] added_tags = [] for objtag in tags: objtag = objtag.strip() known = False for tagobj in obj.tags: if tagobj.tag == objtag: known = True if known: continue tagobj = get_tag(session, objtag) if not tagobj: tagobj = model.Tag(tag=objtag) session.add(tagobj) session.flush() if isinstance(obj, model.Issue): dbobjtag = model.TagIssue( issue_uid=obj.uid, tag=tagobj.tag, ) if isinstance(obj, model.Project): dbobjtag = model.TagProject( project_id=obj.id, tag=tagobj.tag, ) session.add(dbobjtag) # Make sure we won't have SQLAlchemy error before we continue session.flush() added_tags.append(tagobj.tag) if isinstance(obj, model.Issue): pagure.lib.git.update_git( obj, repo=obj.project, repofolder=ticketfolder) if not obj.private: pagure.lib.notify.log( obj.project, topic='issue.tag.added', msg=dict( issue=obj.to_json(public=True), project=obj.project.to_json(public=True), tags=added_tags, agent=user_obj.username, ), redis=REDIS, ) # Send notification for the event-source server if REDIS: REDIS.publish('pagure.%s' % obj.uid, json.dumps( {'added_tags': added_tags})) if added_tags: return 'Tag added: %s' % ', '.join(added_tags) else: return 'Nothing to add' def add_issue_assignee(session, issue, assignee, user, ticketfolder, notify=True): ''' Add an assignee to an issue, in other words, assigned an issue. ''' user_obj = get_user(session, user) if assignee is None and issue.assignee is not None: issue.assignee_id = None session.add(issue) session.commit() pagure.lib.git.update_git( issue, repo=issue.project, repofolder=ticketfolder) if notify: pagure.lib.notify.notify_assigned_issue(issue, None, user_obj) if not issue.private: pagure.lib.notify.log( issue.project, topic='issue.assigned.reset', msg=dict( issue=issue.to_json(public=True), project=issue.project.to_json(public=True), agent=user_obj.username, ), redis=REDIS, ) # Send notification for the event-source server if REDIS: REDIS.publish('pagure.%s' % issue.uid, json.dumps( {'unassigned': '-'})) return 'Assignee reset' elif assignee is None and issue.assignee is None: return # Validate the assignee assignee_obj = get_user(session, assignee) if issue.assignee_id != assignee_obj.id: issue.assignee_id = assignee_obj.id session.add(issue) session.flush() pagure.lib.git.update_git( issue, repo=issue.project, repofolder=ticketfolder) if notify: pagure.lib.notify.notify_assigned_issue( issue, assignee_obj, user_obj) if not issue.private: pagure.lib.notify.log( issue.project, topic='issue.assigned.added', msg=dict( issue=issue.to_json(public=True), project=issue.project.to_json(public=True), agent=user_obj.username, ), redis=REDIS, ) # Send notification for the event-source server if REDIS: REDIS.publish('pagure.%s' % issue.uid, json.dumps( {'assigned': assignee_obj.to_json(public=True)})) return 'Issue assigned' def add_pull_request_assignee( session, request, assignee, user, requestfolder): ''' Add an assignee to a request, in other words, assigned an issue. ''' get_user(session, assignee) user_obj = get_user(session, user) if assignee is None and request.assignee is not None: request.assignee_id = None session.add(request) session.commit() pagure.lib.git.update_git( request, repo=request.project, repofolder=requestfolder) pagure.lib.notify.notify_assigned_request(request, None, user_obj) pagure.lib.notify.log( request.project, topic='request.assigned.reset', msg=dict( request=request.to_json(public=True), project=request.project.to_json(public=True), agent=user_obj.username, ), redis=REDIS, ) return 'Request reset' elif assignee is None and request.assignee is None: return # Validate the assignee assignee_obj = get_user(session, assignee) if request.assignee_id != assignee_obj.id: request.assignee_id = assignee_obj.id session.add(request) session.flush() pagure.lib.git.update_git( request, repo=request.project, repofolder=requestfolder) pagure.lib.notify.notify_assigned_request( request, assignee_obj, user_obj) pagure.lib.notify.log( request.project, topic='request.assigned.added', msg=dict( request=request.to_json(public=True), project=request.project.to_json(public=True), agent=user_obj.username, ), redis=REDIS, ) return 'Request assigned' def add_issue_dependency( session, issue, issue_blocked, user, ticketfolder): ''' Add a dependency between two issues. ''' user_obj = get_user(session, user) if issue.uid == issue_blocked.uid: raise pagure.exceptions.PagureException( 'An issue cannot depend on itself' ) if issue_blocked not in issue.children: i2i = model.IssueToIssue( parent_issue_id=issue_blocked.uid, child_issue_id=issue.uid ) session.add(i2i) # Make sure we won't have SQLAlchemy error before we continue session.flush() pagure.lib.git.update_git( issue, repo=issue.project, repofolder=ticketfolder) pagure.lib.git.update_git( issue_blocked, repo=issue_blocked.project, repofolder=ticketfolder) #pagure.lib.notify.notify_assigned_issue(issue, user_obj) #pagure.lib.notify.notify_assigned_issue(issue_blocked, user_obj) if not issue.private: pagure.lib.notify.log( issue.project, topic='issue.dependency.added', msg=dict( issue=issue.to_json(public=True), project=issue.project.to_json(public=True), added_dependency=issue_blocked.id, agent=user_obj.username, ), redis=REDIS, ) # Send notification for the event-source server if REDIS: REDIS.publish('pagure.%s' % issue.uid, json.dumps({ 'added_dependency': issue_blocked.id, 'issue_uid': issue.uid, 'type': 'children', })) REDIS.publish('pagure.%s' % issue_blocked.uid, json.dumps({ 'added_dependency': issue.id, 'issue_uid': issue_blocked.uid, 'type': 'parent', })) return 'Dependency added' def remove_issue_dependency( session, issue, issue_blocked, user, ticketfolder): ''' Remove a dependency between two issues. ''' user_obj = get_user(session, user) if issue.uid == issue_blocked.uid: raise pagure.exceptions.PagureException( 'An issue cannot depend on itself' ) if issue_blocked in issue.children: child_del = [] for child in issue.children: if child.uid == issue_blocked.uid: child_del.append(child.id) issue.children.remove(child) # Make sure we won't have SQLAlchemy error before we continue session.flush() pagure.lib.git.update_git( issue, repo=issue.project, repofolder=ticketfolder) pagure.lib.git.update_git( issue_blocked, repo=issue_blocked.project, repofolder=ticketfolder) #pagure.lib.notify.notify_assigned_issue(issue, user_obj) #pagure.lib.notify.notify_assigned_issue(issue_blocked, user_obj) if not issue.private: pagure.lib.notify.log( issue.project, topic='issue.dependency.removed', msg=dict( issue=issue.to_json(public=True), project=issue.project.to_json(public=True), removed_dependency=child_del, agent=user_obj.username, ), redis=REDIS, ) # Send notification for the event-source server if REDIS: REDIS.publish('pagure.%s' % issue.uid, json.dumps({ 'removed_dependency': child_del, 'issue_uid': issue.uid, 'type': 'children', })) REDIS.publish('pagure.%s' % issue_blocked.uid, json.dumps({ 'removed_dependency': issue.id, 'issue_uid': issue_blocked.uid, 'type': 'parent', })) return 'Dependency removed' def remove_tags(session, project, tags, ticketfolder, user): ''' Removes the specified tag of a project. ''' user_obj = get_user(session, user) if not isinstance(tags, list): tags = [tags] issues = search_issues(session, project, closed=False, tags=tags) issues.extend(search_issues(session, project, closed=True, tags=tags)) msgs = [] removed_tags = [] if not issues: raise pagure.exceptions.PagureException( 'No issue found with the tags: %s' % ', '.join(tags)) else: for issue in issues: for issue_tag in issue.tags: if issue_tag.tag in tags: tag = issue_tag.tag removed_tags.append(tag) session.delete(issue_tag) msgs.append('Removed tag: %s' % tag) pagure.lib.git.update_git( issue, repo=issue.project, repofolder=ticketfolder) pagure.lib.notify.log( project, topic='project.tag.removed', msg=dict( project=project.to_json(public=True), tags=removed_tags, agent=user_obj.username, ), redis=REDIS, ) return msgs def remove_tags_obj( session, obj, tags, ticketfolder, user): ''' Removes the specified tag(s) of a given object. ''' user_obj = get_user(session, user) if isinstance(tags, basestring): tags = [tags] removed_tags = [] for objtag in obj.tags: if objtag.tag in tags: tag = objtag.tag removed_tags.append(tag) session.delete(objtag) if isinstance(obj, model.Issue): pagure.lib.git.update_git( obj, repo=obj.project, repofolder=ticketfolder) pagure.lib.notify.log( obj.project, topic='issue.tag.removed', msg=dict( issue=obj.to_json(public=True), project=obj.project.to_json(public=True), tags=removed_tags, agent=user_obj.username, ), redis=REDIS, ) # Send notification for the event-source server if REDIS: REDIS.publish('pagure.%s' % obj.uid, json.dumps( {'removed_tags': removed_tags})) return 'Removed tag: %s' % ', '.join(removed_tags) def edit_issue_tags(session, project, old_tag, new_tag, ticketfolder, user): ''' Removes the specified tag of a project. ''' user_obj = get_user(session, user) if old_tag == new_tag: raise pagure.exceptions.PagureException( 'Old tag: "%s" is the same as new tag "%s", nothing to change' % (old_tag, new_tag)) issues = search_issues(session, project, closed=False, tags=old_tag) issues.extend(search_issues(session, project, closed=True, tags=old_tag)) msgs = [] if not issues: raise pagure.exceptions.PagureException( 'No issue found with the tags: %s' % old_tag) else: tagobj = get_tag(session, new_tag) if not tagobj: tagobj = model.Tag(tag=new_tag) session.add(tagobj) session.flush() for issue in set(issues): add = True # Drop the old tag cnt = 0 while cnt < len(issue.tags): issue_tag = issue.tags[cnt] if issue_tag.tag == old_tag: issue.tags.remove(issue_tag) cnt -= 1 if issue_tag.tag == new_tag: add = False cnt += 1 session.flush() # Add the new one if add: issue_tag = model.TagIssue( issue_uid=issue.uid, tag=tagobj.tag ) session.add(issue_tag) session.flush() # Update the git version pagure.lib.git.update_git( issue, repo=issue.project, repofolder=ticketfolder) msgs.append('Edited tag: %s to %s' % (old_tag, new_tag)) pagure.lib.notify.log( project, topic='project.tag.edited', msg=dict( project=project.to_json(public=True), old_tag=old_tag, new_tag=new_tag, agent=user_obj.username, ), redis=REDIS, ) return msgs def add_user_to_project(session, project, new_user, user): ''' Add a specified user to a specified project. ''' new_user_obj = get_user(session, new_user) user_obj = get_user(session, user) users = set([user.user for user in project.users]) users.add(project.user.user) if new_user in users: raise pagure.exceptions.PagureException( 'This user is already listed on this project.' ) project_user = model.ProjectUser( project_id=project.id, user_id=new_user_obj.id, ) session.add(project_user) # Make sure we won't have SQLAlchemy error before we continue session.flush() pagure.lib.notify.log( project, topic='project.user.added', msg=dict( project=project.to_json(public=True), new_user=new_user_obj.username, agent=user_obj.username, ), redis=REDIS, ) return 'User added' def add_group_to_project( session, project, new_group, user, create=False, is_admin=False): ''' Add a specified group to a specified project. ''' user_obj = search_user(session, username=user) if not user_obj: raise pagure.exceptions.PagureException( 'No user %s found.' % user ) group_obj = search_groups(session, group_name=new_group) if not group_obj: if create: group_obj = pagure.lib.model.PagureGroup( group_name=new_group, group_type='user', user_id=user_obj.id, ) session.add(group_obj) session.flush() else: raise pagure.exceptions.PagureException( 'No group %s found.' % new_group ) if user_obj not in project.users \ and user_obj != project.user \ and not is_admin: raise pagure.exceptions.PagureException( 'You are not allowed to add a group of users to this project' ) groups = set([group.group_name for group in project.groups]) if new_group in groups: raise pagure.exceptions.PagureException( 'This group is already associated to this project.' ) project_group = model.ProjectGroup( project_id=project.id, group_id=group_obj.id, ) session.add(project_group) # Make sure we won't have SQLAlchemy error before we continue session.flush() pagure.lib.notify.log( project, topic='project.group.added', msg=dict( project=project.to_json(public=True), new_group=group_obj.group_name, agent=user, ), redis=REDIS, ) return 'Group added' def add_pull_request_comment(session, request, commit, tree_id, filename, row, comment, user, requestfolder, notify=True, notification=False): ''' Add a comment to a pull-request. ''' user_obj = get_user(session, user) pr_comment = model.PullRequestComment( pull_request_uid=request.uid, commit_id=commit, tree_id=tree_id, filename=filename, line=row, comment=comment, user_id=user_obj.id, notification=notification, ) session.add(pr_comment) # Make sure we won't have SQLAlchemy error before we continue session.flush() pagure.lib.git.update_git( request, repo=request.project, repofolder=requestfolder) if notify: pagure.lib.notify.notify_pull_request_comment(pr_comment, user_obj) # Send notification for the event-source server if REDIS: comment_text = pr_comment.comment if not notification: comment_text = text2markdown(pr_comment.comment) REDIS.publish('pagure.%s' % request.uid, json.dumps({ 'request_id': request.id, 'comment_added': comment_text, 'comment_user': pr_comment.user.user, 'comment_id': pr_comment.id, 'avatar_url': avatar_url_from_openid( pr_comment.user.default_email, size=16), 'comment_date': pr_comment.date_created.strftime( '%Y-%m-%d %H:%M:%S'), 'commit_id': commit, 'filename': filename, 'line': row, 'notification': notification, })) # Send notification to the CI server, if the comment added was a # notification and the PR is still open if notification and request.status == 'Open' \ and request.project.ci_hook and PAGURE_CI: REDIS.publish('pagure.ci', json.dumps({ 'ci_type': request.project.ci_hook.ci_type, 'pr': request.to_json(public=True, with_comments=False) })) pagure.lib.notify.log( request.project, topic='pull-request.comment.added', msg=dict( pullrequest=request.to_json(public=True), agent=user_obj.username, ), redis=REDIS, ) return 'Comment added' def edit_comment(session, parent, comment, user, updated_comment, folder): ''' Edit a comment. ''' user_obj = get_user(session, user) comment.comment = updated_comment comment.edited_on = datetime.datetime.utcnow() comment.editor = user_obj session.add(comment) # Make sure we won't have SQLAlchemy error before we continue session.flush() pagure.lib.git.update_git( parent, repo=parent.project, repofolder=folder) topic = 'unknown' key = 'unknown' id_ = 'unknown' if parent.isa == 'pull-request': topic = 'pull-request.comment.edited' key = 'pullrequest' id_ = 'request_id' elif parent.isa == 'issue': topic = 'issue.comment.edited' key = 'issue' id_ = 'issue_id' pagure.lib.notify.log( parent.project, topic=topic, msg={ key: parent.to_json(public=True, with_comments=False), 'project': parent.project.to_json(public=True), 'comment': comment.to_json(public=True), 'agent': user_obj.username, }, redis=REDIS, ) if REDIS: if parent.isa == 'issue' and comment.parent.private: REDIS.publish('pagure.%s' % comment.parent.uid, json.dumps({ 'comment_updated': 'private', 'comment_id': comment.id, })) else: REDIS.publish('pagure.%s' % parent.uid, json.dumps({ id_: len(parent.comments), 'comment_updated': text2markdown(comment.comment), 'comment_id': comment.id, 'parent_id': comment.parent.id, 'comment_editor': user_obj.user, 'avatar_url': avatar_url_from_openid( comment.user.default_email, size=16), 'comment_date': comment.edited_on.strftime( '%Y-%m-%d %H:%M:%S'), })) return "Comment updated" def add_pull_request_flag(session, request, username, percent, comment, url, uid, user, requestfolder): ''' Add a flag to a pull-request. ''' user_obj = get_user(session, user) action = 'added' pr_flag = get_pull_request_flag_by_uid(session, uid) if pr_flag: action = 'updated' pr_flag.comment = comment pr_flag.percent = percent pr_flag.url = url else: pr_flag = model.PullRequestFlag( pull_request_uid=request.uid, uid=uid or uuid.uuid4().hex, username=username, percent=percent, comment=comment, url=url, user_id=user_obj.id, ) session.add(pr_flag) # Make sure we won't have SQLAlchemy error before we continue session.flush() pagure.lib.git.update_git( request, repo=request.project, repofolder=requestfolder) pagure.lib.notify.log( request.project, topic='pull-request.flag.%s' % action, msg=dict( pullrequest=request.to_json(public=True), flag=pr_flag.to_json(public=True), agent=user_obj.username, ), redis=REDIS, ) return 'Flag %s' % action def new_project(session, user, name, blacklist, allowed_prefix, gitfolder, docfolder, ticketfolder, requestfolder, description=None, url=None, avatar_email=None, parent_id=None, add_readme=False, userobj=None, prevent_40_chars=False, namespace=None): ''' Create a new project based on the information provided. ''' if name in blacklist or ( namespace and '%s/%s' % (namespace, name) in blacklist): raise pagure.exceptions.RepoExistsException( 'No project "%s" are allowed to be created due to potential ' 'conflicts in URLs with pagure itself' % name ) user_obj = get_user(session, user) allowed_prefix = allowed_prefix + [grp for grp in user_obj.groups] if namespace and namespace not in allowed_prefix: raise pagure.exceptions.PagureException( 'The namespace of your project must be in the list of allowed ' 'namespaces set by the admins of this pagure instance, or the ' 'name of a group of which you are a member.' ) if len(name) == 40 and prevent_40_chars: # We must block project with a name / where the length # of is exactly 40 characters long as this would otherwise # conflict with the old URL schema used for commit that was # /. To keep backward compatibility, we have an # endpoint redirecting / to /c/ # available as an option. raise pagure.exceptions.PagureException( 'Your project name cannot have exactly 40 characters after ' 'the `/`' ) path = name if namespace: path = '%s/%s' % (namespace, name) gitrepo = os.path.join(gitfolder, '%s.git' % path) if os.path.exists(gitrepo): raise pagure.exceptions.RepoExistsException( 'The project repo "%s" already exists' % path ) project = model.Project( name=name, namespace=namespace, description=description if description else None, url=url if url else None, avatar_email=avatar_email if avatar_email else None, user_id=user_obj.id, parent_id=parent_id, hook_token=pagure.lib.login.id_generator(40) ) session.add(project) # Make sure we won't have SQLAlchemy error before we create the repo session.flush() if not add_readme: pygit2.init_repository(gitrepo, bare=True) else: temp_gitrepo_path = tempfile.mkdtemp(prefix='pagure-') temp_gitrepo = pygit2.init_repository(temp_gitrepo_path, bare=False) author = userobj.fullname or userobj.user author_email = userobj.default_email if six.PY2: author = author.encode('utf-8') author_email = author_email.encode('utf-8') author = pygit2.Signature(author, author_email) content = u"# %s\n\n%s" % (name, description) readme_file = os.path.join(temp_gitrepo.workdir, "README.md") with open(readme_file, 'wb') as stream: stream.write(content.encode('utf-8')) temp_gitrepo.index.add_all() temp_gitrepo.index.write() tree = temp_gitrepo.index.write_tree() temp_gitrepo.create_commit( 'HEAD', author, author, 'Added the README', tree, []) pygit2.clone_repository(temp_gitrepo_path, gitrepo, bare=True) shutil.rmtree(temp_gitrepo_path) http_clone_file = os.path.join(gitrepo, 'git-daemon-export-ok') if not os.path.exists(http_clone_file): with open(http_clone_file, 'w') as stream: pass docrepo = os.path.join(docfolder, project.path) if os.path.exists(docrepo): shutil.rmtree(gitrepo) raise pagure.exceptions.RepoExistsException( 'The docs repo "%s" already exists' % project.path ) pygit2.init_repository(docrepo, bare=True) ticketrepo = os.path.join(ticketfolder, project.path) if os.path.exists(ticketrepo): shutil.rmtree(gitrepo) shutil.rmtree(docrepo) raise pagure.exceptions.RepoExistsException( 'The tickets repo "%s" already exists' % project.path ) pygit2.init_repository( ticketrepo, bare=True, mode=pygit2.C.GIT_REPOSITORY_INIT_SHARED_GROUP) requestrepo = os.path.join(requestfolder, project.path) if os.path.exists(requestrepo): shutil.rmtree(gitrepo) shutil.rmtree(docrepo) shutil.rmtree(ticketrepo) raise pagure.exceptions.RepoExistsException( 'The requests repo "%s" already exists' % project.path ) pygit2.init_repository( requestrepo, bare=True, mode=pygit2.C.GIT_REPOSITORY_INIT_SHARED_GROUP) # create the project in the db session.commit() pagure.lib.notify.log( project, topic='project.new', msg=dict( project=project.to_json(public=True), agent=user_obj.username, ), ) return 'Project "%s" created' % name def new_issue(session, repo, title, content, user, ticketfolder, issue_id=None, issue_uid=None, private=False, status=None, notify=True, date_created=None): ''' Create a new issue for the specified repo. ''' user_obj = get_user(session, user) issue = model.Issue( id=issue_id or get_next_id(session, repo.id), project_id=repo.id, title=title, content=content, user_id=user_obj.id, uid=issue_uid or uuid.uuid4().hex, private=private, date_created=date_created, ) if status is not None: issue.status = status session.add(issue) # Make sure we won't have SQLAlchemy error before we create the issue session.flush() pagure.lib.git.update_git( issue, repo=repo, repofolder=ticketfolder) if notify: pagure.lib.notify.notify_new_issue(issue, user=user_obj) if not private: pagure.lib.notify.log( issue.project, topic='issue.new', msg=dict( issue=issue.to_json(public=True), project=issue.project.to_json(public=True), agent=user_obj.username, ), redis=REDIS, ) return issue def drop_issue(session, issue, user, ticketfolder): ''' Delete a specified issue. ''' user_obj = get_user(session, user) private = issue.private session.delete(issue) # Make sure we won't have SQLAlchemy error before we create the issue session.flush() pagure.lib.git.clean_git( issue, repo=issue.project, repofolder=ticketfolder) if not private: pagure.lib.notify.log( issue.project, topic='issue.drop', msg=dict( issue=issue.to_json(public=True), project=issue.project.to_json(public=True), agent=user_obj.username, ), redis=REDIS, ) return issue def new_pull_request(session, branch_from, repo_to, branch_to, title, user, requestfolder, initial_comment=None, repo_from=None, remote_git=None, requestuid=None, requestid=None, status='Open', notify=True): ''' Create a new pull request on the specified repo. ''' if not repo_from and not remote_git: raise pagure.exceptions.PagureException( 'Invalid input, you must specify either a local repo or a ' 'remote one') user_obj = get_user(session, user) request = model.PullRequest( id=requestid or get_next_id(session, repo_to.id), uid=requestuid or uuid.uuid4().hex, project_id=repo_to.id, project_id_from=repo_from.id if repo_from else None, remote_git=remote_git if remote_git else None, branch=branch_to, branch_from=branch_from, title=title, initial_comment=initial_comment or None, user_id=user_obj.id, status=status, ) session.add(request) # Make sure we won't have SQLAlchemy error before we create the request session.flush() pagure.lib.git.update_git( request, repo=request.project, repofolder=requestfolder) if notify: pagure.lib.notify.notify_new_pull_request(request) pagure.lib.notify.log( request.project, topic='pull-request.new', msg=dict( pullrequest=request.to_json(public=True), agent=user_obj.username, ), redis=REDIS, ) # Send notification to the CI server if REDIS and request.project.ci_hook and PAGURE_CI: REDIS.publish('pagure.ci', json.dumps({ 'ci_type': request.project.ci_hook.ci_type, 'pr': request.to_json(public=True, with_comments=False) })) return request def edit_issue(session, issue, ticketfolder, user, title=None, content=None, status=None, priority=None, milestone=None, private=False): ''' Edit the specified issue. ''' user_obj = get_user(session, user) if status == 'Fixed' and issue.parents: for parent in issue.parents: if parent.status == 'Open': raise pagure.exceptions.PagureException( 'You cannot close a ticket that has ticket ' 'depending that are still open.') edit = [] if title and title != issue.title: issue.title = title edit.append('title') if content and content != issue.content: issue.content = content edit.append('content') if status and status != issue.status: issue.status = status if status.lower() != 'open': issue.closed_at = datetime.datetime.utcnow() edit.append('status') if priority: try: priority = int(priority) except: priority = None if priority != issue.priority: issue.priority = priority edit.append('priority') if private in [True, False] and private != issue.private: issue.private = private edit.append('private') if milestone != issue.milestone: issue.milestone = milestone edit.append('milestone') pagure.lib.git.update_git( issue, repo=issue.project, repofolder=ticketfolder) if 'status' in edit: add_issue_comment( session, issue, comment='@%s changed the status to ``%s``' % ( user_obj.username, status), user=user, ticketfolder=ticketfolder, notify=False, notification=True, ) if not issue.private and edit: pagure.lib.notify.log( issue.project, topic='issue.edit', msg=dict( issue=issue.to_json(public=True), project=issue.project.to_json(public=True), fields=edit, agent=user_obj.username, ), redis=REDIS, ) if REDIS and edit: if issue.private: REDIS.publish('pagure.%s' % issue.uid, json.dumps({ 'issue': 'private', 'fields': edit, })) else: REDIS.publish('pagure.%s' % issue.uid, json.dumps({ 'fields': edit, 'issue': issue.to_json(public=True, with_comments=False), })) if edit: session.add(issue) session.flush() return 'Successfully edited issue #%s' % issue.id def update_project_settings(session, repo, settings, user): ''' Update the settings of a project. ''' user_obj = get_user(session, user) update = [] new_settings = repo.settings for key in new_settings: if key in settings: if new_settings[key] != settings[key]: update.append(key) if key == 'Minimum_score_to_merge_pull-request': try: settings[key] = int(settings[key]) \ if settings[key] else -1 except ValueError: raise pagure.exceptions.PagureException( "Please enter a numeric value for the 'minimum " "score to merge pull request' field.") elif key == 'Web-hooks': settings[key] = settings[key] or None new_settings[key] = settings[key] else: update.append(key) val = False if key == 'Web-hooks': val = None new_settings[key] = val if not update: return 'No settings to change' else: repo.settings = new_settings session.add(repo) session.flush() pagure.lib.notify.log( repo, topic='project.edit', msg=dict( project=repo.to_json(public=True), fields=update, agent=user_obj.username, ), redis=REDIS, ) return 'Edited successfully settings of repo: %s' % repo.fullname def fork_project(session, user, repo, gitfolder, docfolder, ticketfolder, requestfolder): ''' Fork a given project into the user's forks. ''' reponame = os.path.join(gitfolder, repo.path) forkreponame = '%s.git' % os.path.join( gitfolder, 'forks', user, repo.name) if os.path.exists(forkreponame): raise pagure.exceptions.RepoExistsException( 'Repo "forks/%s/%s" already exists' % (user, repo.name)) user_obj = get_user(session, user) project = model.Project( name=repo.name, description=repo.description, user_id=user_obj.id, parent_id=repo.id, is_fork=True, hook_token=pagure.lib.login.id_generator(40) ) # disable issues, PRs in the fork by default default_repo_settings = project.settings default_repo_settings['issue_tracker'] = False default_repo_settings['pull_requests'] = False project.settings = default_repo_settings session.add(project) # Make sure we won't have SQLAlchemy error before we create the repo session.flush() frepo = pygit2.clone_repository(reponame, forkreponame, bare=True) # Clone all the branches as well for branch in frepo.listall_branches(pygit2.GIT_BRANCH_REMOTE): branch_obj = frepo.lookup_branch(branch, pygit2.GIT_BRANCH_REMOTE) name = branch_obj.branch_name.replace( branch_obj.remote_name, '')[1:] if name in frepo.listall_branches(pygit2.GIT_BRANCH_LOCAL): continue frepo.create_branch(name, frepo.get(branch_obj.target.hex)) # Create the git-daemin-export-ok file on the clone http_clone_file = os.path.join(forkreponame, 'git-daemon-export-ok') if not os.path.exists(http_clone_file): with open(http_clone_file, 'w'): pass docrepo = os.path.join(docfolder, project.path) if os.path.exists(docrepo): shutil.rmtree(forkreponame) raise pagure.exceptions.RepoExistsException( 'The docs "%s" already exists' % project.path ) pygit2.init_repository(docrepo, bare=True) ticketrepo = os.path.join(ticketfolder, project.path) if os.path.exists(ticketrepo): shutil.rmtree(forkreponame) shutil.rmtree(docrepo) raise pagure.exceptions.RepoExistsException( 'The tickets repo "%s" already exists' % project.path ) pygit2.init_repository( ticketrepo, bare=True, mode=pygit2.C.GIT_REPOSITORY_INIT_SHARED_GROUP) requestrepo = os.path.join(requestfolder, project.path) if os.path.exists(requestrepo): shutil.rmtree(forkreponame) shutil.rmtree(docrepo) shutil.rmtree(ticketrepo) raise pagure.exceptions.RepoExistsException( 'The requests repo "%s" already exists' % project.path ) pygit2.init_repository( requestrepo, bare=True, mode=pygit2.C.GIT_REPOSITORY_INIT_SHARED_GROUP) pagure.lib.notify.log( project, topic='project.forked', msg=dict( project=project.to_json(public=True), agent=user_obj.username, ), ) return 'Repo "%s" cloned to "%s/%s"' % (repo.name, user, repo.name) def search_projects( session, username=None, fork=None, tags=None, namespace=None, pattern=None, start=None, limit=None, count=False, sort=None): '''List existing projects ''' projects = session.query( sqlalchemy.distinct(model.Project.id) ) if username is not None: projects = projects.filter( # User created the project sqlalchemy.and_( model.User.user == username, model.User.id == model.Project.user_id, ) ) sub_q2 = session.query( model.Project.id ).filter( # User got commit right sqlalchemy.and_( model.User.user == username, model.User.id == model.ProjectUser.user_id, model.ProjectUser.project_id == model.Project.id ) ) sub_q3 = session.query( model.Project.id ).filter( # User created a group that has commit right sqlalchemy.and_( model.User.user == username, model.PagureGroup.user_id == model.User.id, model.PagureGroup.group_type == 'user', model.PagureGroup.id == model.ProjectGroup.group_id, model.Project.id == model.ProjectGroup.project_id, ) ) sub_q4 = session.query( model.Project.id ).filter( # User is part of a group that has commit right sqlalchemy.and_( model.User.user == username, model.PagureUserGroup.user_id == model.User.id, model.PagureUserGroup.group_id == model.PagureGroup.id, model.PagureGroup.group_type == 'user', model.PagureGroup.id == model.ProjectGroup.group_id, model.Project.id == model.ProjectGroup.project_id, ) ) projects = projects.union(sub_q2).union(sub_q3).union(sub_q4) if fork is not None: if fork is True: projects = projects.filter( model.Project.is_fork == True ) elif fork is False: projects = projects.filter( model.Project.is_fork == False ) if tags: if not isinstance(tags, (list, tuple)): tags = [tags] projects = projects.filter( model.Project.id == model.TagProject.project_id ).filter( model.TagProject.tag.in_(tags) ) if pattern: pattern = pattern.replace('*', '%') if '%' in pattern: projects = projects.filter( model.Project.name.like(pattern) ) else: projects = projects.filter( model.Project.name == pattern ) if namespace: projects = projects.filter( model.Project.namespace == namespace ) query = session.query( model.Project ).filter( model.Project.id.in_(projects.subquery()) ) if sort == 'latest': query = query.order_by( model.Project.date_created.desc() ) elif sort == 'oldest': query = query.order_by( model.Project.date_created.asc() ) else: query = query.order_by( asc(func.lower(model.Project.name)) ) if start is not None: query = query.offset(start) if limit is not None: query = query.limit(limit) if count: return query.count() else: return query.all() def get_project(session, name, user=None, namespace=None): '''Get a project from the database ''' query = session.query( model.Project ).filter( model.Project.name == name ).filter( model.Project.namespace == namespace ) if user is not None: query = query.filter( model.User.user == user ).filter( model.User.id == model.Project.user_id ).filter( model.Project.is_fork == True ) else: query = query.filter( model.Project.is_fork == False ) return query.first() def search_issues( session, repo, issueid=None, issueuid=None, status=None, closed=False, tags=None, assignee=None, author=None, private=None, priority=None, milestones=None, count=False, offset=None, limit=None): ''' Retrieve one or more issues associated to a project with the given criterias. Watch out that the closed argument is incompatible with the status argument. The closed argument will return all the issues whose status is not 'Open', otherwise it will return the issues having the specified status. The `tags` argument can be used to filter the issues returned based on a certain tag. If the `issueid` argument is specified a single Issue object (or None) will be returned instead of a list of Issue objects. :arg session: the session to use to connect to the database. :arg repo: a Project object to which the issues should be associated :type repo: pagure.lib.model.Project :kwarg issueid: the identifier of the issue to look for :type issueid: int or None :kwarg issueuid: the unique identifier of the issue to look for :type issueuid: str or None :kwarg status: the status of the issue to look for (incompatible with the `closed` argument). :type status: str or None :kwarg closed: a boolean indicating whether the issue to retrieve are closed or open (incompatible with the `status` argument). :type closed: bool or None :kwarg tags: a tag the issue(s) returned should be associated with :type tags: str or list(str) or None :kwarg assignee: the name of the user assigned to the issues to search :type assignee: str or None :kwarg author: the name of the user who created the issues to search :type author: str or None :kwarg private: boolean or string to use to include or exclude private tickets. Defaults to False. If False: private tickets are excluded If None: private tickets are included If user name is specified: private tickets reported by that user are included. :type private: False, None or str :kwarg priority: the priority of the issues to search :type priority: int or None :kwarg milestones: a milestone the issue(s) returned should be associated with. :type milestones: str or list(str) or None :kwarg count: a boolean to specify if the method should return the list of Issues or just do a COUNT query. :type count: boolean :return: A single Issue object if issueid is specified, a list of Project objects otherwise. :rtype: Project or [Project] ''' query = session.query( sqlalchemy.distinct(model.Issue.uid) ).filter( model.Issue.project_id == repo.id ) if issueid is not None: query = query.filter( model.Issue.id == issueid ) if issueuid is not None: query = query.filter( model.Issue.uid == issueuid ) if status is not None and not closed: query = query.filter( model.Issue.status == status ) if closed: query = query.filter( model.Issue.status != 'Open' ) if priority: query = query.filter( model.Issue.priority == priority ) if tags is not None and tags != []: if isinstance(tags, basestring): tags = [tags] notags = [] ytags = [] for tag in tags: if tag.startswith('!'): notags.append(tag[1:]) else: ytags.append(tag) if ytags: sub_q2 = session.query( sqlalchemy.distinct(model.Issue.uid) ).filter( model.Issue.project_id == repo.id ).filter( model.Issue.uid == model.TagIssue.issue_uid ).filter( model.TagIssue.tag.in_(ytags) ) if notags: sub_q3 = session.query( sqlalchemy.distinct(model.Issue.uid) ).filter( model.Issue.project_id == repo.id ).filter( model.Issue.uid == model.TagIssue.issue_uid ).filter( model.TagIssue.tag.in_(notags) ) # Adjust the main query based on the parameters specified if ytags and not notags: query = query.filter(model.Issue.uid.in_(sub_q2)) elif not ytags and notags: query = query.filter(~model.Issue.uid.in_(sub_q3)) elif ytags and notags: final_set = set(sub_q2.all()) - set(sub_q3.all()) if final_set: query = query.filter(model.Issue.uid.in_(list(final_set))) if assignee is not None: if str(assignee).lower() not in ['false', '0', 'true', '1']: user2 = aliased(model.User) if assignee.startswith('!'): sub = session.query( model.Issue.uid ).filter( model.Issue.assignee_id == user2.id ).filter( user2.user == assignee[1:] ) query = query.filter( ~model.Issue.uid.in_(sub) ) else: query = query.filter( model.Issue.assignee_id == user2.id ).filter( user2.user == assignee ) elif str(assignee).lower() in ['true', '1']: query = query.filter( model.Issue.assignee_id != None ) else: query = query.filter( model.Issue.assignee_id == None ) if author is not None: query = query.filter( model.Issue.user_id == model.User.id ).filter( model.User.user == author ) if private is False: query = query.filter( model.Issue.private == False ) elif isinstance(private, basestring): user2 = aliased(model.User) query = query.filter( sqlalchemy.or_( model.Issue.private == False, sqlalchemy.and_( model.Issue.private == True, model.Issue.user_id == user2.id, user2.user == private, ) ) ) if milestones is not None and milestones != []: if isinstance(milestones, basestring): milestones = [milestones] query = query.filter( model.Issue.milestone.in_(milestones) ) query = session.query( model.Issue ).filter( model.Issue.uid.in_(query.subquery()) ).filter( model.Issue.project_id == repo.id ) query = query.order_by( model.Issue.date_created.desc() ) if issueid is not None or issueuid is not None: output = query.first() elif count: output = query.count() else: if offset: query = query.offset(offset) if limit: query = query.limit(limit) output = query.all() return output def get_tags_of_project(session, project, pattern=None): ''' Returns the list of tags associated with the issues of a project. ''' query = session.query( model.Tag ).filter( model.Tag.tag == model.TagIssue.tag ).filter( model.TagIssue.issue_uid == model.Issue.uid ).filter( model.Issue.project_id == project.id ).order_by( model.Tag.tag ) if pattern: query = query.filter( model.Tag.tag.ilike(pattern.replace('*', '%')) ) return query.all() def get_tag(session, tag): ''' Returns a Tag object for the given tag text. ''' query = session.query( model.Tag ).filter( model.Tag.tag == tag ) return query.first() def search_pull_requests( session, requestid=None, project_id=None, project_id_from=None, status=None, author=None, assignee=None, count=False, offset=None, limit=None): ''' Retrieve the specified issue ''' query = session.query( model.PullRequest ).order_by( model.PullRequest.id.desc() ) if requestid: query = query.filter( model.PullRequest.id == requestid ) if project_id: query = query.filter( model.PullRequest.project_id == project_id ) if project_id_from: query = query.filter( model.PullRequest.project_id_from == project_id_from ) if status is not None: if isinstance(status, bool): if status: query = query.filter( model.PullRequest.status == 'Open' ) else: query = query.filter( model.PullRequest.status != 'Open' ) else: query = query.filter( model.PullRequest.status == status ) if assignee is not None: if str(assignee).lower() not in ['false', '0', 'true', '1']: user2 = aliased(model.User) if assignee.startswith('!'): sub = session.query( model.PullRequest.uid ).filter( model.PullRequest.assignee_id == user2.id ).filter( user2.user == assignee[1:] ) query = query.filter( ~model.PullRequest.uid.in_(sub) ) else: query = query.filter( model.PullRequest.assignee_id == user2.id ).filter( user2.user == assignee ) elif str(assignee).lower() in ['true', '1']: query = query.filter( model.PullRequest.assignee_id != None ) else: query = query.filter( model.PullRequest.assignee_id == None ) if author is not None: query = query.filter( model.PullRequest.user_id == model.User.id ).filter( model.User.user == author ) if requestid: output = query.first() elif count: output = query.count() else: if offset: query = query.offset(offset) if limit: query = query.limit(limit) output = query.all() return output def close_pull_request(session, request, user, requestfolder, merged=True): ''' Close the provided pull-request. ''' user_obj = get_user(session, user) if merged is True: request.status = 'Merged' else: request.status = 'Closed' request.closed_by_id = user_obj.id request.closed_at = datetime.datetime.utcnow() session.add(request) session.flush() if merged is True: pagure.lib.notify.notify_merge_pull_request(request, user_obj) else: pagure.lib.notify.notify_cancelled_pull_request(request, user_obj) pagure.lib.git.update_git( request, repo=request.project, repofolder=requestfolder) pagure.lib.add_pull_request_comment( session, request, commit=None, tree_id=None, filename=None, row=None, comment='Pull-Request has been %s by %s' % ( request.status.lower(), user), user=user, requestfolder=requestfolder, notify=False, notification=True ) pagure.lib.notify.log( request.project, topic='pull-request.closed', msg=dict( pullrequest=request.to_json(public=True), merged=merged, agent=user_obj.username, ), redis=REDIS, ) def reset_status_pull_request(session, project): ''' Reset the status of all opened Pull-Requests of a project. ''' requests = search_pull_requests( session, project_id=project.id, status='Open') for request in requests: request.merge_status = None session.add(request) session.commit() def get_issue_statuses(session): ''' Return the complete list of status an issue can have. ''' output = [] statuses = session.query(model.StatusIssue).all() for status in statuses: output.append(status.status) return output def get_issue_comment(session, issue_uid, comment_id): ''' Return a specific comment of a specified issue. ''' query = session.query( model.IssueComment ).filter( model.IssueComment.issue_uid == issue_uid ).filter( model.IssueComment.id == comment_id ) return query.first() def get_request_comment(session, request_uid, comment_id): ''' Return a specific comment of a specified request. ''' query = session.query( model.PullRequestComment ).filter( model.PullRequestComment.pull_request_uid == request_uid ).filter( model.PullRequestComment.id == comment_id ) return query.first() def get_issue_by_uid(session, issue_uid): ''' Return the issue corresponding to the specified unique identifier. :arg session: the session to use to connect to the database. :arg issue_uid: the unique identifier of an issue. This identifier is unique accross all projects on this pagure instance and should be unique accross multiple pagure instances as well :type issue_uid: str or None :return: A single Issue object. :rtype: pagure.lib.model.Issue ''' query = session.query( model.Issue ).filter( model.Issue.uid == issue_uid ) return query.first() def get_request_by_uid(session, request_uid): ''' Return the request corresponding to the specified unique identifier. :arg session: the session to use to connect to the database. :arg request_uid: the unique identifier of a request. This identifier is unique accross all projects on this pagure instance and should be unique accross multiple pagure instances as well :type request_uid: str or None :return: A single Issue object. :rtype: pagure.lib.model.PullRequest ''' query = session.query( model.PullRequest ).filter( model.PullRequest.uid == request_uid ) return query.first() def get_pull_request_flag_by_uid(session, flag_uid): ''' Return the flag corresponding to the specified unique identifier. :arg session: the session to use to connect to the database. :arg flag_uid: the unique identifier of a request. This identifier is unique accross all flags on this pagure instance and should be unique accross multiple pagure instances as well :type request_uid: str or None :return: A single Issue object. :rtype: pagure.lib.model.PullRequestFlag ''' query = session.query( model.PullRequestFlag ).filter( model.PullRequestFlag.uid == flag_uid.strip() if flag_uid else None ) return query.first() def set_up_user(session, username, fullname, default_email, emails=None, ssh_key=None, keydir=None): ''' Set up a new user into the database or update its information. ''' user = search_user(session, username=username) if not user: user = model.User( user=username, fullname=fullname, default_email=default_email ) session.add(user) session.flush() if user.fullname != fullname: user.fullname = fullname session.add(user) session.flush() if emails: emails = set(emails) else: emails = set() emails.add(default_email) for email in emails: add_email_to_user(session, user, email) if ssh_key and not user.public_ssh_key: update_user_ssh(session, user, ssh_key, keydir) return user def add_email_to_user(session, user, user_email): ''' Add the provided email to the specified user. ''' emails = [email.email for email in user.emails] if user_email not in emails: useremail = model.UserEmail( user_id=user.id, email=user_email) session.add(useremail) session.flush() def update_user_ssh(session, user, ssh_key, keydir): ''' Set up a new user into the database or update its information. ''' if isinstance(user, basestring): user = get_user(session, user) user.public_ssh_key = ssh_key if keydir and user.public_ssh_key: create_user_ssh_keys_on_disk(user, keydir) pagure.lib.git.generate_gitolite_acls() session.add(user) session.flush() def avatar_url_from_openid(openid, size=64, default='retro', dns=False): """ Our own implementation since fas doesn't support this nicely yet. """ if dns: # pragma: no cover # This makes an extra DNS SRV query, which can slow down our webapps. # It is necessary for libravatar federation, though. import libravatar return libravatar.libravatar_url( openid=openid, size=size, default=default, ) else: import urllib import hashlib query = urllib.urlencode({'s': size, 'd': default}) hashhex = hashlib.sha256(openid).hexdigest() return "https://seccdn.libravatar.org/avatar/%s?%s" % ( hashhex, query) def update_tags(session, obj, tags, username, ticketfolder): """ Update the tags of a specified object (adding or removing them). This object can be either an issue or a project. """ if isinstance(tags, basestring): tags = [tags] toadd = set(tags) - set(obj.tags_text) torm = set(obj.tags_text) - set(tags) messages = [] if toadd: messages.append( add_tag_obj( session, obj=obj, tags=toadd, user=username, ticketfolder=ticketfolder, ) ) if torm: messages.append( remove_tags_obj( session, obj=obj, tags=torm, user=username, ticketfolder=ticketfolder, ) ) session.commit() return messages def update_dependency_issue( session, repo, issue, depends, username, ticketfolder): """ Update the dependency of a specified issue (adding or removing them) """ if isinstance(depends, basestring): depends = [depends] toadd = set(depends) - set(issue.depends_text) torm = set(issue.depends_text) - set(depends) messages = [] # Add issue depending for depend in toadd: issue_depend = search_issues(session, repo, issueid=depend) if issue_depend is None: continue if issue_depend.id in issue.depends_text: # pragma: no cover # we should never be in this case but better safe than sorry... continue messages.append( add_issue_dependency( session, issue=issue_depend, issue_blocked=issue, user=username, ticketfolder=ticketfolder, ) ) # Remove issue depending for depend in torm: issue_depend = search_issues(session, repo, issueid=depend) if issue_depend is None: # pragma: no cover # We cannot test this as it would mean we managed to put in an # invalid ticket as dependency earlier continue if issue_depend.id not in issue.depends_text: # pragma: no cover # we should never be in this case but better safe than sorry... continue messages.append( remove_issue_dependency( session, issue=issue, issue_blocked=issue_depend, user=username, ticketfolder=ticketfolder, ) ) session.commit() return messages def update_blocked_issue( session, repo, issue, blocks, username, ticketfolder): """ Update the upstream dependency of a specified issue (adding or removing them) """ if isinstance(blocks, basestring): blocks = [blocks] toadd = set(blocks) - set(issue.blocks_text) torm = set(issue.blocks_text) - set(blocks) messages = [] # Add issue blocked for block in toadd: issue_block = search_issues(session, repo, issueid=block) if issue_block is None: continue if issue_block.id in issue.blocks_text: # pragma: no cover # we should never be in this case but better safe than sorry... continue messages.append( add_issue_dependency( session, issue=issue, issue_blocked=issue_block, user=username, ticketfolder=ticketfolder, ) ) session.commit() # Remove issue blocked for block in torm: issue_block = search_issues(session, repo, issueid=block) if issue_block is None: # pragma: no cover # We cannot test this as it would mean we managed to put in an # invalid ticket as dependency earlier continue if issue_block.id not in issue.blocks_text: # pragma: no cover # we should never be in this case but better safe than sorry... continue messages.append( remove_issue_dependency( session, issue=issue_block, issue_blocked=issue, user=username, ticketfolder=ticketfolder, ) ) session.commit() return messages def add_user_pending_email(session, userobj, email): ''' Add the provided user to the specified user. ''' other_user = search_user(session, email=email) if other_user and other_user != userobj: raise pagure.exceptions.PagureException( 'Someone else has already registered this email' ) pending_email = search_pending_email(session, email=email) if pending_email: raise pagure.exceptions.PagureException( 'This email is already pending confirmation' ) tmpemail = pagure.lib.model.UserEmailPending( user_id=userobj.id, token=pagure.lib.login.id_generator(40), email=email ) session.add(tmpemail) session.flush() pagure.lib.notify.notify_new_email(tmpemail, user=userobj) def resend_pending_email(session, userobj, email): ''' Resend to the user the confirmation email for the provided email address. ''' other_user = search_user(session, email=email) if other_user and other_user != userobj: raise pagure.exceptions.PagureException( 'Someone else has already registered this email address' ) pending_email = search_pending_email(session, email=email) if not pending_email: raise pagure.exceptions.PagureException( 'This email address has already been confirmed' ) pending_email.token = pagure.lib.login.id_generator(40) session.add(pending_email) session.flush() pagure.lib.notify.notify_new_email(pending_email, user=userobj) def search_pending_email(session, email=None, token=None): ''' Searches the database for the pending email matching the given criterias. :arg session: the session to use to connect to the database. :kwarg email: the email to look for :type email: string or None :kwarg token: the token of the pending email to look for :type token: string or None :return: A single UserEmailPending object :rtype: UserEmailPending ''' query = session.query( model.UserEmailPending ) if email is not None: query = query.filter( model.UserEmailPending.email == email ) if token is not None: query = query.filter( model.UserEmailPending.token == token ) output = query.first() return output def generate_hook_token(session): ''' For each project in the database, re-generate a unique hook_token. ''' for project in search_projects(session): project.hook_token = pagure.lib.login.id_generator(40) session.add(project) session.commit() def get_group_types(session, group_type=None): ''' Return the list of type a group can have. ''' query = session.query( model.PagureGroupType ).order_by( model.PagureGroupType.group_type ) if group_type: query = query.filter( model.PagureGroupType.group_type == group_type ) return query.all() def search_groups(session, pattern=None, group_name=None, group_type=None): ''' Return the groups based on the criteria specified. ''' query = session.query( model.PagureGroup ).order_by( model.PagureGroup.group_type ) if pattern: pattern = pattern.replace('*', '%') query = query.filter( model.PagureGroup.group_name.like(pattern) ) if group_name: query = query.filter( model.PagureGroup.group_name == group_name ) if group_type: query = query.filter( model.PagureGroup.group_type == group_type ) if group_name: return query.first() else: return query.all() def add_user_to_group(session, username, group, user, is_admin): ''' Add the specified user to the given group. ''' new_user = search_user(session, username=username) if not new_user: raise pagure.exceptions.PagureException( 'No user `%s` found' % username) action_user = user user = search_user(session, username=user) if not user: raise pagure.exceptions.PagureException( 'No user `%s` found' % action_user) if group.group_name not in user.groups and not is_admin\ and user.username != group.creator.username: raise pagure.exceptions.PagureException( 'You are not allowed to add user to this group') for guser in group.users: if guser.username == new_user.username: return 'User `%s` already in the group, nothing to change.' % ( new_user.username) grp = model.PagureUserGroup( group_id=group.id, user_id=new_user.id ) session.add(grp) session.flush() return 'User `%s` added to the group `%s`.' % ( new_user.username, group.group_name) def edit_group_info( session, group, display_name, description, user, is_admin): ''' Edit the information regarding a given group. ''' action_user = user user = search_user(session, username=user) if not user: raise pagure.exceptions.PagureException( 'No user `%s` found' % action_user) if group.group_name not in user.groups \ and not is_admin \ and user.username != group.creator.username: raise pagure.exceptions.PagureException( 'You are not allowed to edit this group') edits = [] if display_name and display_name != group.display_name: group.display_name = display_name edits.append('display_name') if description and description != group.description: group.description = description edits.append('description') session.add(group) session.flush() msg = 'Nothing changed' if edits: pagure.lib.notify.log( None, topic='group.edit', msg=dict( group=group.to_json(public=True), fields=edits, agent=user.username, ), redis=REDIS, ) msg = 'Group "%s" (%s) edited' % ( group.display_name, group.group_name) return msg def delete_user_of_group(session, username, groupname, user, is_admin, force=False): ''' Removes the specified user from the given group. ''' group_obj = search_groups(session, group_name=groupname) if not group_obj: raise pagure.exceptions.PagureException( 'No group `%s` found' % groupname) drop_user = search_user(session, username=username) if not drop_user: raise pagure.exceptions.PagureException( 'No user `%s` found' % username) action_user = user user = search_user(session, username=user) if not user: raise pagure.exceptions.PagureException( 'Could not find user %s' % action_user) if group_obj.group_name not in user.groups and not is_admin: raise pagure.exceptions.PagureException( 'You are not allowed to remove user from this group') if drop_user.username == group_obj.creator.username and not force: raise pagure.exceptions.PagureException( 'The creator of a group cannot be removed') user_grp = get_user_group(session, drop_user.id, group_obj.id) if not user_grp: raise pagure.exceptions.PagureException( 'User `%s` could not be found in the group `%s`' % ( username, groupname)) session.delete(user_grp) session.flush() def add_group( session, group_name, display_name, description, group_type, user, is_admin, blacklist): ''' Creates a new group with the given information. ''' if ' ' in group_name: raise pagure.exceptions.PagureException( 'Spaces are not allowed in group names: %s' % group_name) if group_name in blacklist: raise pagure.exceptions.PagureException( 'This group name has been blacklisted, ' 'please choose another one') group_types = ['user'] if is_admin: group_types = [ grp.group_type for grp in get_group_types(session) ] if not is_admin: group_type = 'user' if group_type not in group_types: raise pagure.exceptions.PagureException( 'Invalide type for this group') username = user user = search_user(session, username=user) if not user: raise pagure.exceptions.PagureException( 'Could not find user %s' % username) group = search_groups(session, group_name=group_name) if group: raise pagure.exceptions.PagureException( 'There is already a group named %s' % group_name) grp = pagure.lib.model.PagureGroup( group_name=group_name, display_name=display_name, description=description, group_type=group_type, user_id=user.id, ) session.add(grp) session.flush() return add_user_to_group( session, user.username, grp, user.username, is_admin) def get_user_group(session, userid, groupid): ''' Return a specific user_group for the specified group and user identifiers. :arg session: the session with which to connect to the database. ''' query = session.query( model.PagureUserGroup ).filter( model.PagureUserGroup.user_id == userid ).filter( model.PagureUserGroup.group_id == groupid ) return query.first() def is_group_member(session, user, groupname): """ Return whether the user is a member of the specified group. """ if not user: return False user = search_user(session, username=user) if not user: return False return groupname in user.groups def get_api_token(session, token_str): """ Return the Token object corresponding to the provided token string if there is any, returns None otherwise. """ query = session.query( model.Token ).filter( model.Token.id == token_str ) return query.first() def get_acls(session): """ Returns all the possible ACLs a token can have according to the database. """ query = session.query( model.ACL ).order_by( model.ACL.name ) return query.all() def add_token_to_user(session, project, acls, username): """ Create a new token for the specified user on the specified project with the given ACLs. """ acls_obj = session.query( model.ACL ).filter( model.ACL.name.in_(acls) ).all() user = search_user(session, username=username) token = pagure.lib.model.Token( id=pagure.lib.login.id_generator(64), user_id=user.id, project_id=project.id, expiration=datetime.datetime.utcnow() + datetime.timedelta(days=60) ) session.add(token) session.flush() for acl in acls_obj: item = pagure.lib.model.TokenAcl( token_id=token.id, acl_id=acl.id, ) session.add(item) session.commit() return 'Token created' def text2markdown(text, extended=True): """ Simple text to html converter using the markdown library. """ md_processor = markdown.Markdown(safe_mode="escape") if extended: # Install our markdown modifications md_processor = markdown.Markdown(extensions=['pagure.pfmarkdown']) if text: # Hack to allow blockquotes to be marked by ~~~ ntext = [] indent = False for line in text.split('\n'): if line.startswith('~~~'): indent = not indent continue if indent: line = ' %s' % line ntext.append(line) return clean_input(md_processor.convert('\n'.join(ntext))) return '' def filter_img_src(name, value): ''' Filter in img html tags images coming from a different domain. ''' if name in ('alt', 'height', 'width', 'class'): return True if name == 'src': parsed = urlparse.urlparse(value) return (not parsed.netloc) or parsed.netloc == urlparse.urlparse( pagure.APP.config['APP_URL']).netloc return False def clean_input(text, ignore=None): """ For a given html text, escape everything we do not want to support to avoid potential security breach. """ if ignore and not isinstance(ignore, (tuple, set, list)): ignore = [ignore] attrs = bleach.ALLOWED_ATTRIBUTES if not ignore or not 'img' in ignore: attrs['img'] = filter_img_src tags = bleach.ALLOWED_TAGS + [ 'p', 'br', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'table', 'td', 'tr', 'th', 'col', 'tbody', 'pre', 'img', 'hr', 'dl', 'dt', 'dd', 'span', 'kbd', 'var', 'del', ] if ignore: for tag in ignore: if tag in tags: tags.remove(tag) return bleach.clean(text, tags=tags, attributes=attrs) def could_be_text(text): """ Returns wether we think this chain of character could be text or not """ try: text.decode('utf-8') return True except: return False def get_pull_request_of_user(session, username): '''List the opened pull-requests of an user. These pull-requests have either been opened by that user or against projects that user has commit on. ''' projects = session.query( sqlalchemy.distinct(model.Project.id) ) projects = projects.filter( # User created the project sqlalchemy.and_( model.User.user == username, model.User.id == model.Project.user_id, ) ) sub_q2 = session.query( model.Project.id ).filter( # User got commit right sqlalchemy.and_( model.User.user == username, model.User.id == model.ProjectUser.user_id, model.ProjectUser.project_id == model.Project.id ) ) sub_q3 = session.query( model.Project.id ).filter( # User created a group that has commit right sqlalchemy.and_( model.User.user == username, model.PagureGroup.user_id == model.User.id, model.PagureGroup.group_type == 'user', model.PagureGroup.id == model.ProjectGroup.group_id, model.Project.id == model.ProjectGroup.project_id, ) ) sub_q4 = session.query( model.Project.id ).filter( # User is part of a group that has commit right sqlalchemy.and_( model.User.user == username, model.PagureUserGroup.user_id == model.User.id, model.PagureUserGroup.group_id == model.PagureGroup.id, model.PagureGroup.group_type == 'user', model.PagureGroup.id == model.ProjectGroup.group_id, model.Project.id == model.ProjectGroup.project_id, ) ) projects = projects.union(sub_q2).union(sub_q3).union(sub_q4) query = session.query( model.PullRequest ).filter( model.PullRequest.project_id.in_(projects.subquery()) ).order_by( model.PullRequest.date_created.desc() ) return query.all() def update_watch_status(session, project, user, watch): ''' Update the user status for watching a project. ''' user_obj = get_user(session, user) if not user_obj: raise pagure.exceptions.PagureException( 'No user with username: %s' % user) watcher = session.query( model.Watcher ).filter( sqlalchemy.and_( model.Watcher.project_id == project.id, model.Watcher.user_id == user_obj.id, ) ).first() if not watcher: watcher = model.Watcher( project_id=project.id, user_id=user_obj.id, watch=watch ) else: watcher.watch = watch session.add(watcher) session.flush() msg_success = 'You are now watching this repo.' if not int(watch): msg_success = 'You are no longer watching this repo.' return msg_success def is_watching(session, user, reponame, repouser=None, namespace=None): ''' Check user watching the project. ''' if user is None: return False user_obj = search_user(session, username=user.username) if not user_obj: return False query = session.query( model.Watcher ).filter( model.Watcher.user_id == user_obj.id ).filter( model.Watcher.project_id == model.Project.id ).filter( model.Project.name == reponame ) if repouser is not None: query = query.filter( model.User.user == repouser ).filter( model.User.id == model.Project.user_id ).filter( model.Project.is_fork == True ) else: query = query.filter( model.Project.is_fork == False ) if namespace is not None: query = query.filter( model.Project.namespace == namespace ) watcher = query.first() if watcher: return watcher.watch project = pagure.lib.get_project( session, reponame, user=repouser, namespace=namespace) if not project: return False if user.username == project.user.username: return True for group in project.groups: for guser in group.users: if user.username == guser.username: return True return False def user_watch_list(session, user): ''' Returns list of all the projects which the user is watching ''' user_obj = search_user(session, username=user) if not user_obj: return [] unwatched = session.query( model.Watcher ).filter( model.Watcher.user_id == user_obj.id ).filter( model.Watcher.watch == False ) unwatched_list = [] if unwatched: unwatched_list = [unwatch.project for unwatch in unwatched.all()] watched = session.query( model.Watcher ).filter( model.Watcher.user_id == user_obj.id ).filter( model.Watcher.watch == True ) watched_list = [] if watched: watched_list = [watch.project for watch in watched.all()] user_projects = search_projects(session, username=user_obj.user) watch = set(watched_list + user_projects) for project in user_projects: if project in unwatched_list: watch.remove(project) return sorted(list(watch), key=lambda proj: proj.name) def save_report(session, repo, name, url, username): """ Save the report of issues based on the given URL of the project. """ url_obj = urlparse.urlparse(url) url = url_obj.geturl().replace(url_obj.query, '') query = dict(urlparse.parse_qsl(url_obj.query)) reports = repo.reports reports[name] = query repo.reports = reports session.add(repo)