From e7472ff91b3f448c94b387f199de48ed3fc09bca Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Jun 03 2015 14:36:09 +0000 Subject: [PATCH 1/9] Add a form with the minimal information required for online editing --- diff --git a/pagure/forms.py b/pagure/forms.py index 2d4b2d6..98825ba 100644 --- a/pagure/forms.py +++ b/pagure/forms.py @@ -264,3 +264,13 @@ class NewGroupForm(wtf.Form): self.group_type.choices = [ (grptype, grptype) for grptype in kwargs['group_types'] ] + + +class EditFileForm(wtf.Form): + """ Form used to edit a file. """ + content = wtforms.TextAreaField( + 'content', [wtforms.validators.Required()]) + commit_title = wtforms.TextField( + 'Title', [wtforms.validators.Required()]) + commit_message = wtforms.TextAreaField( + 'Commit message', [wtforms.validators.optional()]) From 54335c2d224b66afbb84777dce40c257028b0e9f Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Jun 03 2015 14:36:09 +0000 Subject: [PATCH 2/9] Create the update_file_in_git in pagure.lib.git This method does what it is named for, it updates a specific file with the given content and commit the changes under the specified user. --- diff --git a/pagure/lib/git.py b/pagure/lib/git.py index 2070491..764ac14 100644 --- a/pagure/lib/git.py +++ b/pagure/lib/git.py @@ -649,6 +649,83 @@ def add_file_to_git(repo, issue, ticketfolder, user, filename, filestream): return os.path.join('files', filename) +def update_file_in_git(repo, branch, filename, content, message, user): + ''' Udpate a specific file in the specified repository with the content + given and commit the change under the user's name. + + :arg repo: the Project object from the database + :arg filename: the name of the file to save + :arg content: the new content of the file + :arg message: the message of the git commit + :arg user: the user object with its username and email + + ''' + + # Get the fork + repopath = pagure.get_repo_path(repo) + + # Clone the repo into a temp folder + newpath = tempfile.mkdtemp(prefix='pagure-') + new_repo = pygit2.clone_repository(repopath, newpath) + + file_path = os.path.join(newpath, filename) + + # Get the current index + index = new_repo.index + + # Write down what changed + with open(file_path, 'w') as stream: + stream.write(content.replace('\r', '')) + + # Retrieve the list of files that changed + diff = new_repo.diff() + files = [patch.new_file_path for patch in diff] + + # Add the changes to the index + for filename in files: + index.add(filename) + + # If not change, return + if not files and not added: + shutil.rmtree(newpath) + return + + # See if there is a parent to this commit + branch_ref = get_branch_ref(new_repo, branch) + parent = branch_ref.get_object() + + parents = [] + if parent: + parents.append(parent.hex) + + # Author/commiter will always be this one + author = pygit2.Signature( + name=user.username, + email=user.email + ) + + # Actually commit + new_repo.create_commit( + branch_ref.name, + author, + author, + message.strip(), + new_repo.index.write_tree(), + parents) + index.write() + + # Push to origin + ori_remote = new_repo.remotes[0] + refname = '%s:refs/heads/%s' % (branch_ref.name, branch) + + ori_remote.push(refname) + + # Remove the clone + shutil.rmtree(newpath) + + return os.path.join('files', filename) + + def read_output(cmd, abspath, input=None, keepends=False, **kw): if input: stdin = subprocess.PIPE From 7ff84004ce0f7ea9830817ef03769993a760c14e Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Jun 03 2015 14:36:09 +0000 Subject: [PATCH 3/9] Add a new endpoint to edit file online --- diff --git a/pagure/static/pagure.css b/pagure/static/pagure.css index a75c4be..3ac7fab 100644 --- a/pagure/static/pagure.css +++ b/pagure/static/pagure.css @@ -893,3 +893,25 @@ span.CONFLICTS { #pr_flags table tr{ margin-bottom: 1em; } + +.textAreaLine { + width: 950px; + overflow: hidden; + position: relative; + text-indent: 1em; +} + +.lineNum { + left: 0; + position: absolute; + text-align: right; + top: 0; + width: 27px; + font-size: 92.3%; + line-height: 100%; +} + +#commit { + padding-top: 2em; + padding-left: 2em; +} diff --git a/pagure/templates/edit_file.html b/pagure/templates/edit_file.html new file mode 100644 index 0000000..d8c6bbe --- /dev/null +++ b/pagure/templates/edit_file.html @@ -0,0 +1,112 @@ +{% extends "repo_master.html" %} +{% from "_formhelper.html" import render_field %} + +{% block title %}Edit - {{ repo.name }}{% endblock %} +{%block tag %}home{% endblock %} + + +{% block repo %} + +

+ {{ branchname }}/{% + for file in filename.split('/') %} + {% if loop.first %} + {% set path = file %} + {% else %} + {% set path = path + '/' + file %} + {% endif %} + {% if loop.index != loop.length %}{{ file }}/{% else %}{{ file }}{% endif %} + {% endfor %} +

