git plugin: addressed review + misc improvments
* fixed annoying recursion in exception handlers (fake_env and sand_env) * "pubs git" always not quiet * color option for git ouput through "pubs git" * "pubs git" output without any "info:" prefix or extraneous new line. * is_loaded() method for plugins
This commit is contained in:
parent
439b941de6
commit
e4665f734a
@ -96,11 +96,24 @@ active = force_list(default=list('alias'))
|
|||||||
# description = lists number of pubs in repo
|
# description = lists number of pubs in repo
|
||||||
|
|
||||||
[[git]]
|
[[git]]
|
||||||
# the plugin allows to use `pubs git` and commit changes automatically
|
# The git plugin will commit changes to the repository in a git repository
|
||||||
# if False, will display git output when invoked
|
# created at the root of the pubs directory. All detected changes will be
|
||||||
|
# commited every time a change is made by a pubs command.
|
||||||
|
# The plugin also propose the `pubs git` subcommand, to directory send git
|
||||||
|
# command to the pubs repository. Therefore, `pubs git status` is equivalent
|
||||||
|
# to `git -C <pubsdir> status`, with the `-C` flag instructing
|
||||||
|
# to invoke git as if the current directory was <pubsdir>. Note that a
|
||||||
|
# limitation of the subcommand is that you cannot use git commands with the
|
||||||
|
# `-c` option (pubs will interpret it first.)
|
||||||
|
|
||||||
|
# if False, will display git output when automatic commit are made.
|
||||||
|
# Invocation of `pubs git` will always have output displayed.
|
||||||
quiet = boolean(default=True)
|
quiet = boolean(default=True)
|
||||||
# if True, git will not automatically commit changes
|
# if True, git will not automatically commit changes
|
||||||
manual = boolean(default=False)
|
manual = boolean(default=False)
|
||||||
|
# if True, color will be conserved from git output (this add `-c color:always`
|
||||||
|
# to the git invocation).
|
||||||
|
force_color = boolean(default=True)
|
||||||
|
|
||||||
|
|
||||||
[internal]
|
[internal]
|
||||||
|
@ -48,27 +48,27 @@ class PaperChangeEvent(Event):
|
|||||||
|
|
||||||
# Used by repo.push_paper()
|
# Used by repo.push_paper()
|
||||||
class AddEvent(PaperChangeEvent):
|
class AddEvent(PaperChangeEvent):
|
||||||
_format = "Adds paper {citekey}."
|
_format = "Added paper {citekey}."
|
||||||
|
|
||||||
# Used by repo.push_doc()
|
# Used by repo.push_doc()
|
||||||
class DocAddEvent(PaperChangeEvent):
|
class DocAddEvent(PaperChangeEvent):
|
||||||
_format = "Adds document for {citekey}."
|
_format = "Added document for {citekey}."
|
||||||
|
|
||||||
# Used by repo.remove_paper()
|
# Used by repo.remove_paper()
|
||||||
class RemoveEvent(PaperChangeEvent):
|
class RemoveEvent(PaperChangeEvent):
|
||||||
_format = "Removes paper for {citekey}."
|
_format = "Removed paper for {citekey}."
|
||||||
|
|
||||||
# Used by repo.remove_doc()
|
# Used by repo.remove_doc()
|
||||||
class DocRemoveEvent(PaperChangeEvent):
|
class DocRemoveEvent(PaperChangeEvent):
|
||||||
_format = "Removes document for {citekey}."
|
_format = "Removed document for {citekey}."
|
||||||
|
|
||||||
# Used by commands.tag_cmd.command()
|
# Used by commands.tag_cmd.command()
|
||||||
class TagEvent(PaperChangeEvent):
|
class TagEvent(PaperChangeEvent):
|
||||||
_format = "Updates tags for {citekey}."
|
_format = "Updated tags for {citekey}."
|
||||||
|
|
||||||
# Used by commands.edit_cmd.command()
|
# Used by commands.edit_cmd.command()
|
||||||
class ModifyEvent(PaperChangeEvent):
|
class ModifyEvent(PaperChangeEvent):
|
||||||
_format = "Modifies {file_type} file of {citekey}."
|
_format = "Modified {file_type} file of {citekey}."
|
||||||
|
|
||||||
def __init__(self, citekey, file_type):
|
def __init__(self, citekey, file_type):
|
||||||
super(ModifyEvent, self).__init__(citekey)
|
super(ModifyEvent, self).__init__(citekey)
|
||||||
@ -80,7 +80,7 @@ class ModifyEvent(PaperChangeEvent):
|
|||||||
|
|
||||||
# Used by repo.rename_paper()
|
# Used by repo.rename_paper()
|
||||||
class RenameEvent(PaperChangeEvent):
|
class RenameEvent(PaperChangeEvent):
|
||||||
_format = "Renames paper {old_citekey} to {citekey}."
|
_format = "Renamed paper {old_citekey} to {citekey}."
|
||||||
|
|
||||||
def __init__(self, paper, old_citekey):
|
def __init__(self, paper, old_citekey):
|
||||||
super(RenameEvent, self).__init__(paper.citekey)
|
super(RenameEvent, self).__init__(paper.citekey)
|
||||||
@ -93,4 +93,4 @@ class RenameEvent(PaperChangeEvent):
|
|||||||
|
|
||||||
# Used by commands.note_cmd.command()
|
# Used by commands.note_cmd.command()
|
||||||
class NoteEvent(PaperChangeEvent):
|
class NoteEvent(PaperChangeEvent):
|
||||||
_format = "Modifies note {citekey}."
|
_format = "Modified note of {citekey}."
|
||||||
|
@ -27,6 +27,10 @@ class PapersPlugin(object):
|
|||||||
else:
|
else:
|
||||||
raise RuntimeError("{} instance not created".format(cls.__name__))
|
raise RuntimeError("{} instance not created".format(cls.__name__))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def is_loaded(cls):
|
||||||
|
return cls in _instances
|
||||||
|
|
||||||
|
|
||||||
def load_plugins(conf, ui):
|
def load_plugins(conf, ui):
|
||||||
"""Imports the modules for a sequence of plugin names. Each name
|
"""Imports the modules for a sequence of plugin names. Each name
|
||||||
|
@ -1,14 +1,16 @@
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import argparse
|
import argparse
|
||||||
from subprocess import Popen, PIPE
|
from subprocess import Popen, PIPE, STDOUT
|
||||||
from pipes import quote as shell_quote
|
from pipes import quote as shell_quote
|
||||||
|
|
||||||
|
from ... import uis
|
||||||
from ...plugins import PapersPlugin
|
from ...plugins import PapersPlugin
|
||||||
from ...events import PaperChangeEvent, PostCommandEvent
|
from ...events import PaperChangeEvent, PostCommandEvent
|
||||||
|
|
||||||
|
|
||||||
GITIGNORE = """# files or directories for the git plugin to ignore
|
GITIGNORE = """# files or directories for the git plugin to ignore
|
||||||
|
.gitignore
|
||||||
.cache/
|
.cache/
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -26,8 +28,9 @@ class GitPlugin(PapersPlugin):
|
|||||||
|
|
||||||
def __init__(self, conf, ui):
|
def __init__(self, conf, ui):
|
||||||
self.ui = ui
|
self.ui = ui
|
||||||
self.pubsdir = conf['main']['pubsdir']
|
self.pubsdir = os.path.expanduser(conf['main']['pubsdir'])
|
||||||
self.manual = conf['plugins'].get('git', {}).get('manual', False)
|
self.manual = conf['plugins'].get('git', {}).get('manual', False)
|
||||||
|
self.force_color = conf['plugins'].get('git', {}).get('force_color', True)
|
||||||
self.quiet = conf['plugins'].get('git', {}).get('quiet', True)
|
self.quiet = conf['plugins'].get('git', {}).get('quiet', True)
|
||||||
self.list_of_changes = []
|
self.list_of_changes = []
|
||||||
self._gitinit()
|
self._gitinit()
|
||||||
@ -35,13 +38,16 @@ class GitPlugin(PapersPlugin):
|
|||||||
def _gitinit(self):
|
def _gitinit(self):
|
||||||
"""Initialize the git repository if necessary."""
|
"""Initialize the git repository if necessary."""
|
||||||
# check that a `.git` directory is present in the pubs dir
|
# check that a `.git` directory is present in the pubs dir
|
||||||
git_path = os.path.expanduser(os.path.join(self.pubsdir, '.git'))
|
git_path = os.path.join(self.pubsdir, '.git')
|
||||||
if not os.path.isdir(git_path):
|
if not os.path.isdir(git_path):
|
||||||
|
try:
|
||||||
self.shell('init')
|
self.shell('init')
|
||||||
|
except RuntimeError as exc:
|
||||||
|
self.ui.error(exc.args[0])
|
||||||
|
sys.exit(1)
|
||||||
# check that a `.gitignore` file is present
|
# check that a `.gitignore` file is present
|
||||||
gitignore_path = os.path.expanduser(os.path.join(self.pubsdir, '.gitignore'))
|
gitignore_path = os.path.join(self.pubsdir, '.gitignore')
|
||||||
if not os.path.isfile(gitignore_path):
|
if not os.path.isfile(gitignore_path):
|
||||||
print('bla')
|
|
||||||
with open(gitignore_path, 'w') as fd:
|
with open(gitignore_path, 'w') as fd:
|
||||||
fd.write(GITIGNORE)
|
fd.write(GITIGNORE)
|
||||||
|
|
||||||
@ -55,25 +61,30 @@ class GitPlugin(PapersPlugin):
|
|||||||
|
|
||||||
def command(self, conf, args):
|
def command(self, conf, args):
|
||||||
"""Execute a git command in the pubs directory"""
|
"""Execute a git command in the pubs directory"""
|
||||||
self.shell(' '.join([shell_quote(a) for a in args.arguments]))
|
self.shell(' '.join([shell_quote(a) for a in args.arguments]), command=True)
|
||||||
|
|
||||||
def shell(self, cmd, input_stdin=None):
|
def shell(self, cmd, input_stdin=None, command=False):
|
||||||
"""Runs the git program in a shell
|
"""Runs the git program in a shell
|
||||||
|
|
||||||
:param cmd: the git command, and all arguments, as a single string (e.g. 'add .')
|
:param cmd: the git command, and all arguments, as a single string (e.g. 'add .')
|
||||||
:param input_stdin: if Python 3, must be bytes (i.e., from str, s.encode('utf-8'))
|
:param input_stdin: if Python 3, must be bytes (i.e., from str, s.encode('utf-8'))
|
||||||
|
:param command: if True, we're dealing with an explicit `pubs git` invocation.
|
||||||
"""
|
"""
|
||||||
git_cmd = 'git -C {} {}'.format(self.pubsdir, cmd)
|
colorize = ' -c color.ui=always' if self.force_color else ''
|
||||||
p = Popen(git_cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE, shell=True)
|
git_cmd = 'git -C {}{} {}'.format(self.pubsdir, colorize, cmd)
|
||||||
|
#print(git_cmd)
|
||||||
|
p = Popen(git_cmd, stdin=PIPE, stdout=PIPE, stderr=STDOUT, shell=True)
|
||||||
output, err = p.communicate(input_stdin)
|
output, err = p.communicate(input_stdin)
|
||||||
p.wait()
|
p.wait()
|
||||||
|
|
||||||
if p.returncode != 0:
|
if p.returncode != 0:
|
||||||
msg = ('The git plugin encountered an error when running the git command:\n' +
|
raise RuntimeError('The git plugin encountered an error when running the git command:\n' +
|
||||||
'{}\n{}\n'.format(git_cmd, err.decode('utf-8')) +
|
'{}\n\nReturned output:\n{}\n'.format(git_cmd, output.decode('utf-8')) +
|
||||||
'You may fix the state of the git repository {} manually.\n'.format(self.pubsdir) +
|
'If needed, you may fix the state of the {} git repository '.format(self.pubsdir) +
|
||||||
'If relevant, you may submit a bug report at ' +
|
'manually.\nIf relevant, you may submit a bug report at ' +
|
||||||
'https://github.com/pubs/pubs/issues')
|
'https://github.com/pubs/pubs/issues')
|
||||||
self.ui.warning(msg)
|
elif command:
|
||||||
|
self.ui.message(output.decode('utf-8'), end='')
|
||||||
elif not self.quiet:
|
elif not self.quiet:
|
||||||
self.ui.info(output.decode('utf-8'))
|
self.ui.info(output.decode('utf-8'))
|
||||||
return output, err, p.returncode
|
return output, err, p.returncode
|
||||||
@ -82,18 +93,17 @@ class GitPlugin(PapersPlugin):
|
|||||||
@PaperChangeEvent.listen()
|
@PaperChangeEvent.listen()
|
||||||
def paper_change_event(event):
|
def paper_change_event(event):
|
||||||
"""When a paper is changed, commit the changes to the directory."""
|
"""When a paper is changed, commit the changes to the directory."""
|
||||||
try:
|
if GitPlugin.is_loaded():
|
||||||
git = GitPlugin.get_instance()
|
git = GitPlugin.get_instance()
|
||||||
if not git.manual:
|
if not git.manual:
|
||||||
event_desc = event.description
|
event_desc = event.description
|
||||||
for a, b in [('\\','\\\\'), ('"','\\"'), ('$','\\$'), ('`','\\`')]:
|
for a, b in [('\\','\\\\'), ('"','\\"'), ('$','\\$'), ('`','\\`')]:
|
||||||
event_desc = event_desc.replace(a, b)
|
event_desc = event_desc.replace(a, b)
|
||||||
git.list_of_changes.append(event_desc)
|
git.list_of_changes.append(event_desc)
|
||||||
except RuntimeError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@PostCommandEvent.listen()
|
@PostCommandEvent.listen()
|
||||||
def git_commit(event):
|
def git_commit(event):
|
||||||
|
if GitPlugin.is_loaded():
|
||||||
try:
|
try:
|
||||||
git = GitPlugin.get_instance()
|
git = GitPlugin.get_instance()
|
||||||
if len(git.list_of_changes) > 0:
|
if len(git.list_of_changes) > 0:
|
||||||
@ -103,5 +113,5 @@ def git_commit(event):
|
|||||||
|
|
||||||
git.shell('add .')
|
git.shell('add .')
|
||||||
git.shell('commit -F-', message.encode('utf-8'))
|
git.shell('commit -F-', message.encode('utf-8'))
|
||||||
except RuntimeError:
|
except RuntimeError as exc:
|
||||||
pass
|
uis.get_ui().warning(exc.args[0])
|
||||||
|
13
pubs/uis.py
13
pubs/uis.py
@ -105,6 +105,19 @@ class PrintUI(object):
|
|||||||
self.exit()
|
self.exit()
|
||||||
return True # never happens
|
return True # never happens
|
||||||
|
|
||||||
|
def test_handle_exception(self, exc):
|
||||||
|
"""Attempts to handle exception.
|
||||||
|
|
||||||
|
:returns: True if exception has been handled (currently never happens)
|
||||||
|
"""
|
||||||
|
self.error(ustr(exc))
|
||||||
|
if DEBUG or self.debug:
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
self.exit()
|
||||||
|
return True # never happens
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class InputUI(PrintUI):
|
class InputUI(PrintUI):
|
||||||
"""UI class. Stores configuration parameters and system information.
|
"""UI class. Stores configuration parameters and system information.
|
||||||
|
@ -9,7 +9,7 @@ import dotdot
|
|||||||
from pyfakefs import fake_filesystem, fake_filesystem_unittest
|
from pyfakefs import fake_filesystem, fake_filesystem_unittest
|
||||||
|
|
||||||
from pubs.p3 import input, _fake_stdio, _get_fake_stdio_ucontent
|
from pubs.p3 import input, _fake_stdio, _get_fake_stdio_ucontent
|
||||||
from pubs import content, filebroker
|
from pubs import content, filebroker, uis
|
||||||
|
|
||||||
# code for fake fs
|
# code for fake fs
|
||||||
|
|
||||||
@ -20,6 +20,8 @@ real_shutil = shutil
|
|||||||
real_glob = glob
|
real_glob = glob
|
||||||
real_io = io
|
real_io = io
|
||||||
|
|
||||||
|
original_exception_handler = uis.InputUI.handle_exception
|
||||||
|
|
||||||
|
|
||||||
# capture output
|
# capture output
|
||||||
|
|
||||||
@ -70,6 +72,7 @@ class FakeInput():
|
|||||||
self.inputs = list(inputs) or []
|
self.inputs = list(inputs) or []
|
||||||
self.module_list = module_list
|
self.module_list = module_list
|
||||||
self._cursor = 0
|
self._cursor = 0
|
||||||
|
self._original_handler = None
|
||||||
|
|
||||||
def as_global(self):
|
def as_global(self):
|
||||||
for md in self.module_list:
|
for md in self.module_list:
|
||||||
@ -78,13 +81,11 @@ class FakeInput():
|
|||||||
md.InputUI.editor_input = self
|
md.InputUI.editor_input = self
|
||||||
md.InputUI.edit_file = self.input_to_file
|
md.InputUI.edit_file = self.input_to_file
|
||||||
# Do not catch UnexpectedInput
|
# Do not catch UnexpectedInput
|
||||||
original_handler = md.InputUI.handle_exception
|
|
||||||
|
|
||||||
def handler(ui, exc):
|
def handler(ui, exc):
|
||||||
if isinstance(exc, self.UnexpectedInput):
|
if isinstance(exc, self.UnexpectedInput):
|
||||||
raise
|
raise
|
||||||
else:
|
else:
|
||||||
original_handler(ui, exc)
|
original_exception_handler(ui, exc)
|
||||||
|
|
||||||
md.InputUI.handle_exception = handler
|
md.InputUI.handle_exception = handler
|
||||||
|
|
||||||
|
@ -8,15 +8,17 @@ import unittest
|
|||||||
|
|
||||||
import six
|
import six
|
||||||
|
|
||||||
from pubs import pubs_cmd, color, content, uis, p3
|
from pubs import pubs_cmd, color, content, uis, p3, events
|
||||||
from pubs.config import conf
|
from pubs.config import conf
|
||||||
from pubs.p3 import _fake_stdio, _get_fake_stdio_ucontent
|
from pubs.p3 import _fake_stdio, _get_fake_stdio_ucontent
|
||||||
|
|
||||||
|
|
||||||
# makes the tests very noisy
|
# makes the tests very noisy
|
||||||
PRINT_OUTPUT = True
|
PRINT_OUTPUT = False
|
||||||
CAPTURE_OUTPUT = True
|
CAPTURE_OUTPUT = True
|
||||||
|
|
||||||
|
original_exception_handler = uis.InputUI.handle_exception
|
||||||
|
|
||||||
|
|
||||||
class FakeSystemExit(Exception):
|
class FakeSystemExit(Exception):
|
||||||
"""\
|
"""\
|
||||||
@ -71,7 +73,6 @@ class FakeInput():
|
|||||||
input() returns 'no'
|
input() returns 'no'
|
||||||
input() raises IndexError
|
input() raises IndexError
|
||||||
"""
|
"""
|
||||||
|
|
||||||
class UnexpectedInput(Exception):
|
class UnexpectedInput(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -87,14 +88,11 @@ class FakeInput():
|
|||||||
md.InputUI.editor_input = self
|
md.InputUI.editor_input = self
|
||||||
md.InputUI.edit_file = self.input_to_file
|
md.InputUI.edit_file = self.input_to_file
|
||||||
|
|
||||||
# Do not catch UnexpectedInput
|
|
||||||
original_handler = md.InputUI.handle_exception
|
|
||||||
|
|
||||||
def handler(ui, exc):
|
def handler(ui, exc):
|
||||||
if isinstance(exc, self.UnexpectedInput):
|
if isinstance(exc, self.UnexpectedInput):
|
||||||
raise
|
raise
|
||||||
else:
|
else:
|
||||||
original_handler(ui, exc)
|
original_exception_handler(ui, exc)
|
||||||
|
|
||||||
md.InputUI.handle_exception = handler
|
md.InputUI.handle_exception = handler
|
||||||
|
|
||||||
@ -141,7 +139,6 @@ class SandboxedCommandTestCase(unittest.TestCase):
|
|||||||
def _preprocess_cmd(self, cmd):
|
def _preprocess_cmd(self, cmd):
|
||||||
"""Sandbox the pubs command into a temporary directory"""
|
"""Sandbox the pubs command into a temporary directory"""
|
||||||
cmd_chunks = cmd.split(' ')
|
cmd_chunks = cmd.split(' ')
|
||||||
print(cmd, cmd_chunks[0], 'pubs')
|
|
||||||
assert cmd_chunks[0] == 'pubs'
|
assert cmd_chunks[0] == 'pubs'
|
||||||
prefix = ['pubs', '-c', self.default_conf_path]
|
prefix = ['pubs', '-c', self.default_conf_path]
|
||||||
if cmd_chunks[1] == 'init':
|
if cmd_chunks[1] == 'init':
|
||||||
|
@ -59,20 +59,32 @@ class TestGitPlugin(sand_env.SandboxedCommandTestCase):
|
|||||||
self.assertEqual(hash_g, hash_h)
|
self.assertEqual(hash_g, hash_h)
|
||||||
self.assertNotEqual(hash_h, hash_i)
|
self.assertNotEqual(hash_h, hash_i)
|
||||||
|
|
||||||
conf = config.load_conf(path=self.default_conf_path)
|
# # basically can't test that because each command is not completely independent in
|
||||||
conf['plugins']['active'] = []
|
# # SandoboxedCommands.
|
||||||
config.save_conf(conf, path=self.default_conf_path)
|
# # will work if we use subprocess.
|
||||||
|
# conf = config.load_conf(path=self.default_conf_path)
|
||||||
self.execute_cmds([('pubs add data/pagerank.bib',)])
|
# conf['plugins']['active'] = []
|
||||||
hash_j = git_hash(self.default_pubs_dir)
|
# config.save_conf(conf, path=self.default_conf_path)
|
||||||
|
#
|
||||||
self.assertEqual(hash_i, hash_j)
|
# self.execute_cmds([('pubs add data/pagerank.bib',)])
|
||||||
|
# hash_j = git_hash(self.default_pubs_dir)
|
||||||
|
#
|
||||||
|
# self.assertEqual(hash_i, hash_j)
|
||||||
|
|
||||||
|
def test_manual(self):
|
||||||
conf = config.load_conf(path=self.default_conf_path)
|
conf = config.load_conf(path=self.default_conf_path)
|
||||||
conf['plugins']['active'] = ['git']
|
conf['plugins']['active'] = ['git']
|
||||||
conf['plugins']['git']['manual'] = True
|
conf['plugins']['git']['manual'] = True
|
||||||
config.save_conf(conf, path=self.default_conf_path)
|
config.save_conf(conf, path=self.default_conf_path)
|
||||||
|
|
||||||
|
# this three lines just to initialize the git HEAD
|
||||||
|
self.execute_cmds([('pubs add data/pagerank.bib',)])
|
||||||
|
self.execute_cmds([('pubs git add .',)])
|
||||||
|
self.execute_cmds([('pubs git commit -m "initial_commit"',)])
|
||||||
|
|
||||||
|
self.execute_cmds([('pubs add data/pagerank.bib',)])
|
||||||
|
hash_j = git_hash(self.default_pubs_dir)
|
||||||
|
|
||||||
self.execute_cmds([('pubs add data/pagerank.bib',)])
|
self.execute_cmds([('pubs add data/pagerank.bib',)])
|
||||||
hash_k = git_hash(self.default_pubs_dir)
|
hash_k = git_hash(self.default_pubs_dir)
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user