From badf0250d357c391f12ec1e8b5db152a344bf9e7 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Oct 08 2014 09:52:10 +0000 Subject: Move all the controllers to their own folder --- diff --git a/progit/admin.py b/progit/admin.py deleted file mode 100644 index 35093a3..0000000 --- a/progit/admin.py +++ /dev/null @@ -1,65 +0,0 @@ -#-*- coding: utf-8 -*- - -""" - (c) 2014 - Copyright Red Hat Inc - - Authors: - Pierre-Yves Chibon - -""" - -from functools import wraps - -import flask - -from progit import (APP, SESSION, LOG, cla_required, authenticated, - generate_gitolite_acls, generate_authorized_key_file, - is_admin) - - -def admin_required(function): - """ Flask decorator to retrict access to admins of progit. - """ - @wraps(function) - def decorated_function(*args, **kwargs): - """ Decorated function, actually does the work. """ - if not authenticated(): - return flask.redirect( - flask.url_for('auth_login', next=flask.request.url)) - elif not is_admin(): - flask.flash('Access restricted', 'error') - return flask.redirect(flask.url_for('.index')) - return function(*args, **kwargs) - return decorated_function - - -### Application - - -@APP.route('/admin') -@admin_required -def admin_index(): - """ Front page of the admin section of the application. - """ - - return flask.render_template( - 'admin_index.html', - ) - - -@APP.route('/admin/gitolite') -@admin_required -def admin_generate_acl(): - """ Regenerate the gitolite ACL file. """ - generate_gitolite_acls() - flask.flash('Gitolite ACLs updated') - return flask.redirect(flask.url_for('admin_index')) - - -@APP.route('/admin/ssh') -@admin_required -def admin_refresh_ssh(): - """ Regenerate the gitolite ACL file. """ - generate_authorized_key_file() - flask.flash('Authorized file updated') - return flask.redirect(flask.url_for('admin_index')) diff --git a/progit/app.py b/progit/app.py deleted file mode 100644 index 5f1b05e..0000000 --- a/progit/app.py +++ /dev/null @@ -1,245 +0,0 @@ -#-*- coding: utf-8 -*- - -""" - (c) 2014 - Copyright Red Hat Inc - - Authors: - Pierre-Yves Chibon - -""" - -import flask -import os -from math import ceil - -import pygit2 -from sqlalchemy.exc import SQLAlchemyError -from pygments import highlight -from pygments.lexers import guess_lexer -from pygments.lexers.text import DiffLexer -from pygments.formatters import HtmlFormatter - - -import progit.exceptions -import progit.lib -import progit.forms -from progit import (APP, SESSION, LOG, __get_file_in_tree, cla_required, - generate_gitolite_acls, generate_gitolite_key, - generate_authorized_key_file) - - -def chunks(item_list, chunks_size): - """ Yield successive n-sized chunks from item_list. - """ - for i in xrange(0, len(item_list), chunks_size): - yield item_list[i: i + chunks_size] - - -### Application - - -@APP.route('/') -def index(): - """ Front page of the application. - """ - page = flask.request.args.get('page', 1) - try: - page = int(page) - except ValueError: - page = 1 - - limit = APP.config['ITEM_PER_PAGE'] - start = limit * (page - 1) - - repos = progit.lib.list_projects( - SESSION, - fork=False, - start=start, - limit=limit) - num_repos = progit.lib.list_projects( - SESSION, - fork=False, - count=True) - - total_page = int(ceil(num_repos / float(limit))) - - return flask.render_template( - 'index.html', - repos=chunks(repos, 3), - total_page=total_page, - page=page, - ) - - -@APP.route('/users/') -def view_users(): - """ Present the list of users. - """ - page = flask.request.args.get('page', 1) - try: - page = int(page) - except ValueError: - page = 1 - - users = progit.lib.get_all_users(SESSION) - - limit = APP.config['ITEM_PER_PAGE'] - start = limit * (page - 1) - end = limit * page - users_length = len(users) - users = users[start:end] - - total_page = int(ceil(users_length / float(limit))) - - return flask.render_template( - 'user_list.html', - users=users, - users_length=users_length, - total_page=total_page, - page=page, - ) - - -@APP.route('/user/') -def view_user(username): - """ Front page of a specific user. - """ - - repopage = flask.request.args.get('repopage', 1) - try: - repopage = int(repopage) - except ValueError: - repopage = 1 - - forkpage = flask.request.args.get('forkpage', 1) - try: - forkpage = int(forkpage) - except ValueError: - forkpage = 1 - - limit = APP.config['ITEM_PER_PAGE'] - repo_start = limit * (repopage - 1) - fork_start = limit * (forkpage - 1) - - repos = progit.lib.list_projects( - SESSION, - username=username, - fork=False, - start=repo_start, - limit=limit) - repos_length = progit.lib.list_projects( - SESSION, - username=username, - fork=False, - count=True) - - forks = progit.lib.list_projects( - SESSION, - username=username, - fork=True, - start=fork_start, - limit=limit) - forks_length = progit.lib.list_projects( - SESSION, - username=username, - fork=True, - count=True) - - total_page_repos = int(ceil(repos_length / float(limit))) - total_page_forks = int(ceil(forks_length / float(limit))) - - repos_obj = [ - pygit2.Repository( - os.path.join(APP.config['GIT_FOLDER'], repo.path)) - for repo in repos] - - forks_obj = [ - pygit2.Repository( - os.path.join(APP.config['FORK_FOLDER'], repo.path)) - for repo in forks] - - return flask.render_template( - 'user_info.html', - username=username, - repos=repos, - repos_obj=repos_obj, - total_page_repos=total_page_repos, - forks=forks, - forks_obj=forks_obj, - total_page_forks=total_page_forks, - repopage=repopage, - forkpage=forkpage, - ) - - -@APP.route('/new/', methods=('GET', 'POST')) -@cla_required -def new_project(): - """ Form to create a new project. - """ - form = progit.forms.ProjectForm() - if form.validate_on_submit(): - name = form.name.data - description = form.description.data - - try: - message = progit.lib.new_project( - SESSION, - name=name, - description=description, - user=flask.g.fas_user.username, - gitfolder=APP.config['GIT_FOLDER'], - docfolder=APP.config['DOCS_FOLDER'], - ticketfolder=APP.config['TICKETS_FOLDER'] - ) - SESSION.commit() - generate_gitolite_acls() - flask.flash(message) - return flask.redirect(flask.url_for('view_repo', repo=name)) - except progit.exceptions.ProgitException, err: - flask.flash(str(err), 'error') - except SQLAlchemyError, err: # pragma: no cover - SESSION.rollback() - flask.flash(str(err), 'error') - - return flask.render_template( - 'new_project.html', - form=form, - ) - - -@APP.route('/settings/', methods=('GET', 'POST')) -@cla_required -def user_settings(): - """ Update the user settings. - """ - user = progit.lib.get_user(SESSION, flask.g.fas_user.username) - - form = progit.forms.UserSettingsForm() - if form.validate_on_submit(): - ssh_key = form.ssh_key.data - - try: - message = progit.lib.update_user_ssh( - SESSION, - user=user, - ssh_key=ssh_key, - ) - if message != 'Nothing to update': - generate_gitolite_key(user.user, ssh_key) - generate_authorized_key_file() - SESSION.commit() - flask.flash(message) - return flask.redirect( - flask.url_for('view_user', username=user.user)) - except SQLAlchemyError, err: # pragma: no cover - SESSION.rollback() - flask.flash(str(err), 'error') - elif flask.request.method == 'GET': - form.ssh_key.data = user.public_ssh_key - - return flask.render_template( - 'user_settings.html', - user=user, - form=form, - ) diff --git a/progit/docs.py b/progit/docs.py deleted file mode 100644 index ae098ee..0000000 --- a/progit/docs.py +++ /dev/null @@ -1,161 +0,0 @@ -#-*- coding: utf-8 -*- - -""" - (c) 2014 - Copyright Red Hat Inc - - Authors: - Pierre-Yves Chibon - -""" - -import flask -import os -from math import ceil - -import pygit2 -from sqlalchemy.exc import SQLAlchemyError -from pygments import highlight -from pygments.lexers import guess_lexer -from pygments.lexers.text import DiffLexer -from pygments.formatters import HtmlFormatter - - -import progit.doc_utils -import progit.exceptions -import progit.lib -import progit.forms -from progit import APP, SESSION, LOG, cla_required - - -def __get_tree(repo_obj, tree, filepath, startswith=False): - ''' Retrieve the entry corresponding to the provided filename in a - given tree. - ''' - filename = filepath[0] - if isinstance(tree, pygit2.Blob): - return (tree, None) - cnt = 0 - for el in tree: - cnt += 1 - ok = False - if el.name.startswith(filename): - ok = True - if el.name == filename: - ok = True - if ok and len(filepath) == 1: - return (el, tree) - elif ok: - return __get_tree( - repo_obj, repo_obj[el.oid], filepath[1:], - startswith=startswith) - - if len(filepath) == 1: - return None, tree - else: - return __get_tree( - repo_obj, repo_obj[tree.oid], filepath[1:], - startswith=startswith) - - -def __get_tree_and_content(repo_obj, commit, path, startswith): - ''' Return the tree and the content of the specified file. ''' - - (blob_or_tree, tree_obj) = __get_tree( - repo_obj, commit.tree, path, startswith=startswith) - - if blob_or_tree is None: - return tree_obj, None - - if not repo_obj[blob_or_tree.oid]: - flask.abort(404, 'File not found') - - blob_or_tree_obj = repo_obj[blob_or_tree.oid] - blob = repo_obj[blob_or_tree.oid] - - content = None - if isinstance(blob, pygit2.Blob): # Returned a file - name, ext = os.path.splitext(blob_or_tree.name) - content = progit.doc_utils.convert_readme(blob_or_tree_obj.data, ext) - else: # Returned a tree - raise progit.exceptions.FileNotFoundException('File not found') - - tree = sorted(tree_obj, key=lambda x: x.filemode) - return (tree, content) - - -## URLs - - -@APP.route('//docs') -@APP.route('//docs/') -@APP.route('//docs/') -@APP.route('//docs//') -@APP.route('/fork///docs') -@APP.route('/fork///docs/') -@APP.route('/fork///docs/') -@APP.route('/fork///docs//') -def view_docs(repo, username=None, branchname=None, filename=None): - """ Display the documentation - """ - status = flask.request.args.get('status', None) - - repo = progit.lib.get_project(SESSION, repo, user=username) - - if not repo: - flask.abort(404, 'Project not found') - - if not repo.project_docs: - flask.abort(404, 'No documentation found for this project') - - reponame = os.path.join(APP.config['DOCS_FOLDER'], repo.path) - if not os.path.exists(reponame): - flask.flash( - 'No docs repository could be found, please contact an admin', - 'error') - return flask.redirect(flask.url_for( - 'view_repo', repo=repo.name, username=username)) - - repo_obj = pygit2.Repository(reponame) - - if branchname in repo_obj.listall_branches(): - branch = repo_obj.lookup_branch(branchname) - commit = branch.get_object() - else: - if not repo_obj.is_empty: - commit = repo_obj[repo_obj.head.target] - else: - commit = None - branchname = 'master' - - content = None - tree = None - startswith = False - if not filename: - path = ['index'] - startswith = True - else: - path = filename.split('/') - - if commit: - try: - (tree, content) = __get_tree_and_content( - repo_obj, commit, path, startswith) - except progit.exceptions.FileNotFoundException: - if not path[0].startswith('index'): - path.append('index') - filename = filename + '/' - - (tree, content) = __get_tree_and_content( - repo_obj, commit, path, startswith=True) - - return flask.render_template( - 'docs.html', - select='docs', - repo_obj=repo_obj, - repo=repo, - username=username, - branchname=branchname, - filename=filename, - tree=tree, - content=content, - ) diff --git a/progit/fork.py b/progit/fork.py deleted file mode 100644 index 385449c..0000000 --- a/progit/fork.py +++ /dev/null @@ -1,584 +0,0 @@ -#-*- coding: utf-8 -*- - -""" - (c) 2014 - Copyright Red Hat Inc - - Authors: - Pierre-Yves Chibon - -""" - -import flask -import os -import shutil -import tempfile -from math import ceil - -import pygit2 -from sqlalchemy.exc import SQLAlchemyError -from pygments import highlight -from pygments.lexers import guess_lexer -from pygments.lexers.text import DiffLexer -from pygments.formatters import HtmlFormatter - - -import progit.doc_utils -import progit.lib -import progit.forms -from progit import (APP, SESSION, LOG, __get_file_in_tree, cla_required, - is_repo_admin, generate_gitolite_acls) - - -@APP.route('//request-pulls') -@APP.route('/fork///request-pulls') -def request_pulls(repo, username=None): - """ Request pulling the changes from the fork into the project. - """ - status = flask.request.args.get('status', True) - - repo = progit.lib.get_project(SESSION, repo, user=username) - - if not repo: - flask.abort(404, 'Project not found') - - if status is False or str(status).lower() == 'closed': - requests = progit.lib.get_pull_requests( - SESSION, project_id=repo.id, status=False) - else: - requests = progit.lib.get_pull_requests( - SESSION, project_id=repo.id, status=status) - - return flask.render_template( - 'requests.html', - select='requests', - repo=repo, - username=username, - requests=requests, - status=status, - ) - - -@APP.route('//request-pull/') -@APP.route('/fork///request-pull/') -def request_pull(repo, requestid, username=None): - """ Request pulling the changes from the fork into the project. - """ - - repo = progit.lib.get_project(SESSION, repo, user=username) - - if not repo: - flask.abort(404, 'Project not found') - - request = progit.lib.get_pull_request( - SESSION, project_id=repo.id, requestid=requestid) - - if not request: - flask.abort(404, 'Pull-request not found') - - repo = request.repo_from - - if repo.is_fork: - repopath = os.path.join(APP.config['FORK_FOLDER'], repo.path) - else: - repopath = os.path.join(APP.config['GIT_FOLDER'], repo.path) - repo_obj = pygit2.Repository(repopath) - - if repo.parent: - parentname = os.path.join(APP.config['GIT_FOLDER'], repo.parent.path) - else: - parentname = os.path.join(APP.config['GIT_FOLDER'], repo.path) - orig_repo = pygit2.Repository(parentname) - - diff_commits = [] - diffs = [] - repo_commit = repo_obj[request.stop_id] - if not repo_obj.is_empty and not orig_repo.is_empty: - orig_commit = orig_repo[ - orig_repo.lookup_branch('master').get_object().hex] - - master_commits = [ - commit.oid.hex - for commit in orig_repo.walk( - orig_repo.lookup_branch('master').get_object().hex, - pygit2.GIT_SORT_TIME) - ] - - repo_commit = repo_obj[request.start_id] - - for commit in repo_obj.walk( - request.stop_id, pygit2.GIT_SORT_TIME): - if commit.oid.hex in master_commits: - break - diff_commits.append(commit) - diffs.append( - repo_obj.diff( - repo_obj.revparse_single(commit.parents[0].oid.hex), - repo_obj.revparse_single(commit.oid.hex) - ) - ) - - elif orig_repo.is_empty: - orig_commit = None - diff = repo_commit.tree.diff_to_tree(swap=True) - else: - flask.flash( - 'Fork is empty, there are no commits to request pulling', - 'error') - return flask.redirect(flask.url_for( - 'view_repo', username=username, repo=repo.name)) - - html_diffs = [] - for diff in diffs: - html_diffs.append( - highlight( - diff.patch, - DiffLexer(), - HtmlFormatter( - noclasses=True, - style="tango",) - ) - ) - - return flask.render_template( - 'pull_request.html', - select='requests', - requestid=requestid, - repo=repo, - username=username, - request=request, - repo_admin=is_repo_admin(request.repo), - repo_obj=repo_obj, - orig_repo=orig_repo, - diff_commits=diff_commits, - diffs=diffs, - html_diffs=html_diffs, - ) - -@APP.route('//request-pull/.patch') -@APP.route('/fork///request-pull/.patch') -def request_pull_patch(repo, requestid, username=None): - """ Returns the commits from the specified pull-request as patches. - """ - - repo = progit.lib.get_project(SESSION, repo, user=username) - - if not repo: - flask.abort(404, 'Project not found') - - request = progit.lib.get_pull_request( - SESSION, project_id=repo.id, requestid=requestid) - - if not request: - flask.abort(404, 'Pull-request not found') - - repo = request.repo_from - - if repo.is_fork: - repopath = os.path.join(APP.config['FORK_FOLDER'], repo.path) - else: - repopath = os.path.join(APP.config['GIT_FOLDER'], repo.path) - repo_obj = pygit2.Repository(repopath) - - if repo.parent: - parentname = os.path.join(APP.config['GIT_FOLDER'], repo.parent.path) - else: - parentname = os.path.join(APP.config['GIT_FOLDER'], repo.path) - orig_repo = pygit2.Repository(parentname) - - diff_commits = [] - repo_commit = repo_obj[request.stop_id] - if not repo_obj.is_empty and not orig_repo.is_empty: - orig_commit = orig_repo[ - orig_repo.lookup_branch('master').get_object().hex] - - master_commits = [ - commit.oid.hex - for commit in orig_repo.walk( - orig_repo.lookup_branch('master').get_object().hex, - pygit2.GIT_SORT_TIME) - ] - - repo_commit = repo_obj[request.start_id] - - for commit in repo_obj.walk( - request.stop_id, pygit2.GIT_SORT_TIME): - if commit.oid.hex in master_commits: - break - diff_commits.append(commit) - - elif orig_repo.is_empty: - orig_commit = None - diff = repo_commit.tree.diff_to_tree(swap=True) - else: - flask.flash( - 'Fork is empty, there are no commits to request pulling', - 'error') - return flask.redirect(flask.url_for( - 'view_repo', username=username, repo=repo.name)) - - diff_commits.reverse() - patch = progit.lib.commit_to_patch(repo_obj, diff_commits) - - return flask.Response(patch, content_type="text/plain;charset=UTF-8") - - -@APP.route('//request-pull//comment//', - methods=('GET', 'POST')) -@APP.route('/fork///request-pull//comment/' - '/', methods=('GET', 'POST')) -def pull_request_add_comment(repo, requestid, commit, row, username=None): - """ Add a comment to a commit in a pull-request. - """ - repo = progit.lib.get_project(SESSION, repo, user=username) - - if not repo: - flask.abort(404, 'Project not found') - - request = progit.lib.get_pull_request( - SESSION, project_id=repo.id, requestid=requestid) - repo = request.repo_from - - if not request: - flask.abort(404, 'Pull-request not found') - - form = progit.forms.AddPullRequestCommentForm() - form.commit.data = commit - form.requestid.data = requestid - form.row.data = row - - if form.validate_on_submit(): - comment = form.comment.data - - try: - message = progit.lib.add_pull_request_comment( - SESSION, - request=request, - commit=commit, - row=row, - comment=comment, - user=flask.g.fas_user.username, - ) - SESSION.commit() - flask.flash(message) - except SQLAlchemyError, err: # pragma: no cover - SESSION.rollback() - flask.flash(str(err), 'error') - - return flask.redirect(flask.url_for( - 'request_pull', username=username, - repo=repo.name, requestid=requestid)) - - return flask.render_template( - 'pull_request_comment.html', - select='requests', - requestid=requestid, - repo=repo, - username=username, - commit=commit, - row=row, - form=form, - ) - - - -@APP.route('//request-pull/merge/') -@APP.route('/fork///request-pull/merge/') -def merge_request_pull(repo, requestid, username=None): - """ Request pulling the changes from the fork into the project. - """ - repo = progit.lib.get_project(SESSION, repo, user=username) - - if not repo: - flask.abort(404, 'Project not found') - - request = progit.lib.get_pull_request( - SESSION, project_id=repo.id, requestid=requestid) - - if not request: - flask.abort(404, 'Pull-request not found') - - if not is_repo_admin(repo): - flask.abort( - 403, - 'You are not allowed to merge pull-request for this project') - - error_output = flask.url_for( - 'request_pull', repo=repo.name, requestid=requestid) - if username: - error_output = flask.url_for( - 'fork_request_pull', - repo=repo.name, - requestid=requestid, - username=username) - - # Get the fork - if request.repo_from.is_fork: - repopath = os.path.join( - APP.config['FORK_FOLDER'], request.repo_from.path) - else: - repopath = os.path.join( - APP.config['GIT_FOLDER'], request.repo_from.path) - fork_obj = pygit2.Repository(repopath) - - # Get the original repo - parentpath = os.path.join(APP.config['GIT_FOLDER'], request.repo.path) - orig_repo = pygit2.Repository(parentpath) - - # Clone the original repo into a temp folder - newpath = tempfile.mkdtemp() - new_repo = pygit2.clone_repository(parentpath, newpath) - - repo_commit = fork_obj[request.stop_id] - - ori_remote = new_repo.remotes[0] - # Add the fork as remote repo - reponame = '%s_%s' % (request.user.user, repo.name) - remote = new_repo.create_remote(reponame, repopath) - - # Fetch the commits - remote.fetch() - - merge = new_repo.merge(repo_commit.oid) - master_ref = new_repo.lookup_reference('HEAD').resolve() - - refname = '%s:%s' % (master_ref.name, master_ref.name) - if merge.is_uptodate: - flask.flash('Nothing to do, changes were already merged', 'error') - progit.lib.close_pull_request(SESSION, request) - SESSION.commit() - return flask.redirect(error_output) - elif merge.is_fastforward: - master_ref.target = merge.fastforward_oid - ori_remote.push(refname) - flask.flash('Changes merged!') - else: - new_repo.index.write() - try: - tree = new_repo.index.write_tree() - except pygit2.GitError: - shutil.rmtree(newpath) - flask.flash('Merge conflicts!', 'error') - return flask.redirect(flask.url_for( - 'request_pull', - repo=repo.name, - username=username, - requestid=requestid)) - head = new_repo.lookup_reference('HEAD').get_object() - commit = new_repo[head.oid] - sha = new_repo.create_commit( - 'refs/heads/master', - repo_commit.author, - repo_commit.committer, - 'Merge #%s `%s`' % (request.id, request.title), - tree, - [head.hex, repo_commit.oid.hex]) - ori_remote.push(refname) - flask.flash('Changes merged!') - - # Update status - progit.lib.close_pull_request(SESSION, request) - SESSION.commit() - shutil.rmtree(newpath) - - return flask.redirect(flask.url_for('view_repo', repo=repo.name)) - - -## Specific actions - - -@APP.route('/do_fork/') -@APP.route('/do_fork//') -@cla_required -def fork_project(repo, username=None): - """ Fork the project specified into the user's namespace - """ - repo = progit.lib.get_project(SESSION, repo, user=username) - - if repo is None: - flask.abort(404) - - try: - message = progit.lib.fork_project( - session=SESSION, - repo=repo, - gitfolder=APP.config['GIT_FOLDER'], - forkfolder=APP.config['FORK_FOLDER'], - docfolder=APP.config['DOCS_FOLDER'], - ticketfolder=APP.config['TICKETS_FOLDER'], - user=flask.g.fas_user.username) - - SESSION.commit() - generate_gitolite_acls() - flask.flash(message) - return flask.redirect( - flask.url_for( - 'view_repo', - username=flask.g.fas_user.username, - repo=repo.name) - ) - except progit.exceptions.ProgitException, err: - flask.flash(str(err), 'error') - except SQLAlchemyError, err: # pragma: no cover - SESSION.rollback() - flask.flash(str(err), 'error') - - return flask.redirect(flask.url_for('view_repo', repo=repo.name)) - - -@APP.route('//request-pull/new', - methods=('GET', 'POST')) -@APP.route('//request-pull/new/', - methods=('GET', 'POST')) -@APP.route('/fork///request-pull/new', - methods=('GET', 'POST')) -@APP.route('/fork///request-pull/new/', - methods=('GET', 'POST')) -@cla_required -def new_request_pull(repo, username=None, commitid=None): - """ Request pulling the changes from the fork into the project. - """ - repo = progit.lib.get_project(SESSION, repo, user=username) - - if not repo: - flask.abort(404) - - if not is_repo_admin(repo): - flask.abort( - 403, - 'You are not allowed to create pull-requests for this project') - - if repo.is_fork: - repopath = os.path.join(APP.config['FORK_FOLDER'], repo.path) - else: - repopath = os.path.join(APP.config['GIT_FOLDER'], repo.path) - repo_obj = pygit2.Repository(repopath) - - if repo.parent: - parentname = os.path.join(APP.config['GIT_FOLDER'], repo.parent.path) - else: - parentname = os.path.join(APP.config['GIT_FOLDER'], repo.path) - orig_repo = pygit2.Repository(parentname) - - frombranchname = flask.request.args.get('from_branch', 'master') - frombranch = repo_obj.lookup_branch(frombranchname) - if not frombranch: - flask.flash('Branch %s does not exist' % frombranchname, 'error') - frombranchname = 'master' - - branchname = flask.request.args.get('branch', 'master') - branch = orig_repo.lookup_branch(branchname) - if not branch: - flask.flash('Branch %s does not exist' % branchname, 'error') - branchname = 'master' - - if commitid is None: - commitid = repo_obj.head.target - if branchname: - branch = repo_obj.lookup_branch(frombranchname) - commitid = branch.get_object().hex - - diff_commits = [] - diffs = [] - if not repo_obj.is_empty and not orig_repo.is_empty: - orig_commit = orig_repo[ - orig_repo.lookup_branch(branchname).get_object().hex] - - master_commits = [ - commit.oid.hex - for commit in orig_repo.walk( - orig_repo.lookup_branch(branchname).get_object().hex, - pygit2.GIT_SORT_TIME) - ] - - repo_commit = repo_obj[commitid] - - for commit in repo_obj.walk( - repo_commit.oid.hex, pygit2.GIT_SORT_TIME): - if commit.oid.hex in master_commits: - break - diff_commits.append(commit) - diffs.append( - repo_obj.diff( - repo_obj.revparse_single(commit.parents[0].oid.hex), - repo_obj.revparse_single(commit.oid.hex) - ) - ) - - elif orig_repo.is_empty: - orig_commit = None - repo_commit = repo_obj[repo_obj.head.target] - diff = repo_commit.tree.diff_to_tree(swap=True) - else: - flask.flash( - 'Fork is empty, there are no commits to request pulling', - 'error') - return flask.redirect(flask.url_for( - 'view_repo', username=username, repo=repo.name)) - - html_diffs = [] - for diff in diffs: - html_diffs.append( - highlight( - diff.patch, - DiffLexer(), - HtmlFormatter( - noclasses=True, - style="tango",) - ) - ) - - form = progit.forms.RequestPullForm() - if form.validate_on_submit(): - try: - if orig_commit: - orig_commit = orig_commit.oid.hex - - parent = repo - if repo.parent: - parent = repo.parent - - message = progit.lib.new_pull_request( - SESSION, - repo=parent, - repo_from=repo, - branch=branchname, - title=form.title.data, - start_id=orig_commit, - stop_id=repo_commit.oid.hex, - user=flask.g.fas_user.username, - ) - SESSION.commit() - flask.flash(message) - - if not parent.is_fork: - url = flask.url_for( - 'request_pulls', username=None, repo=parent.name) - else: - url = flask.url_for( - 'request_pulls', username=parent.user, repo=parent.name) - - return flask.redirect(url) - except progit.exceptions.ProgitException, err: - flask.flash(str(err), 'error') - except SQLAlchemyError, err: # pragma: no cover - SESSION.rollback() - flask.flash(str(err), 'error') - - return flask.render_template( - 'pull_request.html', - select='requests', - repo=repo, - username=username, - commitid=commitid, - repo_obj=repo_obj, - orig_repo=orig_repo, - diff_commits=diff_commits, - diffs=diffs, - html_diffs=html_diffs, - form=form, - branches=[ - branch.replace('refs/heads/', '') - for branch in sorted(orig_repo.listall_references()) - ], - branchname=branchname, - ) diff --git a/progit/issues.py b/progit/issues.py deleted file mode 100644 index 72a6946..0000000 --- a/progit/issues.py +++ /dev/null @@ -1,272 +0,0 @@ -#-*- coding: utf-8 -*- - -""" - (c) 2014 - Copyright Red Hat Inc - - Authors: - Pierre-Yves Chibon - -""" - -import flask -import os -from math import ceil - -import pygit2 -from sqlalchemy.exc import SQLAlchemyError -from pygments import highlight -from pygments.lexers import guess_lexer -from pygments.lexers.text import DiffLexer -from pygments.formatters import HtmlFormatter - - -import progit.doc_utils -import progit.lib -import progit.forms -from progit import (APP, SESSION, LOG, __get_file_in_tree, cla_required, - is_repo_admin, authenticated) - - -## URLs - -@APP.route('//issue//add', methods=('GET', 'POST')) -@APP.route('/fork///issue//add', - methods=('GET', 'POST')) -def add_comment_issue(repo, issueid, username=None): - ''' Add a comment to an issue. ''' - repo = progit.lib.get_project(SESSION, repo, user=username) - - if repo is None: - flask.abort(404, 'Project not found') - - if not repo.issue_tracker: - flask.abort(404, 'No issue tracker found for this project') - - issue = progit.lib.get_issue(SESSION, repo.id, issueid) - - if issue is None or issue.project != repo: - flask.abort(404, 'Issue not found') - - form = progit.forms.AddIssueCommentForm() - if form.validate_on_submit(): - comment = form.comment.data - - try: - message = progit.lib.add_issue_comment( - SESSION, - issue=issue, - comment=comment, - user=flask.g.fas_user.username, - ticketfolder=APP.config['TICKETS_FOLDER'], - ) - SESSION.commit() - flask.flash(message) - except SQLAlchemyError, err: # pragma: no cover - SESSION.rollback() - flask.flash(str(err), 'error') - - return flask.redirect(flask.url_for( - 'view_issue', username=username, repo=repo.name, issueid=issueid)) - - -@APP.route('//issues') -@APP.route('/fork///issues') -def view_issues(repo, username=None): - """ List all issues associated to a repo - """ - status = flask.request.args.get('status', None) - - repo = progit.lib.get_project(SESSION, repo, user=username) - - if repo is None: - flask.abort(404, 'Project not found') - - if not repo.issue_tracker: - flask.abort(404, 'No issue tracker found for this project') - - if status is not None: - if status.lower() == 'closed': - issues = progit.lib.get_issues(SESSION, repo, closed=True) - else: - issues = progit.lib.get_issues(SESSION, repo, status=status) - else: - issues = progit.lib.get_issues(SESSION, repo, status='Open') - - return flask.render_template( - 'issues.html', - select='issues', - repo=repo, - username=username, - status=status, - issues=issues, - ) - - -@APP.route('//new_issue', methods=('GET', 'POST')) -@APP.route('/fork///new_issue', methods=('GET', 'POST')) -@cla_required -def new_issue(repo, username=None): - """ Create a new issue - """ - repo = progit.lib.get_project(SESSION, repo, user=username) - - if repo is None: - flask.abort(404, 'Project not found') - - status = progit.lib.get_issue_statuses(SESSION) - form = progit.forms.IssueForm(status=status) - if form.validate_on_submit(): - title = form.title.data - content = form.issue_content.data - - try: - message = progit.lib.new_issue( - SESSION, - repo=repo, - title=title, - content=content, - user=flask.g.fas_user.username, - ticketfolder=APP.config['TICKETS_FOLDER'], - ) - SESSION.commit() - flask.flash(message) - return flask.redirect(flask.url_for( - 'view_issues', username=username, repo=repo.name)) - except progit.exceptions.ProgitException, err: - flask.flash(str(err), 'error') - except SQLAlchemyError, err: # pragma: no cover - SESSION.rollback() - flask.flash(str(err), 'error') - - return flask.render_template( - 'new_issue.html', - select='issues', - form=form, - repo=repo, - username=username, - ) - - -@APP.route('//issue/', methods=('GET', 'POST')) -@APP.route('/fork///issue/', - methods=('GET', 'POST')) -def view_issue(repo, issueid, username=None): - """ List all issues associated to a repo - """ - - repo = progit.lib.get_project(SESSION, repo, user=username) - - if repo is None: - flask.abort(404, 'Project not found') - - if not repo.issue_tracker: - flask.abort(404, 'No issue tracker found for this project') - - issue = progit.lib.get_issue(SESSION, repo.id, issueid) - - if issue is None or issue.project != repo: - flask.abort(404, 'Issue not found') - - status = progit.lib.get_issue_statuses(SESSION) - - form_comment = progit.forms.AddIssueCommentForm() - form = None - if authenticated() and is_repo_admin(repo): - form = progit.forms.UpdateIssueStatusForm(status=status) - - if form.validate_on_submit(): - try: - message = progit.lib.edit_issue( - SESSION, - issue=issue, - status=form.status.data, - ticketfolder=APP.config['TICKETS_FOLDER'], - ) - SESSION.commit() - flask.flash(message) - url = flask.url_for( - 'view_issues', username=username, repo=repo.name) - return flask.redirect(url) - except SQLAlchemyError, err: # pragma: no cover - SESSION.rollback() - flask.flash(str(err), 'error') - elif flask.request.method == 'GET': - form.status.data = issue.status - - return flask.render_template( - 'issue.html', - select='issues', - repo=repo, - username=username, - issue=issue, - issueid=issueid, - form=form, - form_comment=form_comment, - ) - - -@APP.route('//issue//edit', methods=('GET', 'POST')) -@APP.route('/fork///issue//edit', - methods=('GET', 'POST')) -@cla_required -def edit_issue(repo, issueid, username=None): - """ Edit the specified issue - """ - repo = progit.lib.get_project(SESSION, repo, user=username) - - if repo is None: - flask.abort(404, 'Project not found') - - if not repo.issue_tracker: - flask.abort(404, 'No issue tracker found for this project') - - if not is_repo_admin(repo): - flask.abort( - 403, 'You are not allowed to edit issues for this project') - - issue = progit.lib.get_issue(SESSION, repo.id, issueid) - - if issue is None or issue.project != repo: - flask.abort(404, 'Issue not found') - - status = progit.lib.get_issue_statuses(SESSION) - form = progit.forms.IssueForm(status=status) - if form.validate_on_submit(): - title = form.title.data - content = form.issue_content.data - status = form.status.data - - try: - message = progit.lib.edit_issue( - SESSION, - issue=issue, - title=title, - content=content, - status=status, - ticketfolder=APP.config['TICKETS_FOLDER'], - ) - SESSION.commit() - flask.flash(message) - url = flask.url_for( - 'view_issue', username=username, - repo=repo.name, issueid=issueid) - return flask.redirect(url) - except progit.exceptions.ProgitException, err: - flask.flash(str(err), 'error') - except SQLAlchemyError, err: # pragma: no cover - SESSION.rollback() - flask.flash(str(err), 'error') - elif flask.request.method == 'GET': - form.title.data = issue.title - form.issue_content.data = issue.content - form.status.data = issue.status - - return flask.render_template( - 'new_issue.html', - select='issues', - type='edit', - form=form, - repo=repo, - username=username, - issue=issue, - ) diff --git a/progit/plugins.py b/progit/plugins.py deleted file mode 100644 index 9770c63..0000000 --- a/progit/plugins.py +++ /dev/null @@ -1,142 +0,0 @@ -#-*- coding: utf-8 -*- - -""" - (c) 2014 - Copyright Red Hat Inc - - Authors: - Pierre-Yves Chibon - -""" - -import flask -import os -import sys -from math import ceil - -import pygit2 -from sqlalchemy.exc import SQLAlchemyError -from straight.plugin import load -from hooks import BaseHook - -import progit.exceptions -import progit.lib -import progit.forms -from progit import APP, SESSION, LOG, cla_required, is_repo_admin -from progit.model import BASE - - -def get_plugin_names(): - ''' Return the list of plugins names. ''' - plugins = load('progit.hooks', subclasses=BaseHook) - output = [plugin.name for plugin in plugins] - return output - - -def get_plugin_tables(): - ''' Return the list of all plugins. ''' - plugins = load('progit.hooks', subclasses=BASE) - return plugins - - -def get_plugin(plugin_name): - ''' Return the list of plugins names. ''' - plugins = load('progit.hooks', subclasses=BaseHook) - for plugin in plugins: - if plugin.name == plugin_name: - return plugin - - -@APP.route('//settings/', methods=('GET', 'POST')) -@APP.route('/fork///settings/', - methods=('GET', 'POST')) -@cla_required -def view_plugin(repo, plugin, username=None): - """ Presents the settings of the project. - """ - return view_plugin_page(repo, plugin, username=username, full=True) - - -@APP.route( - '//settings//', methods=('GET', 'POST')) -@APP.route( - '/fork///settings//', - methods=('GET', 'POST')) -@cla_required -def view_plugin_page(repo, plugin, full, username=None): - """ Presents the settings of the project. - """ - repo = progit.lib.get_project(SESSION, repo, user=username) - - if not repo: - flask.abort(404, 'Project not found') - - if not is_repo_admin(repo): - flask.abort( - 403, - 'You are not allowed to change the settings for this project') - - plugin = get_plugin(plugin) - fields = [] - new = True - dbobj = plugin.db_object() - if hasattr(repo, plugin.backref): - dbobj = getattr(repo, plugin.backref) - # There should always be only one, but let's double check - if dbobj and len(dbobj) > 0: - dbobj = dbobj[0] - new = False - else: - dbobj = plugin.db_object() - - form = plugin.form(obj=dbobj) - for field in plugin.form_fields: - fields.append(getattr(form, field)) - - if form.validate_on_submit(): - form.populate_obj(obj=dbobj) - if new: - dbobj.project_id = repo.id - SESSION.add(dbobj) - try: - SESSION.flush() - except SQLAlchemyError, err: - SESSION.rollback() - APP.logger.debug('Could not add plugin %s', plugin.name) - APP.logger.exception(err) - flask.flash( - 'Could not add plugin %s, please contact an admin' - % plugin.name) - - return flask.render_template( - 'plugin.html', - select='settings', - full=full, - repo=repo, - username=username, - plugin=plugin, - form=form, - fields=fields, - ) - - if form.active.data: - # Set up the main script if necessary - plugin.set_up(repo) - # Install the plugin itself - plugin.install(repo, dbobj) - flask.flash('Hook activated') - else: - plugin.remove(repo) - flask.flash('Hook inactived') - - SESSION.commit() - - return flask.render_template( - 'plugin.html', - select='settings', - full=full, - repo=repo, - username=username, - plugin=plugin, - form=form, - fields=fields, - ) diff --git a/progit/repo.py b/progit/repo.py deleted file mode 100644 index 99c200d..0000000 --- a/progit/repo.py +++ /dev/null @@ -1,692 +0,0 @@ -#-*- coding: utf-8 -*- - -""" - (c) 2014 - Copyright Red Hat Inc - - Authors: - Pierre-Yves Chibon - -""" - -import flask -import shutil -import os -from math import ceil - -import pygit2 -from sqlalchemy.exc import SQLAlchemyError -from pygments import highlight -from pygments.lexers import guess_lexer -from pygments.lexers.text import DiffLexer -from pygments.formatters import HtmlFormatter - -import progit.exceptions -import progit.lib -import progit.forms -import progit.plugins -from progit import (APP, SESSION, LOG, __get_file_in_tree, cla_required, - is_repo_admin) - - -@APP.route('/') -@APP.route('/fork//') -def view_repo(repo, username=None): - """ Front page of a specific repo. - """ - repo = progit.lib.get_project(SESSION, repo, user=username) - - if repo is None: - flask.abort(404, 'Project not found') - - reponame = os.path.join(APP.config['GIT_FOLDER'], repo.path) - if repo.is_fork: - reponame = os.path.join(APP.config['FORK_FOLDER'], repo.path) - repo_obj = pygit2.Repository(reponame) - - cnt = 0 - last_commits = [] - tree = [] - if not repo_obj.is_empty: - for commit in repo_obj.walk( - repo_obj.head.target, pygit2.GIT_SORT_TIME): - last_commits.append(commit) - cnt += 1 - if cnt == 10: - break - tree = sorted(last_commits[0].tree, key=lambda x: x.filemode) - - readme = None - for i in tree: - name, ext = os.path.splitext(i.name) - if name == 'README': - content = repo_obj[i.oid].data - readme = progit.doc_utils.convert_readme(content, ext) - - diff_commits = [] - if repo.is_fork: - parentname = os.path.join( - APP.config['GIT_FOLDER'], repo.parent.path) - if repo.parent.is_fork: - parentname = os.path.join( - APP.config['FORK_FOLDER'], repo.parent.path) - elif repo.parent: - parentname = os.path.join( - APP.config['GIT_FOLDER'], repo.parent.path) - else: - parentname = os.path.join(APP.config['GIT_FOLDER'], repo.path) - - orig_repo = pygit2.Repository(parentname) - - if not repo_obj.is_empty and not orig_repo.is_empty: - orig_commit = orig_repo[ - orig_repo.lookup_branch('master').get_object().hex] - - master_commits = [ - commit.oid.hex - for commit in orig_repo.walk( - orig_repo.lookup_branch('master').get_object().hex, - pygit2.GIT_SORT_TIME) - ] - - repo_commit = repo_obj[ - repo_obj.lookup_branch('master').get_object().hex] - - for commit in repo_obj.walk( - repo_commit.oid.hex, pygit2.GIT_SORT_TIME): - if commit.oid.hex in master_commits: - break - diff_commits.append(commit.oid.hex) - - return flask.render_template( - 'repo_info.html', - select='overview', - repo=repo, - repo_obj=repo_obj, - username=username, - readme=readme, - branches=sorted(repo_obj.listall_branches()), - branchname='master', - last_commits=last_commits, - tree=tree, - diff_commits=diff_commits, - ) - - -@APP.route('//branch/') -@APP.route('/fork///branch/') -def view_repo_branch(repo, branchname, username=None): - repo = progit.lib.get_project(SESSION, repo, user=username) - - if not repo: - flask.abort(404, 'Project not found') - - reponame = os.path.join(APP.config['GIT_FOLDER'], repo.path) - if repo.is_fork: - reponame = os.path.join(APP.config['FORK_FOLDER'], repo.path) - repo_obj = pygit2.Repository(reponame) - - if branchname not in repo_obj.listall_branches(): - flask.abort(404, 'Branch no found') - - branch = repo_obj.lookup_branch(branchname) - - cnt = 0 - last_commits = [] - for commit in repo_obj.walk(branch.get_object().hex, pygit2.GIT_SORT_TIME): - last_commits.append(commit) - cnt += 1 - if cnt == 10: - break - - diff_commits = [] - if repo.is_fork: - parentname = os.path.join( - APP.config['GIT_FOLDER'], repo.parent.path) - if repo.parent.is_fork: - parentname = os.path.join( - APP.config['FORK_FOLDER'], repo.parent.path) - elif repo.parent: - parentname = os.path.join( - APP.config['GIT_FOLDER'], repo.parent.path) - else: - parentname = os.path.join(APP.config['GIT_FOLDER'], repo.path) - - orig_repo = pygit2.Repository(parentname) - - if not repo_obj.is_empty and not orig_repo.is_empty: - orig_commit = orig_repo[ - orig_repo.lookup_branch('master').get_object().hex] - - master_commits = [ - commit.oid.hex - for commit in orig_repo.walk( - orig_repo.lookup_branch('master').get_object().hex, - pygit2.GIT_SORT_TIME) - ] - - repo_commit = repo_obj[branch.get_object().hex] - - for commit in repo_obj.walk( - repo_commit.oid.hex, pygit2.GIT_SORT_TIME): - if commit.oid.hex in master_commits: - break - diff_commits.append(commit.oid.hex) - - return flask.render_template( - 'repo_info.html', - select='overview', - repo=repo, - username=username, - branches=sorted(repo_obj.listall_branches()), - branchname=branchname, - last_commits=last_commits, - tree=sorted(last_commits[0].tree, key=lambda x: x.filemode), - diff_commits=diff_commits, - ) - - -@APP.route('//log') -@APP.route('//log/') -@APP.route('/fork///log') -@APP.route('/fork///log/') -def view_log(repo, branchname=None, username=None): - """ Displays the logs of the specified repo. - """ - repo = progit.lib.get_project(SESSION, repo, user=username) - - if not repo: - flask.abort(404, 'Project not found') - - reponame = os.path.join(APP.config['GIT_FOLDER'], repo.path) - if repo.is_fork: - reponame = os.path.join(APP.config['FORK_FOLDER'], repo.path) - repo_obj = pygit2.Repository(reponame) - - if branchname and branchname not in repo_obj.listall_branches(): - flask.abort(404, 'Branch no found') - - if branchname: - branch = repo_obj.lookup_branch(branchname) - else: - branch = repo_obj.lookup_branch('master') - - try: - page = int(flask.request.args.get('page', 1)) - except ValueError: - page = 1 - - limit = APP.config['ITEM_PER_PAGE'] - start = limit * (page - 1) - end = limit * page - - n_commits = 0 - last_commits = [] - if branch: - for commit in repo_obj.walk( - branch.get_object().hex, pygit2.GIT_SORT_TIME): - if n_commits >= start and n_commits <= end: - last_commits.append(commit) - n_commits += 1 - - total_page = int(ceil(n_commits / float(limit))) - - diff_commits = [] - if repo.is_fork: - parentname = os.path.join( - APP.config['GIT_FOLDER'], repo.parent.path) - if repo.parent.is_fork: - parentname = os.path.join( - APP.config['FORK_FOLDER'], repo.parent.path) - elif repo.parent: - parentname = os.path.join( - APP.config['GIT_FOLDER'], repo.parent.path) - else: - parentname = os.path.join(APP.config['GIT_FOLDER'], repo.path) - - orig_repo = pygit2.Repository(parentname) - - if not repo_obj.is_empty and not orig_repo.is_empty: - orig_commit = orig_repo[ - orig_repo.lookup_branch('master').get_object().hex] - - master_commits = [ - commit.oid.hex - for commit in orig_repo.walk( - orig_repo.lookup_branch('master').get_object().hex, - pygit2.GIT_SORT_TIME) - ] - - repo_commit = repo_obj[branch.get_object().hex] - - for commit in repo_obj.walk( - repo_commit.oid.hex, pygit2.GIT_SORT_TIME): - if commit.oid.hex in master_commits: - break - diff_commits.append(commit.oid.hex) - - origin = 'view_log' - - return flask.render_template( - 'repo_info.html', - select='logs', - origin=origin, - repo_obj=repo_obj, - repo=repo, - username=username, - branches=sorted(repo_obj.listall_branches()), - branchname=branchname, - last_commits=last_commits, - diff_commits=diff_commits, - page=page, - total_page=total_page, - ) - - -@APP.route('//blob//') -@APP.route('//blob//') -@APP.route('/fork///blob//') -@APP.route('/fork///blob//') -def view_file(repo, identifier, filename, username=None): - """ Displays the content of a file or a tree for the specified repo. - """ - repo = progit.lib.get_project(SESSION, repo, user=username) - - if not repo: - flask.abort(404, 'Project not found') - - reponame = os.path.join(APP.config['GIT_FOLDER'], repo.path) - if repo.is_fork: - reponame = os.path.join(APP.config['FORK_FOLDER'], repo.path) - repo_obj = pygit2.Repository(reponame) - - if identifier in repo_obj.listall_branches(): - branchname = identifier - branch = repo_obj.lookup_branch(identifier) - commit = branch.get_object() - else: - try: - commit = repo_obj.get(identifier) - branchname = identifier - except ValueError: - # If it's not a commit id then it's part of the filename - commit = repo_obj[repo_obj.head.target] - branchname = 'master' - - content = __get_file_in_tree(repo_obj, commit.tree, filename.split('/')) - if not content: - flask.abort(404, 'File not found') - - content = repo_obj[content.oid] - if isinstance(content, pygit2.Blob): - content = highlight( - content.data, - guess_lexer(content.data), - HtmlFormatter( - noclasses=True, - style="tango",) - ) - output_type = 'file' - else: - content = sorted(content, key=lambda x: x.filemode) - output_type = 'tree' - - return flask.render_template( - 'file.html', - select='tree', - repo=repo, - username=username, - branchname=branchname, - filename=filename, - content=content, - output_type=output_type, - ) - - -@APP.route('//') -@APP.route('/fork///') -def view_commit(repo, commitid, username=None): - """ Render a commit in a repo - """ - repo = progit.lib.get_project(SESSION, repo, user=username) - - if not repo: - flask.abort(404, 'Project not found') - - reponame = os.path.join(APP.config['GIT_FOLDER'], repo.path) - if repo.is_fork: - reponame = os.path.join(APP.config['FORK_FOLDER'], repo.path) - repo_obj = pygit2.Repository(reponame) - - try: - commit = repo_obj.get(commitid) - except ValueError: - flask.abort(404, 'Commit not found') - - if commit.parents: - diff = commit.tree.diff_to_tree() - - parent = repo_obj.revparse_single('%s^' % commitid) - diff = repo_obj.diff(parent, commit) - else: - # First commit in the repo - diff = commit.tree.diff_to_tree(swap=True) - - html_diff = highlight( - diff.patch, - DiffLexer(), - HtmlFormatter( - noclasses=True, - style="tango",) - ) - - return flask.render_template( - 'commit.html', - select='logs', - repo=repo, - username=username, - commitid=commitid, - commit=commit, - diff=diff, - html_diff=html_diff, - ) - - -@APP.route('//.patch') -@APP.route('/fork///.patch') -def view_commit_patch(repo, commitid, username=None): - """ Render a commit in a repo as patch - """ - repo = progit.lib.get_project(SESSION, repo, user=username) - - if not repo: - flask.abort(404, 'Project not found') - - reponame = os.path.join(APP.config['GIT_FOLDER'], repo.path) - if repo.is_fork: - reponame = os.path.join(APP.config['FORK_FOLDER'], repo.path) - repo_obj = pygit2.Repository(reponame) - - try: - commit = repo_obj.get(commitid) - except ValueError: - flask.abort(404, 'Commit not found') - - patch = progit.lib.commit_to_patch(repo_obj, commit) - - return flask.Response(patch, content_type="text/plain;charset=UTF-8") - - -@APP.route('//tree/') -@APP.route('//tree/') -@APP.route('/fork///tree/') -@APP.route('/fork///tree/') -def view_tree(repo, identifier=None, username=None): - """ Render the tree of the repo - """ - repo = progit.lib.get_project(SESSION, repo, user=username) - - if repo is None: - flask.abort(404, 'Project not found') - - reponame = os.path.join(APP.config['GIT_FOLDER'], repo.path) - if repo.is_fork: - reponame = os.path.join(APP.config['FORK_FOLDER'], repo.path) - repo_obj = pygit2.Repository(reponame) - - branchname = None - content = None - output_type = None - if not repo_obj.is_empty: - if identifier in repo_obj.listall_branches(): - branchname = identifier - branch = repo_obj.lookup_branch(identifier) - commit = branch.get_object() - else: - try: - commit = repo_obj.get(identifier) - branchname = identifier - except (ValueError, TypeError): - # If it's not a commit id then it's part of the filename - commit = repo_obj[repo_obj.head.target] - branchname = 'master' - - content = sorted(commit.tree, key=lambda x: x.filemode) - output_type = 'tree' - - return flask.render_template( - 'file.html', - select='tree', - repo_obj=repo_obj, - repo=repo, - username=username, - branchname=branchname, - filename='', - content=content, - output_type=output_type, - ) - - -@APP.route('//forks') -@APP.route('/fork///forks') -def view_forks(repo, username=None): - """ Presents all the forks of the project. - """ - repo = progit.lib.get_project(SESSION, repo, user=username) - - if not repo: - flask.abort(404, 'Project not found') - - return flask.render_template( - 'forks.html', - select='forks', - username=username, - repo=repo, - ) - - -@APP.route('//settings', methods=('GET', 'POST')) -@APP.route('/fork///settings', methods=('GET', 'POST')) -@cla_required -def view_settings(repo, username=None): - """ Presents the settings of the project. - """ - repo = progit.lib.get_project(SESSION, repo, user=username) - - if not repo: - flask.abort(404, 'Project not found') - - if not is_repo_admin(repo): - flask.abort( - 403, - 'You are not allowed to change the settings for this project') - - plugins = progit.plugins.get_plugin_names() - - form = progit.forms.ProjectSettingsForm() - - if form.validate_on_submit(): - issue_tracker = form.issue_tracker.data - project_docs = form.project_docs.data - - try: - message = progit.lib.update_project_settings( - SESSION, - repo=repo, - issue_tracker=issue_tracker, - project_docs=project_docs, - ) - SESSION.commit() - flask.flash(message) - return flask.redirect(flask.url_for( - 'view_repo', username=username, repo=repo.name)) - except progit.exceptions.ProgitException, err: - flask.flash(str(err), 'error') - except SQLAlchemyError, err: # pragma: no cover - SESSION.rollback() - flask.flash(str(err), 'error') - elif flask.request.method == 'GET': - form = progit.forms.ProjectSettingsForm(project=repo) - - return flask.render_template( - 'settings.html', - select='settings', - username=username, - repo=repo, - form=form, - plugins=plugins, - ) - - -@APP.route('//updatedesc', methods=['POST']) -@APP.route('/fork///updatedesc', methods=['POST']) -@cla_required -def update_description(repo, username=None): - """ Update the description of a project. - """ - repo = progit.lib.get_project(SESSION, repo, user=username) - - if not repo: - flask.abort(404, 'Project not found') - - if not is_repo_admin(repo): - flask.abort( - 403, - 'You are not allowed to change the settings for this project') - - form = progit.forms.DescriptionForm() - - if form.validate_on_submit(): - try: - repo.description = form.description.data - SESSION.add(repo) - SESSION.commit() - flask.flash('Description updated') - except SQLAlchemyError, err: # pragma: no cover - SESSION.rollback() - flask.flash(str(err), 'error') - - return flask.redirect(flask.url_for( - 'view_settings', username=username, repo=repo.name)) - - -@APP.route('//delete', methods=['POST']) -@APP.route('/fork///delete', methods=['POST']) -@cla_required -def delete_repo(repo, username=None): - """ Delete the present project. - """ - repo = progit.lib.get_project(SESSION, repo, user=username) - - if not repo: - flask.abort(404, 'Project not found') - - if not is_repo_admin(repo): - flask.abort( - 403, - 'You are not allowed to change the settings for this project') - - for issue in repo.issues: - for comment in issue.comments: - SESSION.delete(comment) - SESSION.delete(issue) - for request in repo.requests: - SESSION.delete(request) - SESSION.delete(repo) - - repopath = os.path.join(APP.config['GIT_FOLDER'], repo.path) - if repo.is_fork: - repopath = os.path.join(APP.config['FORK_FOLDER'], repo.path) - docpath = os.path.join(APP.config['DOCS_FOLDER'], repo.path) - - try: - shutil.rmtree(repopath) - shutil.rmtree(docpath) - SESSION.commit() - except (OSError, IOError), err: - APP.logger.exception(err) - flask.flash('Could not delete the project from the system', 'error') - except SQLAlchemyError, err: # pragma: no cover - SESSION.rollback() - APP.logger.exception(err) - flask.flash('Could not delete the project', 'error') - - return flask.redirect( - flask.url_for('view_user', username=flask.g.fas_user.username)) - - -@APP.route('//dropuser/', methods=['POST']) -@APP.route('/fork///dropuser/', methods=['POST']) -@cla_required -def remove_user(repo, userid, username=None): - """ Remove the specified user from the project. - """ - repo = progit.lib.get_project(SESSION, repo, user=username) - - if not repo: - flask.abort(404, 'Project not found') - - if not is_repo_admin(repo): - flask.abort( - 403, - 'You are not allowed to change the users for this project') - - userids = [str(user.user.id) for user in repo.users] - - if str(userid) not in userids: - flask.flash( - 'User does not have commit or cannot loose it right', 'error') - return flask.redirect( - flask.url_for( - '.view_settings', repo=repo.name, username=username) - ) - - for user in repo.users: - if str(user.user.id) == str(userid): - SESSION.delete(user) - break - SESSION.commit() - - progit.generate_gitolite_acls() - - flask.flash('User removed') - return flask.redirect( - flask.url_for('.view_settings', repo=repo.name, username=username) - ) - - -@APP.route('//adduser', methods=('GET', 'POST')) -@APP.route('/fork///adduser', methods=('GET', 'POST')) -@cla_required -def add_user(repo, username=None): - """ Add the specified user from the project. - """ - repo = progit.lib.get_project(SESSION, repo, user=username) - - if not repo: - flask.abort(404, 'Project not found') - - if not is_repo_admin(repo): - flask.abort( - 403, - 'You are not allowed to add users to this project') - - form = progit.forms.AddUserForm() - - if form.validate_on_submit(): - msg = progit.lib.add_user_to_project(SESSION, repo, form.user.data) - - SESSION.commit() - - progit.generate_gitolite_acls() - - flask.flash(msg) - return flask.redirect( - flask.url_for('.view_settings', repo=repo.name, username=username) - ) - - return flask.render_template( - 'add_user.html', - form=form, - username=username, - repo=repo, - ) diff --git a/progit/ui/admin.py b/progit/ui/admin.py new file mode 100644 index 0000000..35093a3 --- /dev/null +++ b/progit/ui/admin.py @@ -0,0 +1,65 @@ +#-*- coding: utf-8 -*- + +""" + (c) 2014 - Copyright Red Hat Inc + + Authors: + Pierre-Yves Chibon + +""" + +from functools import wraps + +import flask + +from progit import (APP, SESSION, LOG, cla_required, authenticated, + generate_gitolite_acls, generate_authorized_key_file, + is_admin) + + +def admin_required(function): + """ Flask decorator to retrict access to admins of progit. + """ + @wraps(function) + def decorated_function(*args, **kwargs): + """ Decorated function, actually does the work. """ + if not authenticated(): + return flask.redirect( + flask.url_for('auth_login', next=flask.request.url)) + elif not is_admin(): + flask.flash('Access restricted', 'error') + return flask.redirect(flask.url_for('.index')) + return function(*args, **kwargs) + return decorated_function + + +### Application + + +@APP.route('/admin') +@admin_required +def admin_index(): + """ Front page of the admin section of the application. + """ + + return flask.render_template( + 'admin_index.html', + ) + + +@APP.route('/admin/gitolite') +@admin_required +def admin_generate_acl(): + """ Regenerate the gitolite ACL file. """ + generate_gitolite_acls() + flask.flash('Gitolite ACLs updated') + return flask.redirect(flask.url_for('admin_index')) + + +@APP.route('/admin/ssh') +@admin_required +def admin_refresh_ssh(): + """ Regenerate the gitolite ACL file. """ + generate_authorized_key_file() + flask.flash('Authorized file updated') + return flask.redirect(flask.url_for('admin_index')) diff --git a/progit/ui/app.py b/progit/ui/app.py new file mode 100644 index 0000000..5f1b05e --- /dev/null +++ b/progit/ui/app.py @@ -0,0 +1,245 @@ +#-*- coding: utf-8 -*- + +""" + (c) 2014 - Copyright Red Hat Inc + + Authors: + Pierre-Yves Chibon + +""" + +import flask +import os +from math import ceil + +import pygit2 +from sqlalchemy.exc import SQLAlchemyError +from pygments import highlight +from pygments.lexers import guess_lexer +from pygments.lexers.text import DiffLexer +from pygments.formatters import HtmlFormatter + + +import progit.exceptions +import progit.lib +import progit.forms +from progit import (APP, SESSION, LOG, __get_file_in_tree, cla_required, + generate_gitolite_acls, generate_gitolite_key, + generate_authorized_key_file) + + +def chunks(item_list, chunks_size): + """ Yield successive n-sized chunks from item_list. + """ + for i in xrange(0, len(item_list), chunks_size): + yield item_list[i: i + chunks_size] + + +### Application + + +@APP.route('/') +def index(): + """ Front page of the application. + """ + page = flask.request.args.get('page', 1) + try: + page = int(page) + except ValueError: + page = 1 + + limit = APP.config['ITEM_PER_PAGE'] + start = limit * (page - 1) + + repos = progit.lib.list_projects( + SESSION, + fork=False, + start=start, + limit=limit) + num_repos = progit.lib.list_projects( + SESSION, + fork=False, + count=True) + + total_page = int(ceil(num_repos / float(limit))) + + return flask.render_template( + 'index.html', + repos=chunks(repos, 3), + total_page=total_page, + page=page, + ) + + +@APP.route('/users/') +def view_users(): + """ Present the list of users. + """ + page = flask.request.args.get('page', 1) + try: + page = int(page) + except ValueError: + page = 1 + + users = progit.lib.get_all_users(SESSION) + + limit = APP.config['ITEM_PER_PAGE'] + start = limit * (page - 1) + end = limit * page + users_length = len(users) + users = users[start:end] + + total_page = int(ceil(users_length / float(limit))) + + return flask.render_template( + 'user_list.html', + users=users, + users_length=users_length, + total_page=total_page, + page=page, + ) + + +@APP.route('/user/') +def view_user(username): + """ Front page of a specific user. + """ + + repopage = flask.request.args.get('repopage', 1) + try: + repopage = int(repopage) + except ValueError: + repopage = 1 + + forkpage = flask.request.args.get('forkpage', 1) + try: + forkpage = int(forkpage) + except ValueError: + forkpage = 1 + + limit = APP.config['ITEM_PER_PAGE'] + repo_start = limit * (repopage - 1) + fork_start = limit * (forkpage - 1) + + repos = progit.lib.list_projects( + SESSION, + username=username, + fork=False, + start=repo_start, + limit=limit) + repos_length = progit.lib.list_projects( + SESSION, + username=username, + fork=False, + count=True) + + forks = progit.lib.list_projects( + SESSION, + username=username, + fork=True, + start=fork_start, + limit=limit) + forks_length = progit.lib.list_projects( + SESSION, + username=username, + fork=True, + count=True) + + total_page_repos = int(ceil(repos_length / float(limit))) + total_page_forks = int(ceil(forks_length / float(limit))) + + repos_obj = [ + pygit2.Repository( + os.path.join(APP.config['GIT_FOLDER'], repo.path)) + for repo in repos] + + forks_obj = [ + pygit2.Repository( + os.path.join(APP.config['FORK_FOLDER'], repo.path)) + for repo in forks] + + return flask.render_template( + 'user_info.html', + username=username, + repos=repos, + repos_obj=repos_obj, + total_page_repos=total_page_repos, + forks=forks, + forks_obj=forks_obj, + total_page_forks=total_page_forks, + repopage=repopage, + forkpage=forkpage, + ) + + +@APP.route('/new/', methods=('GET', 'POST')) +@cla_required +def new_project(): + """ Form to create a new project. + """ + form = progit.forms.ProjectForm() + if form.validate_on_submit(): + name = form.name.data + description = form.description.data + + try: + message = progit.lib.new_project( + SESSION, + name=name, + description=description, + user=flask.g.fas_user.username, + gitfolder=APP.config['GIT_FOLDER'], + docfolder=APP.config['DOCS_FOLDER'], + ticketfolder=APP.config['TICKETS_FOLDER'] + ) + SESSION.commit() + generate_gitolite_acls() + flask.flash(message) + return flask.redirect(flask.url_for('view_repo', repo=name)) + except progit.exceptions.ProgitException, err: + flask.flash(str(err), 'error') + except SQLAlchemyError, err: # pragma: no cover + SESSION.rollback() + flask.flash(str(err), 'error') + + return flask.render_template( + 'new_project.html', + form=form, + ) + + +@APP.route('/settings/', methods=('GET', 'POST')) +@cla_required +def user_settings(): + """ Update the user settings. + """ + user = progit.lib.get_user(SESSION, flask.g.fas_user.username) + + form = progit.forms.UserSettingsForm() + if form.validate_on_submit(): + ssh_key = form.ssh_key.data + + try: + message = progit.lib.update_user_ssh( + SESSION, + user=user, + ssh_key=ssh_key, + ) + if message != 'Nothing to update': + generate_gitolite_key(user.user, ssh_key) + generate_authorized_key_file() + SESSION.commit() + flask.flash(message) + return flask.redirect( + flask.url_for('view_user', username=user.user)) + except SQLAlchemyError, err: # pragma: no cover + SESSION.rollback() + flask.flash(str(err), 'error') + elif flask.request.method == 'GET': + form.ssh_key.data = user.public_ssh_key + + return flask.render_template( + 'user_settings.html', + user=user, + form=form, + ) diff --git a/progit/ui/docs.py b/progit/ui/docs.py new file mode 100644 index 0000000..ae098ee --- /dev/null +++ b/progit/ui/docs.py @@ -0,0 +1,161 @@ +#-*- coding: utf-8 -*- + +""" + (c) 2014 - Copyright Red Hat Inc + + Authors: + Pierre-Yves Chibon + +""" + +import flask +import os +from math import ceil + +import pygit2 +from sqlalchemy.exc import SQLAlchemyError +from pygments import highlight +from pygments.lexers import guess_lexer +from pygments.lexers.text import DiffLexer +from pygments.formatters import HtmlFormatter + + +import progit.doc_utils +import progit.exceptions +import progit.lib +import progit.forms +from progit import APP, SESSION, LOG, cla_required + + +def __get_tree(repo_obj, tree, filepath, startswith=False): + ''' Retrieve the entry corresponding to the provided filename in a + given tree. + ''' + filename = filepath[0] + if isinstance(tree, pygit2.Blob): + return (tree, None) + cnt = 0 + for el in tree: + cnt += 1 + ok = False + if el.name.startswith(filename): + ok = True + if el.name == filename: + ok = True + if ok and len(filepath) == 1: + return (el, tree) + elif ok: + return __get_tree( + repo_obj, repo_obj[el.oid], filepath[1:], + startswith=startswith) + + if len(filepath) == 1: + return None, tree + else: + return __get_tree( + repo_obj, repo_obj[tree.oid], filepath[1:], + startswith=startswith) + + +def __get_tree_and_content(repo_obj, commit, path, startswith): + ''' Return the tree and the content of the specified file. ''' + + (blob_or_tree, tree_obj) = __get_tree( + repo_obj, commit.tree, path, startswith=startswith) + + if blob_or_tree is None: + return tree_obj, None + + if not repo_obj[blob_or_tree.oid]: + flask.abort(404, 'File not found') + + blob_or_tree_obj = repo_obj[blob_or_tree.oid] + blob = repo_obj[blob_or_tree.oid] + + content = None + if isinstance(blob, pygit2.Blob): # Returned a file + name, ext = os.path.splitext(blob_or_tree.name) + content = progit.doc_utils.convert_readme(blob_or_tree_obj.data, ext) + else: # Returned a tree + raise progit.exceptions.FileNotFoundException('File not found') + + tree = sorted(tree_obj, key=lambda x: x.filemode) + return (tree, content) + + +## URLs + + +@APP.route('//docs') +@APP.route('//docs/') +@APP.route('//docs/') +@APP.route('//docs//') +@APP.route('/fork///docs') +@APP.route('/fork///docs/') +@APP.route('/fork///docs/') +@APP.route('/fork///docs//') +def view_docs(repo, username=None, branchname=None, filename=None): + """ Display the documentation + """ + status = flask.request.args.get('status', None) + + repo = progit.lib.get_project(SESSION, repo, user=username) + + if not repo: + flask.abort(404, 'Project not found') + + if not repo.project_docs: + flask.abort(404, 'No documentation found for this project') + + reponame = os.path.join(APP.config['DOCS_FOLDER'], repo.path) + if not os.path.exists(reponame): + flask.flash( + 'No docs repository could be found, please contact an admin', + 'error') + return flask.redirect(flask.url_for( + 'view_repo', repo=repo.name, username=username)) + + repo_obj = pygit2.Repository(reponame) + + if branchname in repo_obj.listall_branches(): + branch = repo_obj.lookup_branch(branchname) + commit = branch.get_object() + else: + if not repo_obj.is_empty: + commit = repo_obj[repo_obj.head.target] + else: + commit = None + branchname = 'master' + + content = None + tree = None + startswith = False + if not filename: + path = ['index'] + startswith = True + else: + path = filename.split('/') + + if commit: + try: + (tree, content) = __get_tree_and_content( + repo_obj, commit, path, startswith) + except progit.exceptions.FileNotFoundException: + if not path[0].startswith('index'): + path.append('index') + filename = filename + '/' + + (tree, content) = __get_tree_and_content( + repo_obj, commit, path, startswith=True) + + return flask.render_template( + 'docs.html', + select='docs', + repo_obj=repo_obj, + repo=repo, + username=username, + branchname=branchname, + filename=filename, + tree=tree, + content=content, + ) diff --git a/progit/ui/fork.py b/progit/ui/fork.py new file mode 100644 index 0000000..385449c --- /dev/null +++ b/progit/ui/fork.py @@ -0,0 +1,584 @@ +#-*- coding: utf-8 -*- + +""" + (c) 2014 - Copyright Red Hat Inc + + Authors: + Pierre-Yves Chibon + +""" + +import flask +import os +import shutil +import tempfile +from math import ceil + +import pygit2 +from sqlalchemy.exc import SQLAlchemyError +from pygments import highlight +from pygments.lexers import guess_lexer +from pygments.lexers.text import DiffLexer +from pygments.formatters import HtmlFormatter + + +import progit.doc_utils +import progit.lib +import progit.forms +from progit import (APP, SESSION, LOG, __get_file_in_tree, cla_required, + is_repo_admin, generate_gitolite_acls) + + +@APP.route('//request-pulls') +@APP.route('/fork///request-pulls') +def request_pulls(repo, username=None): + """ Request pulling the changes from the fork into the project. + """ + status = flask.request.args.get('status', True) + + repo = progit.lib.get_project(SESSION, repo, user=username) + + if not repo: + flask.abort(404, 'Project not found') + + if status is False or str(status).lower() == 'closed': + requests = progit.lib.get_pull_requests( + SESSION, project_id=repo.id, status=False) + else: + requests = progit.lib.get_pull_requests( + SESSION, project_id=repo.id, status=status) + + return flask.render_template( + 'requests.html', + select='requests', + repo=repo, + username=username, + requests=requests, + status=status, + ) + + +@APP.route('//request-pull/') +@APP.route('/fork///request-pull/') +def request_pull(repo, requestid, username=None): + """ Request pulling the changes from the fork into the project. + """ + + repo = progit.lib.get_project(SESSION, repo, user=username) + + if not repo: + flask.abort(404, 'Project not found') + + request = progit.lib.get_pull_request( + SESSION, project_id=repo.id, requestid=requestid) + + if not request: + flask.abort(404, 'Pull-request not found') + + repo = request.repo_from + + if repo.is_fork: + repopath = os.path.join(APP.config['FORK_FOLDER'], repo.path) + else: + repopath = os.path.join(APP.config['GIT_FOLDER'], repo.path) + repo_obj = pygit2.Repository(repopath) + + if repo.parent: + parentname = os.path.join(APP.config['GIT_FOLDER'], repo.parent.path) + else: + parentname = os.path.join(APP.config['GIT_FOLDER'], repo.path) + orig_repo = pygit2.Repository(parentname) + + diff_commits = [] + diffs = [] + repo_commit = repo_obj[request.stop_id] + if not repo_obj.is_empty and not orig_repo.is_empty: + orig_commit = orig_repo[ + orig_repo.lookup_branch('master').get_object().hex] + + master_commits = [ + commit.oid.hex + for commit in orig_repo.walk( + orig_repo.lookup_branch('master').get_object().hex, + pygit2.GIT_SORT_TIME) + ] + + repo_commit = repo_obj[request.start_id] + + for commit in repo_obj.walk( + request.stop_id, pygit2.GIT_SORT_TIME): + if commit.oid.hex in master_commits: + break + diff_commits.append(commit) + diffs.append( + repo_obj.diff( + repo_obj.revparse_single(commit.parents[0].oid.hex), + repo_obj.revparse_single(commit.oid.hex) + ) + ) + + elif orig_repo.is_empty: + orig_commit = None + diff = repo_commit.tree.diff_to_tree(swap=True) + else: + flask.flash( + 'Fork is empty, there are no commits to request pulling', + 'error') + return flask.redirect(flask.url_for( + 'view_repo', username=username, repo=repo.name)) + + html_diffs = [] + for diff in diffs: + html_diffs.append( + highlight( + diff.patch, + DiffLexer(), + HtmlFormatter( + noclasses=True, + style="tango",) + ) + ) + + return flask.render_template( + 'pull_request.html', + select='requests', + requestid=requestid, + repo=repo, + username=username, + request=request, + repo_admin=is_repo_admin(request.repo), + repo_obj=repo_obj, + orig_repo=orig_repo, + diff_commits=diff_commits, + diffs=diffs, + html_diffs=html_diffs, + ) + +@APP.route('//request-pull/.patch') +@APP.route('/fork///request-pull/.patch') +def request_pull_patch(repo, requestid, username=None): + """ Returns the commits from the specified pull-request as patches. + """ + + repo = progit.lib.get_project(SESSION, repo, user=username) + + if not repo: + flask.abort(404, 'Project not found') + + request = progit.lib.get_pull_request( + SESSION, project_id=repo.id, requestid=requestid) + + if not request: + flask.abort(404, 'Pull-request not found') + + repo = request.repo_from + + if repo.is_fork: + repopath = os.path.join(APP.config['FORK_FOLDER'], repo.path) + else: + repopath = os.path.join(APP.config['GIT_FOLDER'], repo.path) + repo_obj = pygit2.Repository(repopath) + + if repo.parent: + parentname = os.path.join(APP.config['GIT_FOLDER'], repo.parent.path) + else: + parentname = os.path.join(APP.config['GIT_FOLDER'], repo.path) + orig_repo = pygit2.Repository(parentname) + + diff_commits = [] + repo_commit = repo_obj[request.stop_id] + if not repo_obj.is_empty and not orig_repo.is_empty: + orig_commit = orig_repo[ + orig_repo.lookup_branch('master').get_object().hex] + + master_commits = [ + commit.oid.hex + for commit in orig_repo.walk( + orig_repo.lookup_branch('master').get_object().hex, + pygit2.GIT_SORT_TIME) + ] + + repo_commit = repo_obj[request.start_id] + + for commit in repo_obj.walk( + request.stop_id, pygit2.GIT_SORT_TIME): + if commit.oid.hex in master_commits: + break + diff_commits.append(commit) + + elif orig_repo.is_empty: + orig_commit = None + diff = repo_commit.tree.diff_to_tree(swap=True) + else: + flask.flash( + 'Fork is empty, there are no commits to request pulling', + 'error') + return flask.redirect(flask.url_for( + 'view_repo', username=username, repo=repo.name)) + + diff_commits.reverse() + patch = progit.lib.commit_to_patch(repo_obj, diff_commits) + + return flask.Response(patch, content_type="text/plain;charset=UTF-8") + + +@APP.route('//request-pull//comment//', + methods=('GET', 'POST')) +@APP.route('/fork///request-pull//comment/' + '/', methods=('GET', 'POST')) +def pull_request_add_comment(repo, requestid, commit, row, username=None): + """ Add a comment to a commit in a pull-request. + """ + repo = progit.lib.get_project(SESSION, repo, user=username) + + if not repo: + flask.abort(404, 'Project not found') + + request = progit.lib.get_pull_request( + SESSION, project_id=repo.id, requestid=requestid) + repo = request.repo_from + + if not request: + flask.abort(404, 'Pull-request not found') + + form = progit.forms.AddPullRequestCommentForm() + form.commit.data = commit + form.requestid.data = requestid + form.row.data = row + + if form.validate_on_submit(): + comment = form.comment.data + + try: + message = progit.lib.add_pull_request_comment( + SESSION, + request=request, + commit=commit, + row=row, + comment=comment, + user=flask.g.fas_user.username, + ) + SESSION.commit() + flask.flash(message) + except SQLAlchemyError, err: # pragma: no cover + SESSION.rollback() + flask.flash(str(err), 'error') + + return flask.redirect(flask.url_for( + 'request_pull', username=username, + repo=repo.name, requestid=requestid)) + + return flask.render_template( + 'pull_request_comment.html', + select='requests', + requestid=requestid, + repo=repo, + username=username, + commit=commit, + row=row, + form=form, + ) + + + +@APP.route('//request-pull/merge/') +@APP.route('/fork///request-pull/merge/') +def merge_request_pull(repo, requestid, username=None): + """ Request pulling the changes from the fork into the project. + """ + repo = progit.lib.get_project(SESSION, repo, user=username) + + if not repo: + flask.abort(404, 'Project not found') + + request = progit.lib.get_pull_request( + SESSION, project_id=repo.id, requestid=requestid) + + if not request: + flask.abort(404, 'Pull-request not found') + + if not is_repo_admin(repo): + flask.abort( + 403, + 'You are not allowed to merge pull-request for this project') + + error_output = flask.url_for( + 'request_pull', repo=repo.name, requestid=requestid) + if username: + error_output = flask.url_for( + 'fork_request_pull', + repo=repo.name, + requestid=requestid, + username=username) + + # Get the fork + if request.repo_from.is_fork: + repopath = os.path.join( + APP.config['FORK_FOLDER'], request.repo_from.path) + else: + repopath = os.path.join( + APP.config['GIT_FOLDER'], request.repo_from.path) + fork_obj = pygit2.Repository(repopath) + + # Get the original repo + parentpath = os.path.join(APP.config['GIT_FOLDER'], request.repo.path) + orig_repo = pygit2.Repository(parentpath) + + # Clone the original repo into a temp folder + newpath = tempfile.mkdtemp() + new_repo = pygit2.clone_repository(parentpath, newpath) + + repo_commit = fork_obj[request.stop_id] + + ori_remote = new_repo.remotes[0] + # Add the fork as remote repo + reponame = '%s_%s' % (request.user.user, repo.name) + remote = new_repo.create_remote(reponame, repopath) + + # Fetch the commits + remote.fetch() + + merge = new_repo.merge(repo_commit.oid) + master_ref = new_repo.lookup_reference('HEAD').resolve() + + refname = '%s:%s' % (master_ref.name, master_ref.name) + if merge.is_uptodate: + flask.flash('Nothing to do, changes were already merged', 'error') + progit.lib.close_pull_request(SESSION, request) + SESSION.commit() + return flask.redirect(error_output) + elif merge.is_fastforward: + master_ref.target = merge.fastforward_oid + ori_remote.push(refname) + flask.flash('Changes merged!') + else: + new_repo.index.write() + try: + tree = new_repo.index.write_tree() + except pygit2.GitError: + shutil.rmtree(newpath) + flask.flash('Merge conflicts!', 'error') + return flask.redirect(flask.url_for( + 'request_pull', + repo=repo.name, + username=username, + requestid=requestid)) + head = new_repo.lookup_reference('HEAD').get_object() + commit = new_repo[head.oid] + sha = new_repo.create_commit( + 'refs/heads/master', + repo_commit.author, + repo_commit.committer, + 'Merge #%s `%s`' % (request.id, request.title), + tree, + [head.hex, repo_commit.oid.hex]) + ori_remote.push(refname) + flask.flash('Changes merged!') + + # Update status + progit.lib.close_pull_request(SESSION, request) + SESSION.commit() + shutil.rmtree(newpath) + + return flask.redirect(flask.url_for('view_repo', repo=repo.name)) + + +## Specific actions + + +@APP.route('/do_fork/') +@APP.route('/do_fork//') +@cla_required +def fork_project(repo, username=None): + """ Fork the project specified into the user's namespace + """ + repo = progit.lib.get_project(SESSION, repo, user=username) + + if repo is None: + flask.abort(404) + + try: + message = progit.lib.fork_project( + session=SESSION, + repo=repo, + gitfolder=APP.config['GIT_FOLDER'], + forkfolder=APP.config['FORK_FOLDER'], + docfolder=APP.config['DOCS_FOLDER'], + ticketfolder=APP.config['TICKETS_FOLDER'], + user=flask.g.fas_user.username) + + SESSION.commit() + generate_gitolite_acls() + flask.flash(message) + return flask.redirect( + flask.url_for( + 'view_repo', + username=flask.g.fas_user.username, + repo=repo.name) + ) + except progit.exceptions.ProgitException, err: + flask.flash(str(err), 'error') + except SQLAlchemyError, err: # pragma: no cover + SESSION.rollback() + flask.flash(str(err), 'error') + + return flask.redirect(flask.url_for('view_repo', repo=repo.name)) + + +@APP.route('//request-pull/new', + methods=('GET', 'POST')) +@APP.route('//request-pull/new/', + methods=('GET', 'POST')) +@APP.route('/fork///request-pull/new', + methods=('GET', 'POST')) +@APP.route('/fork///request-pull/new/', + methods=('GET', 'POST')) +@cla_required +def new_request_pull(repo, username=None, commitid=None): + """ Request pulling the changes from the fork into the project. + """ + repo = progit.lib.get_project(SESSION, repo, user=username) + + if not repo: + flask.abort(404) + + if not is_repo_admin(repo): + flask.abort( + 403, + 'You are not allowed to create pull-requests for this project') + + if repo.is_fork: + repopath = os.path.join(APP.config['FORK_FOLDER'], repo.path) + else: + repopath = os.path.join(APP.config['GIT_FOLDER'], repo.path) + repo_obj = pygit2.Repository(repopath) + + if repo.parent: + parentname = os.path.join(APP.config['GIT_FOLDER'], repo.parent.path) + else: + parentname = os.path.join(APP.config['GIT_FOLDER'], repo.path) + orig_repo = pygit2.Repository(parentname) + + frombranchname = flask.request.args.get('from_branch', 'master') + frombranch = repo_obj.lookup_branch(frombranchname) + if not frombranch: + flask.flash('Branch %s does not exist' % frombranchname, 'error') + frombranchname = 'master' + + branchname = flask.request.args.get('branch', 'master') + branch = orig_repo.lookup_branch(branchname) + if not branch: + flask.flash('Branch %s does not exist' % branchname, 'error') + branchname = 'master' + + if commitid is None: + commitid = repo_obj.head.target + if branchname: + branch = repo_obj.lookup_branch(frombranchname) + commitid = branch.get_object().hex + + diff_commits = [] + diffs = [] + if not repo_obj.is_empty and not orig_repo.is_empty: + orig_commit = orig_repo[ + orig_repo.lookup_branch(branchname).get_object().hex] + + master_commits = [ + commit.oid.hex + for commit in orig_repo.walk( + orig_repo.lookup_branch(branchname).get_object().hex, + pygit2.GIT_SORT_TIME) + ] + + repo_commit = repo_obj[commitid] + + for commit in repo_obj.walk( + repo_commit.oid.hex, pygit2.GIT_SORT_TIME): + if commit.oid.hex in master_commits: + break + diff_commits.append(commit) + diffs.append( + repo_obj.diff( + repo_obj.revparse_single(commit.parents[0].oid.hex), + repo_obj.revparse_single(commit.oid.hex) + ) + ) + + elif orig_repo.is_empty: + orig_commit = None + repo_commit = repo_obj[repo_obj.head.target] + diff = repo_commit.tree.diff_to_tree(swap=True) + else: + flask.flash( + 'Fork is empty, there are no commits to request pulling', + 'error') + return flask.redirect(flask.url_for( + 'view_repo', username=username, repo=repo.name)) + + html_diffs = [] + for diff in diffs: + html_diffs.append( + highlight( + diff.patch, + DiffLexer(), + HtmlFormatter( + noclasses=True, + style="tango",) + ) + ) + + form = progit.forms.RequestPullForm() + if form.validate_on_submit(): + try: + if orig_commit: + orig_commit = orig_commit.oid.hex + + parent = repo + if repo.parent: + parent = repo.parent + + message = progit.lib.new_pull_request( + SESSION, + repo=parent, + repo_from=repo, + branch=branchname, + title=form.title.data, + start_id=orig_commit, + stop_id=repo_commit.oid.hex, + user=flask.g.fas_user.username, + ) + SESSION.commit() + flask.flash(message) + + if not parent.is_fork: + url = flask.url_for( + 'request_pulls', username=None, repo=parent.name) + else: + url = flask.url_for( + 'request_pulls', username=parent.user, repo=parent.name) + + return flask.redirect(url) + except progit.exceptions.ProgitException, err: + flask.flash(str(err), 'error') + except SQLAlchemyError, err: # pragma: no cover + SESSION.rollback() + flask.flash(str(err), 'error') + + return flask.render_template( + 'pull_request.html', + select='requests', + repo=repo, + username=username, + commitid=commitid, + repo_obj=repo_obj, + orig_repo=orig_repo, + diff_commits=diff_commits, + diffs=diffs, + html_diffs=html_diffs, + form=form, + branches=[ + branch.replace('refs/heads/', '') + for branch in sorted(orig_repo.listall_references()) + ], + branchname=branchname, + ) diff --git a/progit/ui/issues.py b/progit/ui/issues.py new file mode 100644 index 0000000..72a6946 --- /dev/null +++ b/progit/ui/issues.py @@ -0,0 +1,272 @@ +#-*- coding: utf-8 -*- + +""" + (c) 2014 - Copyright Red Hat Inc + + Authors: + Pierre-Yves Chibon + +""" + +import flask +import os +from math import ceil + +import pygit2 +from sqlalchemy.exc import SQLAlchemyError +from pygments import highlight +from pygments.lexers import guess_lexer +from pygments.lexers.text import DiffLexer +from pygments.formatters import HtmlFormatter + + +import progit.doc_utils +import progit.lib +import progit.forms +from progit import (APP, SESSION, LOG, __get_file_in_tree, cla_required, + is_repo_admin, authenticated) + + +## URLs + +@APP.route('//issue//add', methods=('GET', 'POST')) +@APP.route('/fork///issue//add', + methods=('GET', 'POST')) +def add_comment_issue(repo, issueid, username=None): + ''' Add a comment to an issue. ''' + repo = progit.lib.get_project(SESSION, repo, user=username) + + if repo is None: + flask.abort(404, 'Project not found') + + if not repo.issue_tracker: + flask.abort(404, 'No issue tracker found for this project') + + issue = progit.lib.get_issue(SESSION, repo.id, issueid) + + if issue is None or issue.project != repo: + flask.abort(404, 'Issue not found') + + form = progit.forms.AddIssueCommentForm() + if form.validate_on_submit(): + comment = form.comment.data + + try: + message = progit.lib.add_issue_comment( + SESSION, + issue=issue, + comment=comment, + user=flask.g.fas_user.username, + ticketfolder=APP.config['TICKETS_FOLDER'], + ) + SESSION.commit() + flask.flash(message) + except SQLAlchemyError, err: # pragma: no cover + SESSION.rollback() + flask.flash(str(err), 'error') + + return flask.redirect(flask.url_for( + 'view_issue', username=username, repo=repo.name, issueid=issueid)) + + +@APP.route('//issues') +@APP.route('/fork///issues') +def view_issues(repo, username=None): + """ List all issues associated to a repo + """ + status = flask.request.args.get('status', None) + + repo = progit.lib.get_project(SESSION, repo, user=username) + + if repo is None: + flask.abort(404, 'Project not found') + + if not repo.issue_tracker: + flask.abort(404, 'No issue tracker found for this project') + + if status is not None: + if status.lower() == 'closed': + issues = progit.lib.get_issues(SESSION, repo, closed=True) + else: + issues = progit.lib.get_issues(SESSION, repo, status=status) + else: + issues = progit.lib.get_issues(SESSION, repo, status='Open') + + return flask.render_template( + 'issues.html', + select='issues', + repo=repo, + username=username, + status=status, + issues=issues, + ) + + +@APP.route('//new_issue', methods=('GET', 'POST')) +@APP.route('/fork///new_issue', methods=('GET', 'POST')) +@cla_required +def new_issue(repo, username=None): + """ Create a new issue + """ + repo = progit.lib.get_project(SESSION, repo, user=username) + + if repo is None: + flask.abort(404, 'Project not found') + + status = progit.lib.get_issue_statuses(SESSION) + form = progit.forms.IssueForm(status=status) + if form.validate_on_submit(): + title = form.title.data + content = form.issue_content.data + + try: + message = progit.lib.new_issue( + SESSION, + repo=repo, + title=title, + content=content, + user=flask.g.fas_user.username, + ticketfolder=APP.config['TICKETS_FOLDER'], + ) + SESSION.commit() + flask.flash(message) + return flask.redirect(flask.url_for( + 'view_issues', username=username, repo=repo.name)) + except progit.exceptions.ProgitException, err: + flask.flash(str(err), 'error') + except SQLAlchemyError, err: # pragma: no cover + SESSION.rollback() + flask.flash(str(err), 'error') + + return flask.render_template( + 'new_issue.html', + select='issues', + form=form, + repo=repo, + username=username, + ) + + +@APP.route('//issue/', methods=('GET', 'POST')) +@APP.route('/fork///issue/', + methods=('GET', 'POST')) +def view_issue(repo, issueid, username=None): + """ List all issues associated to a repo + """ + + repo = progit.lib.get_project(SESSION, repo, user=username) + + if repo is None: + flask.abort(404, 'Project not found') + + if not repo.issue_tracker: + flask.abort(404, 'No issue tracker found for this project') + + issue = progit.lib.get_issue(SESSION, repo.id, issueid) + + if issue is None or issue.project != repo: + flask.abort(404, 'Issue not found') + + status = progit.lib.get_issue_statuses(SESSION) + + form_comment = progit.forms.AddIssueCommentForm() + form = None + if authenticated() and is_repo_admin(repo): + form = progit.forms.UpdateIssueStatusForm(status=status) + + if form.validate_on_submit(): + try: + message = progit.lib.edit_issue( + SESSION, + issue=issue, + status=form.status.data, + ticketfolder=APP.config['TICKETS_FOLDER'], + ) + SESSION.commit() + flask.flash(message) + url = flask.url_for( + 'view_issues', username=username, repo=repo.name) + return flask.redirect(url) + except SQLAlchemyError, err: # pragma: no cover + SESSION.rollback() + flask.flash(str(err), 'error') + elif flask.request.method == 'GET': + form.status.data = issue.status + + return flask.render_template( + 'issue.html', + select='issues', + repo=repo, + username=username, + issue=issue, + issueid=issueid, + form=form, + form_comment=form_comment, + ) + + +@APP.route('//issue//edit', methods=('GET', 'POST')) +@APP.route('/fork///issue//edit', + methods=('GET', 'POST')) +@cla_required +def edit_issue(repo, issueid, username=None): + """ Edit the specified issue + """ + repo = progit.lib.get_project(SESSION, repo, user=username) + + if repo is None: + flask.abort(404, 'Project not found') + + if not repo.issue_tracker: + flask.abort(404, 'No issue tracker found for this project') + + if not is_repo_admin(repo): + flask.abort( + 403, 'You are not allowed to edit issues for this project') + + issue = progit.lib.get_issue(SESSION, repo.id, issueid) + + if issue is None or issue.project != repo: + flask.abort(404, 'Issue not found') + + status = progit.lib.get_issue_statuses(SESSION) + form = progit.forms.IssueForm(status=status) + if form.validate_on_submit(): + title = form.title.data + content = form.issue_content.data + status = form.status.data + + try: + message = progit.lib.edit_issue( + SESSION, + issue=issue, + title=title, + content=content, + status=status, + ticketfolder=APP.config['TICKETS_FOLDER'], + ) + SESSION.commit() + flask.flash(message) + url = flask.url_for( + 'view_issue', username=username, + repo=repo.name, issueid=issueid) + return flask.redirect(url) + except progit.exceptions.ProgitException, err: + flask.flash(str(err), 'error') + except SQLAlchemyError, err: # pragma: no cover + SESSION.rollback() + flask.flash(str(err), 'error') + elif flask.request.method == 'GET': + form.title.data = issue.title + form.issue_content.data = issue.content + form.status.data = issue.status + + return flask.render_template( + 'new_issue.html', + select='issues', + type='edit', + form=form, + repo=repo, + username=username, + issue=issue, + ) diff --git a/progit/ui/plugins.py b/progit/ui/plugins.py new file mode 100644 index 0000000..9770c63 --- /dev/null +++ b/progit/ui/plugins.py @@ -0,0 +1,142 @@ +#-*- coding: utf-8 -*- + +""" + (c) 2014 - Copyright Red Hat Inc + + Authors: + Pierre-Yves Chibon + +""" + +import flask +import os +import sys +from math import ceil + +import pygit2 +from sqlalchemy.exc import SQLAlchemyError +from straight.plugin import load +from hooks import BaseHook + +import progit.exceptions +import progit.lib +import progit.forms +from progit import APP, SESSION, LOG, cla_required, is_repo_admin +from progit.model import BASE + + +def get_plugin_names(): + ''' Return the list of plugins names. ''' + plugins = load('progit.hooks', subclasses=BaseHook) + output = [plugin.name for plugin in plugins] + return output + + +def get_plugin_tables(): + ''' Return the list of all plugins. ''' + plugins = load('progit.hooks', subclasses=BASE) + return plugins + + +def get_plugin(plugin_name): + ''' Return the list of plugins names. ''' + plugins = load('progit.hooks', subclasses=BaseHook) + for plugin in plugins: + if plugin.name == plugin_name: + return plugin + + +@APP.route('//settings/', methods=('GET', 'POST')) +@APP.route('/fork///settings/', + methods=('GET', 'POST')) +@cla_required +def view_plugin(repo, plugin, username=None): + """ Presents the settings of the project. + """ + return view_plugin_page(repo, plugin, username=username, full=True) + + +@APP.route( + '//settings//', methods=('GET', 'POST')) +@APP.route( + '/fork///settings//', + methods=('GET', 'POST')) +@cla_required +def view_plugin_page(repo, plugin, full, username=None): + """ Presents the settings of the project. + """ + repo = progit.lib.get_project(SESSION, repo, user=username) + + if not repo: + flask.abort(404, 'Project not found') + + if not is_repo_admin(repo): + flask.abort( + 403, + 'You are not allowed to change the settings for this project') + + plugin = get_plugin(plugin) + fields = [] + new = True + dbobj = plugin.db_object() + if hasattr(repo, plugin.backref): + dbobj = getattr(repo, plugin.backref) + # There should always be only one, but let's double check + if dbobj and len(dbobj) > 0: + dbobj = dbobj[0] + new = False + else: + dbobj = plugin.db_object() + + form = plugin.form(obj=dbobj) + for field in plugin.form_fields: + fields.append(getattr(form, field)) + + if form.validate_on_submit(): + form.populate_obj(obj=dbobj) + if new: + dbobj.project_id = repo.id + SESSION.add(dbobj) + try: + SESSION.flush() + except SQLAlchemyError, err: + SESSION.rollback() + APP.logger.debug('Could not add plugin %s', plugin.name) + APP.logger.exception(err) + flask.flash( + 'Could not add plugin %s, please contact an admin' + % plugin.name) + + return flask.render_template( + 'plugin.html', + select='settings', + full=full, + repo=repo, + username=username, + plugin=plugin, + form=form, + fields=fields, + ) + + if form.active.data: + # Set up the main script if necessary + plugin.set_up(repo) + # Install the plugin itself + plugin.install(repo, dbobj) + flask.flash('Hook activated') + else: + plugin.remove(repo) + flask.flash('Hook inactived') + + SESSION.commit() + + return flask.render_template( + 'plugin.html', + select='settings', + full=full, + repo=repo, + username=username, + plugin=plugin, + form=form, + fields=fields, + ) diff --git a/progit/ui/repo.py b/progit/ui/repo.py new file mode 100644 index 0000000..99c200d --- /dev/null +++ b/progit/ui/repo.py @@ -0,0 +1,692 @@ +#-*- coding: utf-8 -*- + +""" + (c) 2014 - Copyright Red Hat Inc + + Authors: + Pierre-Yves Chibon + +""" + +import flask +import shutil +import os +from math import ceil + +import pygit2 +from sqlalchemy.exc import SQLAlchemyError +from pygments import highlight +from pygments.lexers import guess_lexer +from pygments.lexers.text import DiffLexer +from pygments.formatters import HtmlFormatter + +import progit.exceptions +import progit.lib +import progit.forms +import progit.plugins +from progit import (APP, SESSION, LOG, __get_file_in_tree, cla_required, + is_repo_admin) + + +@APP.route('/') +@APP.route('/fork//') +def view_repo(repo, username=None): + """ Front page of a specific repo. + """ + repo = progit.lib.get_project(SESSION, repo, user=username) + + if repo is None: + flask.abort(404, 'Project not found') + + reponame = os.path.join(APP.config['GIT_FOLDER'], repo.path) + if repo.is_fork: + reponame = os.path.join(APP.config['FORK_FOLDER'], repo.path) + repo_obj = pygit2.Repository(reponame) + + cnt = 0 + last_commits = [] + tree = [] + if not repo_obj.is_empty: + for commit in repo_obj.walk( + repo_obj.head.target, pygit2.GIT_SORT_TIME): + last_commits.append(commit) + cnt += 1 + if cnt == 10: + break + tree = sorted(last_commits[0].tree, key=lambda x: x.filemode) + + readme = None + for i in tree: + name, ext = os.path.splitext(i.name) + if name == 'README': + content = repo_obj[i.oid].data + readme = progit.doc_utils.convert_readme(content, ext) + + diff_commits = [] + if repo.is_fork: + parentname = os.path.join( + APP.config['GIT_FOLDER'], repo.parent.path) + if repo.parent.is_fork: + parentname = os.path.join( + APP.config['FORK_FOLDER'], repo.parent.path) + elif repo.parent: + parentname = os.path.join( + APP.config['GIT_FOLDER'], repo.parent.path) + else: + parentname = os.path.join(APP.config['GIT_FOLDER'], repo.path) + + orig_repo = pygit2.Repository(parentname) + + if not repo_obj.is_empty and not orig_repo.is_empty: + orig_commit = orig_repo[ + orig_repo.lookup_branch('master').get_object().hex] + + master_commits = [ + commit.oid.hex + for commit in orig_repo.walk( + orig_repo.lookup_branch('master').get_object().hex, + pygit2.GIT_SORT_TIME) + ] + + repo_commit = repo_obj[ + repo_obj.lookup_branch('master').get_object().hex] + + for commit in repo_obj.walk( + repo_commit.oid.hex, pygit2.GIT_SORT_TIME): + if commit.oid.hex in master_commits: + break + diff_commits.append(commit.oid.hex) + + return flask.render_template( + 'repo_info.html', + select='overview', + repo=repo, + repo_obj=repo_obj, + username=username, + readme=readme, + branches=sorted(repo_obj.listall_branches()), + branchname='master', + last_commits=last_commits, + tree=tree, + diff_commits=diff_commits, + ) + + +@APP.route('//branch/') +@APP.route('/fork///branch/') +def view_repo_branch(repo, branchname, username=None): + repo = progit.lib.get_project(SESSION, repo, user=username) + + if not repo: + flask.abort(404, 'Project not found') + + reponame = os.path.join(APP.config['GIT_FOLDER'], repo.path) + if repo.is_fork: + reponame = os.path.join(APP.config['FORK_FOLDER'], repo.path) + repo_obj = pygit2.Repository(reponame) + + if branchname not in repo_obj.listall_branches(): + flask.abort(404, 'Branch no found') + + branch = repo_obj.lookup_branch(branchname) + + cnt = 0 + last_commits = [] + for commit in repo_obj.walk(branch.get_object().hex, pygit2.GIT_SORT_TIME): + last_commits.append(commit) + cnt += 1 + if cnt == 10: + break + + diff_commits = [] + if repo.is_fork: + parentname = os.path.join( + APP.config['GIT_FOLDER'], repo.parent.path) + if repo.parent.is_fork: + parentname = os.path.join( + APP.config['FORK_FOLDER'], repo.parent.path) + elif repo.parent: + parentname = os.path.join( + APP.config['GIT_FOLDER'], repo.parent.path) + else: + parentname = os.path.join(APP.config['GIT_FOLDER'], repo.path) + + orig_repo = pygit2.Repository(parentname) + + if not repo_obj.is_empty and not orig_repo.is_empty: + orig_commit = orig_repo[ + orig_repo.lookup_branch('master').get_object().hex] + + master_commits = [ + commit.oid.hex + for commit in orig_repo.walk( + orig_repo.lookup_branch('master').get_object().hex, + pygit2.GIT_SORT_TIME) + ] + + repo_commit = repo_obj[branch.get_object().hex] + + for commit in repo_obj.walk( + repo_commit.oid.hex, pygit2.GIT_SORT_TIME): + if commit.oid.hex in master_commits: + break + diff_commits.append(commit.oid.hex) + + return flask.render_template( + 'repo_info.html', + select='overview', + repo=repo, + username=username, + branches=sorted(repo_obj.listall_branches()), + branchname=branchname, + last_commits=last_commits, + tree=sorted(last_commits[0].tree, key=lambda x: x.filemode), + diff_commits=diff_commits, + ) + + +@APP.route('//log') +@APP.route('//log/') +@APP.route('/fork///log') +@APP.route('/fork///log/') +def view_log(repo, branchname=None, username=None): + """ Displays the logs of the specified repo. + """ + repo = progit.lib.get_project(SESSION, repo, user=username) + + if not repo: + flask.abort(404, 'Project not found') + + reponame = os.path.join(APP.config['GIT_FOLDER'], repo.path) + if repo.is_fork: + reponame = os.path.join(APP.config['FORK_FOLDER'], repo.path) + repo_obj = pygit2.Repository(reponame) + + if branchname and branchname not in repo_obj.listall_branches(): + flask.abort(404, 'Branch no found') + + if branchname: + branch = repo_obj.lookup_branch(branchname) + else: + branch = repo_obj.lookup_branch('master') + + try: + page = int(flask.request.args.get('page', 1)) + except ValueError: + page = 1 + + limit = APP.config['ITEM_PER_PAGE'] + start = limit * (page - 1) + end = limit * page + + n_commits = 0 + last_commits = [] + if branch: + for commit in repo_obj.walk( + branch.get_object().hex, pygit2.GIT_SORT_TIME): + if n_commits >= start and n_commits <= end: + last_commits.append(commit) + n_commits += 1 + + total_page = int(ceil(n_commits / float(limit))) + + diff_commits = [] + if repo.is_fork: + parentname = os.path.join( + APP.config['GIT_FOLDER'], repo.parent.path) + if repo.parent.is_fork: + parentname = os.path.join( + APP.config['FORK_FOLDER'], repo.parent.path) + elif repo.parent: + parentname = os.path.join( + APP.config['GIT_FOLDER'], repo.parent.path) + else: + parentname = os.path.join(APP.config['GIT_FOLDER'], repo.path) + + orig_repo = pygit2.Repository(parentname) + + if not repo_obj.is_empty and not orig_repo.is_empty: + orig_commit = orig_repo[ + orig_repo.lookup_branch('master').get_object().hex] + + master_commits = [ + commit.oid.hex + for commit in orig_repo.walk( + orig_repo.lookup_branch('master').get_object().hex, + pygit2.GIT_SORT_TIME) + ] + + repo_commit = repo_obj[branch.get_object().hex] + + for commit in repo_obj.walk( + repo_commit.oid.hex, pygit2.GIT_SORT_TIME): + if commit.oid.hex in master_commits: + break + diff_commits.append(commit.oid.hex) + + origin = 'view_log' + + return flask.render_template( + 'repo_info.html', + select='logs', + origin=origin, + repo_obj=repo_obj, + repo=repo, + username=username, + branches=sorted(repo_obj.listall_branches()), + branchname=branchname, + last_commits=last_commits, + diff_commits=diff_commits, + page=page, + total_page=total_page, + ) + + +@APP.route('//blob//') +@APP.route('//blob//') +@APP.route('/fork///blob//') +@APP.route('/fork///blob//') +def view_file(repo, identifier, filename, username=None): + """ Displays the content of a file or a tree for the specified repo. + """ + repo = progit.lib.get_project(SESSION, repo, user=username) + + if not repo: + flask.abort(404, 'Project not found') + + reponame = os.path.join(APP.config['GIT_FOLDER'], repo.path) + if repo.is_fork: + reponame = os.path.join(APP.config['FORK_FOLDER'], repo.path) + repo_obj = pygit2.Repository(reponame) + + if identifier in repo_obj.listall_branches(): + branchname = identifier + branch = repo_obj.lookup_branch(identifier) + commit = branch.get_object() + else: + try: + commit = repo_obj.get(identifier) + branchname = identifier + except ValueError: + # If it's not a commit id then it's part of the filename + commit = repo_obj[repo_obj.head.target] + branchname = 'master' + + content = __get_file_in_tree(repo_obj, commit.tree, filename.split('/')) + if not content: + flask.abort(404, 'File not found') + + content = repo_obj[content.oid] + if isinstance(content, pygit2.Blob): + content = highlight( + content.data, + guess_lexer(content.data), + HtmlFormatter( + noclasses=True, + style="tango",) + ) + output_type = 'file' + else: + content = sorted(content, key=lambda x: x.filemode) + output_type = 'tree' + + return flask.render_template( + 'file.html', + select='tree', + repo=repo, + username=username, + branchname=branchname, + filename=filename, + content=content, + output_type=output_type, + ) + + +@APP.route('//') +@APP.route('/fork///') +def view_commit(repo, commitid, username=None): + """ Render a commit in a repo + """ + repo = progit.lib.get_project(SESSION, repo, user=username) + + if not repo: + flask.abort(404, 'Project not found') + + reponame = os.path.join(APP.config['GIT_FOLDER'], repo.path) + if repo.is_fork: + reponame = os.path.join(APP.config['FORK_FOLDER'], repo.path) + repo_obj = pygit2.Repository(reponame) + + try: + commit = repo_obj.get(commitid) + except ValueError: + flask.abort(404, 'Commit not found') + + if commit.parents: + diff = commit.tree.diff_to_tree() + + parent = repo_obj.revparse_single('%s^' % commitid) + diff = repo_obj.diff(parent, commit) + else: + # First commit in the repo + diff = commit.tree.diff_to_tree(swap=True) + + html_diff = highlight( + diff.patch, + DiffLexer(), + HtmlFormatter( + noclasses=True, + style="tango",) + ) + + return flask.render_template( + 'commit.html', + select='logs', + repo=repo, + username=username, + commitid=commitid, + commit=commit, + diff=diff, + html_diff=html_diff, + ) + + +@APP.route('//.patch') +@APP.route('/fork///.patch') +def view_commit_patch(repo, commitid, username=None): + """ Render a commit in a repo as patch + """ + repo = progit.lib.get_project(SESSION, repo, user=username) + + if not repo: + flask.abort(404, 'Project not found') + + reponame = os.path.join(APP.config['GIT_FOLDER'], repo.path) + if repo.is_fork: + reponame = os.path.join(APP.config['FORK_FOLDER'], repo.path) + repo_obj = pygit2.Repository(reponame) + + try: + commit = repo_obj.get(commitid) + except ValueError: + flask.abort(404, 'Commit not found') + + patch = progit.lib.commit_to_patch(repo_obj, commit) + + return flask.Response(patch, content_type="text/plain;charset=UTF-8") + + +@APP.route('//tree/') +@APP.route('//tree/') +@APP.route('/fork///tree/') +@APP.route('/fork///tree/') +def view_tree(repo, identifier=None, username=None): + """ Render the tree of the repo + """ + repo = progit.lib.get_project(SESSION, repo, user=username) + + if repo is None: + flask.abort(404, 'Project not found') + + reponame = os.path.join(APP.config['GIT_FOLDER'], repo.path) + if repo.is_fork: + reponame = os.path.join(APP.config['FORK_FOLDER'], repo.path) + repo_obj = pygit2.Repository(reponame) + + branchname = None + content = None + output_type = None + if not repo_obj.is_empty: + if identifier in repo_obj.listall_branches(): + branchname = identifier + branch = repo_obj.lookup_branch(identifier) + commit = branch.get_object() + else: + try: + commit = repo_obj.get(identifier) + branchname = identifier + except (ValueError, TypeError): + # If it's not a commit id then it's part of the filename + commit = repo_obj[repo_obj.head.target] + branchname = 'master' + + content = sorted(commit.tree, key=lambda x: x.filemode) + output_type = 'tree' + + return flask.render_template( + 'file.html', + select='tree', + repo_obj=repo_obj, + repo=repo, + username=username, + branchname=branchname, + filename='', + content=content, + output_type=output_type, + ) + + +@APP.route('//forks') +@APP.route('/fork///forks') +def view_forks(repo, username=None): + """ Presents all the forks of the project. + """ + repo = progit.lib.get_project(SESSION, repo, user=username) + + if not repo: + flask.abort(404, 'Project not found') + + return flask.render_template( + 'forks.html', + select='forks', + username=username, + repo=repo, + ) + + +@APP.route('//settings', methods=('GET', 'POST')) +@APP.route('/fork///settings', methods=('GET', 'POST')) +@cla_required +def view_settings(repo, username=None): + """ Presents the settings of the project. + """ + repo = progit.lib.get_project(SESSION, repo, user=username) + + if not repo: + flask.abort(404, 'Project not found') + + if not is_repo_admin(repo): + flask.abort( + 403, + 'You are not allowed to change the settings for this project') + + plugins = progit.plugins.get_plugin_names() + + form = progit.forms.ProjectSettingsForm() + + if form.validate_on_submit(): + issue_tracker = form.issue_tracker.data + project_docs = form.project_docs.data + + try: + message = progit.lib.update_project_settings( + SESSION, + repo=repo, + issue_tracker=issue_tracker, + project_docs=project_docs, + ) + SESSION.commit() + flask.flash(message) + return flask.redirect(flask.url_for( + 'view_repo', username=username, repo=repo.name)) + except progit.exceptions.ProgitException, err: + flask.flash(str(err), 'error') + except SQLAlchemyError, err: # pragma: no cover + SESSION.rollback() + flask.flash(str(err), 'error') + elif flask.request.method == 'GET': + form = progit.forms.ProjectSettingsForm(project=repo) + + return flask.render_template( + 'settings.html', + select='settings', + username=username, + repo=repo, + form=form, + plugins=plugins, + ) + + +@APP.route('//updatedesc', methods=['POST']) +@APP.route('/fork///updatedesc', methods=['POST']) +@cla_required +def update_description(repo, username=None): + """ Update the description of a project. + """ + repo = progit.lib.get_project(SESSION, repo, user=username) + + if not repo: + flask.abort(404, 'Project not found') + + if not is_repo_admin(repo): + flask.abort( + 403, + 'You are not allowed to change the settings for this project') + + form = progit.forms.DescriptionForm() + + if form.validate_on_submit(): + try: + repo.description = form.description.data + SESSION.add(repo) + SESSION.commit() + flask.flash('Description updated') + except SQLAlchemyError, err: # pragma: no cover + SESSION.rollback() + flask.flash(str(err), 'error') + + return flask.redirect(flask.url_for( + 'view_settings', username=username, repo=repo.name)) + + +@APP.route('//delete', methods=['POST']) +@APP.route('/fork///delete', methods=['POST']) +@cla_required +def delete_repo(repo, username=None): + """ Delete the present project. + """ + repo = progit.lib.get_project(SESSION, repo, user=username) + + if not repo: + flask.abort(404, 'Project not found') + + if not is_repo_admin(repo): + flask.abort( + 403, + 'You are not allowed to change the settings for this project') + + for issue in repo.issues: + for comment in issue.comments: + SESSION.delete(comment) + SESSION.delete(issue) + for request in repo.requests: + SESSION.delete(request) + SESSION.delete(repo) + + repopath = os.path.join(APP.config['GIT_FOLDER'], repo.path) + if repo.is_fork: + repopath = os.path.join(APP.config['FORK_FOLDER'], repo.path) + docpath = os.path.join(APP.config['DOCS_FOLDER'], repo.path) + + try: + shutil.rmtree(repopath) + shutil.rmtree(docpath) + SESSION.commit() + except (OSError, IOError), err: + APP.logger.exception(err) + flask.flash('Could not delete the project from the system', 'error') + except SQLAlchemyError, err: # pragma: no cover + SESSION.rollback() + APP.logger.exception(err) + flask.flash('Could not delete the project', 'error') + + return flask.redirect( + flask.url_for('view_user', username=flask.g.fas_user.username)) + + +@APP.route('//dropuser/', methods=['POST']) +@APP.route('/fork///dropuser/', methods=['POST']) +@cla_required +def remove_user(repo, userid, username=None): + """ Remove the specified user from the project. + """ + repo = progit.lib.get_project(SESSION, repo, user=username) + + if not repo: + flask.abort(404, 'Project not found') + + if not is_repo_admin(repo): + flask.abort( + 403, + 'You are not allowed to change the users for this project') + + userids = [str(user.user.id) for user in repo.users] + + if str(userid) not in userids: + flask.flash( + 'User does not have commit or cannot loose it right', 'error') + return flask.redirect( + flask.url_for( + '.view_settings', repo=repo.name, username=username) + ) + + for user in repo.users: + if str(user.user.id) == str(userid): + SESSION.delete(user) + break + SESSION.commit() + + progit.generate_gitolite_acls() + + flask.flash('User removed') + return flask.redirect( + flask.url_for('.view_settings', repo=repo.name, username=username) + ) + + +@APP.route('//adduser', methods=('GET', 'POST')) +@APP.route('/fork///adduser', methods=('GET', 'POST')) +@cla_required +def add_user(repo, username=None): + """ Add the specified user from the project. + """ + repo = progit.lib.get_project(SESSION, repo, user=username) + + if not repo: + flask.abort(404, 'Project not found') + + if not is_repo_admin(repo): + flask.abort( + 403, + 'You are not allowed to add users to this project') + + form = progit.forms.AddUserForm() + + if form.validate_on_submit(): + msg = progit.lib.add_user_to_project(SESSION, repo, form.user.data) + + SESSION.commit() + + progit.generate_gitolite_acls() + + flask.flash(msg) + return flask.redirect( + flask.url_for('.view_settings', repo=repo.name, username=username) + ) + + return flask.render_template( + 'add_user.html', + form=form, + username=username, + repo=repo, + )