+ +
+{{ form.csrf_token }} + +
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ +
+ +{% endblock %} + +{% block jscripts %} +{{ super() }} + +{% endblock %} diff --git a/pagure/ui/repo.py b/pagure/ui/repo.py index c55bea8..c9bc121 100644 --- a/pagure/ui/repo.py +++ b/pagure/ui/repo.py @@ -1131,3 +1131,82 @@ def revoke_api_token(repo, token_id, username=None): flask.url_for( '.view_settings', repo=repo.name, username=username) ) + + +@APP.route( + '//edit//f/', + methods=('GET', 'POST')) +@APP.route( + '/fork///edit//f/', + methods=('GET', 'POST')) +@cla_required +def edit_file(repo, branchname, filename, username=None): + """ Edit a file online. + """ + repo = pagure.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') + + reponame = pagure.get_repo_path(repo) + + repo_obj = pygit2.Repository(reponame) + + if repo_obj.is_empty: + flask.abort(404, 'Empty repo cannot have a file') + + branch = None + if branchname in repo_obj.listall_branches(): + branch = repo_obj.lookup_branch(branchname) + commit = branch.get_object() + else: + flask.abort(400, 'Invalid branch specified') + + form = pagure.forms.EditFileForm() + if form.validate_on_submit(): + try: + pagure.lib.git.update_file_in_git( + repo, + branch=branchname, + filename=filename, + content=form.content.data, + message='%s\n\n%s' % ( + form.commit_title.data.strip(), + form.commit_message.data.strip() + ), + user=flask.g.fas_user + ) + + return flask.redirect( + flask.url_for( + '.view_file', repo=repo.name, username=username, + identifier=branchname, filename=filename) + ) + except pagure.exceptions.PagureException as err: # pragma: no cover + APP.logger.exception(err) + flask.flash('Commit could not be done', 'error') + data = form.content.data + elif flask.request.method == 'GET': + content = __get_file_in_tree( + repo_obj, commit.tree, filename.split('/')) + if not content or isinstance(content, pygit2.Tree): + flask.abort(404, 'File not found') + data = repo_obj[content.oid].data + else: + data = form.content.data + + return flask.render_template( + 'edit_file.html', + select='tree', + repo=repo, + username=username, + branchname=branchname, + data=data, + filename=filename, + form=form, + ) From 98ed604ce529630992a4684641b97bfd564e99db Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Jun 03 2015 14:36:09 +0000 Subject: [PATCH 4/9] Place the button to edit files online in the file view --- diff --git a/pagure/templates/file.html b/pagure/templates/file.html index 3364c0e..e7554ff 100644 --- a/pagure/templates/file.html +++ b/pagure/templates/file.html @@ -29,6 +29,11 @@
    + {% if output_type=='file' %} +
  • Edit
  • + {% endif %}
  • Blob
  • From fbc679b7dfa55f5c444a53477dcd733ced41baad Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Jun 03 2015 14:36:09 +0000 Subject: [PATCH 5/9] Inform the user that the changes were committed fine --- diff --git a/pagure/ui/repo.py b/pagure/ui/repo.py index c9bc121..665bfd2 100644 --- a/pagure/ui/repo.py +++ b/pagure/ui/repo.py @@ -1181,7 +1181,7 @@ def edit_file(repo, branchname, filename, username=None): ), user=flask.g.fas_user ) - + flask.flash('Changes committed') return flask.redirect( flask.url_for( '.view_file', repo=repo.name, username=username, From b196065aa567d21b5c16abb67f0a51b3b71c87cc Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Jun 03 2015 14:36:09 +0000 Subject: [PATCH 6/9] If the content if binary, we cannot edit it online --- diff --git a/pagure/ui/repo.py b/pagure/ui/repo.py index 665bfd2..04e4546 100644 --- a/pagure/ui/repo.py +++ b/pagure/ui/repo.py @@ -1196,6 +1196,8 @@ def edit_file(repo, branchname, filename, username=None): repo_obj, commit.tree, filename.split('/')) if not content or isinstance(content, pygit2.Tree): flask.abort(404, 'File not found') + if content.is_binary: + flask.abort(400, 'Cannot edit binary files') data = repo_obj[content.oid].data else: data = form.content.data From 39d6c038c9395f6d7ca282b95a0fb4a16d3abc27 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Chibon Date: Jun 03 2015 14:36:10 +0000 Subject: [PATCH 7/9] Add unit-tests for the edit_file endpoint --- diff --git a/tests/test_progit_flask_ui_repo.py b/tests/test_progit_flask_ui_repo.py index 1c0d372..6e8a53f 100644 --- a/tests/test_progit_flask_ui_repo.py +++ b/tests/test_progit_flask_ui_repo.py @@ -1577,6 +1577,130 @@ index 0000000..fb7093d self.assertIn('', output.data) self.assertTrue(output.data.count('tagid'), 1) + def test_edit_file(self): + """ Test the edit_file endpoint. """ + + output = self.app.get('/foo/edit/foo/f/sources') + self.assertEqual(output.status_code, 302) + + user = tests.FakeUser() + with tests.user_set(pagure.APP, user): + # No project registered in the DB + output = self.app.get('/foo/edit/foo/f/sources') + self.assertEqual(output.status_code, 404) + + tests.create_projects(self.session) + + # No a repo admin + output = self.app.get('/test/edit/foo/f/sources') + self.assertEqual(output.status_code, 403) + + user.username = 'pingou' + with tests.user_set(pagure.APP, user): + + # No associated git repo + output = self.app.get('/test/edit/foo/f/sources') + self.assertEqual(output.status_code, 404) + + tests.create_projects_git(tests.HERE, bare=True) + + output = self.app.get('/test/edit/foo/f/sources') + self.assertEqual(output.status_code, 404) + + # Add some content to the git repo + tests.add_content_git_repo(os.path.join(tests.HERE, 'test.git')) + tests.add_readme_git_repo(os.path.join(tests.HERE, 'test.git')) + tests.add_binary_git_repo( + os.path.join(tests.HERE, 'test.git'), 'test.jpg') + tests.add_binary_git_repo( + os.path.join(tests.HERE, 'test.git'), 'test_binary') + + output = self.app.get('/test/edit/master/foofile') + self.assertEqual(output.status_code, 404) + + # Edit page + output = self.app.get('/test/edit/master/f/sources') + self.assertEqual(output.status_code, 200) + self.assertIn( + 'master/sources', + output.data) + self.assertIn( + '