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

initial commit

# 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.
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``
name = reviewer
email =
- ``gitpush`` does the commit and if possible pushes to server.
server = git://
## 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
# -*- 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) ]
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):
def check_before(self):
return True
def __call__(self, *args, **kwargs):
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()
class git_skip(GitCommand):
class git_add(GitCommand):
git_cmd = ['git', 'add']
def git_args(self, path):
return [path]
class git_add_tree(GitCommand):
git_cmd = ['git', 'add', '-u', ':/']
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)]
class git_commit_index(GitCommit):
class git_commit_tree(GitCommit):
git_cmd = ['git', 'commit']
def check_before(self):
return GitAddTree()
#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, \
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__,
def status():
values = git_status()
return render_template("status.html", status = values)
#def add():
# path = request.values.get('path', None)
# if path:
# rv = git_add(path)
# return "done"
# return "failed"
#def commit():
# #author = request.values.get(
# rv = git_commit()
#['git', 'commit', '--author="John Doe <>"'])
# 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" < /dev/null
def publish(self, target_url, credentials=None, **extra):
status = git_status()
yield '%d fichiers modifiés'
for s in status:
yield str(s)
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"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''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)
def setup_git_bp():
app = current_app
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
##pylint: disable=unused-variable
#def after_request_api(response):
# # add modification to user cookie
# # can use request
# return response
addopts = -v -m server
markers =
testpaths =
import ast
import io
import re
from setuptools import setup, find_packages
with'', 'rt', encoding="utf8") as f:
readme =
_description_re = re.compile(r'description\s+=\s+(?P<description>.*)')
with open('', 'rb') as f:
description = str(ast.literal_eval('utf-8')).group(1)))
author='Pascal Molin',
keywords='Lektor plugin',
# url='[link to your repository]',
'Framework :: Lektor',
'Environment :: Plugins',
'lektor.plugins': [
'commit = lektor_commit:CommitPlugin',
<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;
} input[type="radio"]:checked+label {
background-color: #2b2;
td.ignore input[type="radio"]:checked+label {
background-color: #c33;
<h1>Modifications à publier</h1>
{{ status }}
{% for state, label in status.states %}
<h2> Fichiers {{ label }}</h2>
{% for s in status %}
<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>
{% endfor %}
{% endfor %}
{% endfor %}
<button>save all changes</button>
<button>save selected changes</button>
secret_key = xxx
usersdb = users.yaml
id: admin
username: admin
password: admin
read: ['/'] # read and edit everything
write: ['/']
id: view
username: view
password: view
read: ['/'] # can read everything
write: [] # and write nothing
id: test
username: test
password: test
read: ['/'] # read everything
write: ['/test'] # write under test
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
name = page
label = {{this.title}}
label = title
type = string
label = content
type = markdown
\ No newline at end of file
\ No newline at end of file
\ No newline at end of file
\ No newline at end of file
\ 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