Commit 68e70c25 authored by Pascal's avatar Pascal
Browse files

initial commit

parents
dist
build
*.pyc
*.pyo
*.egg-info
# lektor-commit
Add minimal `git` facilities to lektor development server.
This plugin requires ``lektor-auth`` to obtain username.
## Commit, Push
By default, a ``commit`` includes the entire directory state, adding
untracked files and removing deleted ones. In git language:
```
git add -u :/
git commit --author=<authuser> -m 'interface commit, user <username>'
```
A ``push`` can be done to a remote server if there is no conflict.
following https://stackoverflow.com/questions/501407/is-there-a-git-merge-dry-run-option
it first checks there is no merge conflict
```
git fetch origin master
git merge-tree `git merge-base FETCH_HEAD master` FETCH_HEAD master
```
and eventually runs
```
git pull origin master
git push origin master
```
## Publishing methods
Two publishing methods are added:
- ``askreview`` performs a commit and then sends an email to ask for review.
The email address has to be configured in ``configs/commit.ini``
```
[askreview]
name = reviewer
email = moderator@review.com
```
- ``gitpush`` does the commit and if possible pushes to server.
```
[gitpush]
server = git://remote.com/lektor-repo
```
## Further configuration
The following is not yet available and only partially implemented.
When a review is requested, the commit should consist only
in files modified by the user, logged for example in the user cookie.
A push should never generate a conflict.
The plugin provides a status page, giving access to manual commit of selected
files.
# -*- coding: utf-8 -*-
import shlex
import subprocess
def git_decode(letter):
code = {
' ': 'unmodified',
'M': 'modified',
'A': 'added',
'D': 'deleted',
'U': 'unmerged',
'?': 'untracked'
}
return code.get(letter, None)
class PathStatus:
"""
File status as output by git status --porcelain
TODO: consider using porcelain=v2
"""
def __init__(self, line, number):
self.number = number
self.line = line
self.index = git_decode(line[0])
self.tree = git_decode(line[1])
self.path = line[3:]
def __repr__(self):
return self.line
class Status(list):
states = [ ('changed', 'modifiés non publiés'),
('saved', 'validés pour publication') ]
actions = [ ('save', 'valider'),
('skip', 'ignorer'),
('discard', 'abandonner') ]
def __init__(self, gitstatus):
files = [ PathStatus(l,i) for i,l in enumerate(gitstatus) ]
#super().__init__()
super().__init__(files)
class GitCommand(object):
"""
TODO: consider turning this class
into a function,
using __new__(*args)
instead of __call__(self, *args)
"""
git_cmd = []
def git_args(self, *args):
return []
def result(self, rv):
return rv.returncode == 0
def before(self):
pass
def check_before(self):
return True
def __call__(self, *args, **kwargs):
self.before()
assert self.check_before()
args = self.git_cmd + self.git_args(*args)
rv = subprocess.check_output(args)
return self.result(rv)
def instantiate(cls):
return cls()
@instantiate
class git_skip(GitCommand):
pass
@instantiate
class git_add(GitCommand):
git_cmd = ['git', 'add']
def git_args(self, path):
return [path]
@instantiate
class git_add_tree(GitCommand):
git_cmd = ['git', 'add', '-u', ':/']
@instantiate
class git_status(GitCommand):
git_cmd = ['git', 'status', '--porcelain']
def result(self, rv):
return Status(rv.decode('utf-8').splitlines())
class GitCommit(GitCommand):
git_cmd = ['git', 'commit']
def git_args(self, message, author):
author = shlex.quote(author)
return ['-m', message, '--author="%s"'%(author)]
@instantiate
class git_commit_index(GitCommit):
pass
@instantiate
class git_commit_tree(GitCommit):
git_cmd = ['git', 'commit']
def check_before(self):
return GitAddTree()
#@instantiate
#class GitCheckUpstream(GitCommand):
# """
# git fetch origin master
# git merge-tree `git merge-base FETCH_HEAD master` FETCH_HEAD master
# """
# branch = 'master'
# origin = 'origin'
# class GitMergeBase(GitCommand):
# git_cmd = ['git', 'merge-base', 'FETCH_HEAD', branch]
# class GitMergeTree(GitCommand):
# git_cmd = ['git', 'merge-tree']
# def git_args(self, sha):
# sha = merge_base()
# return []
# fetch = GitFetch()
# merge_base = GitMergeBase()
# merge_tree = GitMergeTree()
# def __call__(self, *args):
# self.fetch()
# sha = self.merge_base()
# return self.merge_tree(sha)
# -*- coding: utf-8 -*-
import os
import shutil
import subprocess
from flask import Blueprint, \
current_app, \
render_template, \
url_for, \
Markup, \
has_app_context, \
has_request_context
from lektor.pluginsystem import Plugin
from lektor.publisher import Publisher
from lektor.pluginsystem import get_plugin
from lektor.admin.modules import serve, api
from git import git_add, git_status, git_commit_tree
gitbp = Blueprint('git', __name__,
url_prefix='/git',
static_folder='static',
template_folder='templates'
)
@gitbp.route('/status',methods=['GET'])
def status():
values = git_status()
return render_template("status.html", status = values)
#@gitbp.route('/add',methods=['POST'])
#def add():
# path = request.values.get('path', None)
# if path:
# rv = git_add(path)
# return "done"
# return "failed"
#
#@gitbp.route('/commit',methods=['POST'])
#def commit():
# #author = request.values.get(
# rv = git_commit()
# #subprocess.call(['git', 'commit', '--author="John Doe <john@doe.org>"'])
# return "failed"
class CommitPublisher(Publisher):
"""
commit all changes from current tree
get username from session cookie
"""
def publish(self, target_url, credentials=None, **extra):
rv = git_commit_tree()
yield rv
class AskReviewPublisher(CommitPublisher):
"""
commit changes and ask for review before push
"""
def send_mail(self, status):
#mail -s "Test" pascal.molin@math.univ-paris-diderot.fr < /dev/null
pass
def publish(self, target_url, credentials=None, **extra):
status = git_status()
yield '%d fichiers modifiés'
for s in status:
yield str(s)
self.send_mail(status)
yield 'Message envoyé'
yield 'Les changements seront publiés après validation.'
class GitPushPublisher(CommitPublisher):
"""
commit modified files and push
caveat: the publish method does not have access to the request
which triggered it, hence the publisher name.
As a tentative workaround, we let flask store the user associated
to the last valid /publish request
"""
def publish(self, target_url, credentials=None, **extra):
#src_path = self.output_path
#dst_path = target_url.path
#self.fail("Vous n'avez pas le droit de publier directement, \
# sélectionnez plutôt «signaler les changements pour publication»")
app = current_app
login = get_plugin('login', self.env)
with app.app_context():
user = login.get_auth_user()
if user is not None and user.get('publish',False):
msg = 'user %s'%user['username']
yield msg
yield 'Done'
self.fail('Please commit all changes before publishing')
class CommitPlugin(Plugin):
name = 'lektor-commit'
description = u'Publish by git commit+push, or ask review.'
def on_setup_env(self, *args, **extra):
self.env.add_publisher('askreview', AskReviewPublisher)
self.env.add_publisher('gitpush', GitPushPublisher)
@serve.bp.before_app_first_request
def setup_git_bp():
app = current_app
app.register_blueprint(gitbp)
login = get_plugin('login', self.env)
login.add_button( url_for('git.status'),
'see all changes',
Markup('<i class="fa fa-save"></i>'))
## use to add/commit after each use
#@api.bp.after_request
##pylint: disable=unused-variable
#def after_request_api(response):
# # add modification to user cookie
# # can use request
# return response
[bdist_wheel]
universal=1
[tool:pytest]
addopts = -v -m server
markers =
server
testpaths =
test
import ast
import io
import re
from setuptools import setup, find_packages
with io.open('README.md', 'rt', encoding="utf8") as f:
readme = f.read()
_description_re = re.compile(r'description\s+=\s+(?P<description>.*)')
with open('lektor_commit.py', 'rb') as f:
description = str(ast.literal_eval(_description_re.search(
f.read().decode('utf-8')).group(1)))
setup(
author='Pascal Molin',
author_email='molin.maths@gmail.com',
description=description,
keywords='Lektor plugin',
license='MIT',
long_description=readme,
long_description_content_type='text/markdown',
name='lektor-commit',
packages=find_packages(),
py_modules=['lektor_commit'],
# url='[link to your repository]',
version='0.1',
classifiers=[
'Framework :: Lektor',
'Environment :: Plugins',
],
entry_points={
'lektor.plugins': [
'commit = lektor_commit:CommitPlugin',
]
}
)
<form>
<style scoped>
/*
td input[type="radio"] {
display: none;
}
*/
td label {
display: inline-block;
background-color: #ddd;
padding: 4px 11px;
font-family: Arial;
font-size: 16px;
cursor: pointer;
}
td.save input[type="radio"]:checked+label {
background-color: #2b2;
}
td.ignore input[type="radio"]:checked+label {
background-color: #c33;
}
</style>
<h1>Modifications à publier</h1>
{{ status }}
{% for state, label in status.states %}
<h2> Fichiers {{ label }}</h2>
<table>
<tr>
<th>statut</th>
<th>fichier</th>
<th>publier</th>
<th>ignorer</th>
</tr>
{% for s in status %}
<tr>
<td>{{ s.index }}</td>
<td><input type="hidden" value="{{ s.path }}"/><code>{{ s.path }}</code></td>
{% for a,l in status.actions %}
<td class="{{ a }}">
<input id="id-{{ s.number }}-{{ a }}" type="radio" name="file-{{ s.number }}" value="{{ a }}">
<label for="id-{{ s.number }}-{{ a }}">{{ l }}</label>
</td>
{% endfor %}
</tr>
{% endfor %}
</table>
{% endfor %}
<button>save all changes</button>
<button>save selected changes</button>
</form>
secret_key = xxx
usersdb = users.yaml
admin:
id: admin
username: admin
password: admin
read: ['/'] # read and edit everything
write: ['/']
view:
id: view
username: view
password: view
read: ['/'] # can read everything
write: [] # and write nothing
test:
id: test
username: test
password: test
read: ['/'] # read everything
write: ['/test'] # write under test
blog:
id: blog
username: blog
password: blog
read: ['/blog'] # read and edit only onder blog
write: ['/blog']
title: blog page
---
content: original content
_model: page
---
title: top page
---
content: top content
_model: page
---
title: blog
---
content: toplevel blog entry
_model: page
---
title: post
---
content: data
[model]
name = page
label = {{this.title}}
[fields.title]
label = title
type = string
[fields.content]
label = content
type = markdown
../../../../README.md
\ No newline at end of file
../../../../__init__.py
\ No newline at end of file
../../../../git.py
\ No newline at end of file
../../../../lektor_commit.py
\ No newline at end of file
../../../../setup.cfg
\ No newline at end of file
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment