* Significant refactoring: lugito wrapped into a class with common methods, IRC and Launchpad communications separated as connectors with a separate webhook module. * Some unit-tests added * Added functionality for ircbot to respond to diff links and info as well as extracting anchor links from tasks / diffs e.g. D15#167 * Built into a python package structure Still Requires: * Improved test coverage * Package documentation * Additional functionality for other phabricator appspull/1/head
parent
165c866a5d
commit
f327136c34
@ -1,3 +1,27 @@
|
|||||||
{
|
{
|
||||||
"phabricator.uri" : "https://phab.lubuntu.me/"
|
"hosts": {
|
||||||
|
"http://phab.lubuntu.me/": {
|
||||||
|
"token": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"default": "http://phab.lubunutu.me/api/"
|
||||||
|
},
|
||||||
|
"HMAC": i{
|
||||||
|
"irc": "",
|
||||||
|
},
|
||||||
|
"irc": {
|
||||||
|
"host": "irc.freenode.net",
|
||||||
|
"port": "6697",
|
||||||
|
"username": "",
|
||||||
|
"password": "",
|
||||||
|
"channel": "#lubuntu-devel"
|
||||||
|
},
|
||||||
|
"launchpad": {
|
||||||
|
"application": "lugito",
|
||||||
|
"staging": "production",
|
||||||
|
"version": "devel",
|
||||||
|
"supported_versions": ["Cosmic", "Bionic", "Xenial", "Trusty"]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,24 @@
|
|||||||
|
[run]
|
||||||
|
omit =
|
||||||
|
tests/*
|
||||||
|
*/test*.py
|
||||||
|
*/migrations/*
|
||||||
|
*/wsgi.py
|
||||||
|
*/apps.py
|
||||||
|
*_version.py
|
||||||
|
parallel = true
|
||||||
|
|
||||||
|
[paths]
|
||||||
|
source =
|
||||||
|
lugito
|
||||||
|
|
||||||
|
[report]
|
||||||
|
precision = 2
|
||||||
|
include =
|
||||||
|
lugito/*.py
|
||||||
|
exclude_lines =
|
||||||
|
def __str__
|
||||||
|
pragma: no cover
|
||||||
|
|
||||||
|
[html]
|
||||||
|
directory = coverage_html_report
|
@ -0,0 +1,22 @@
|
|||||||
|
# http://editorconfig.org
|
||||||
|
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
max_line_length=80
|
||||||
|
|
||||||
|
[*.bat]
|
||||||
|
indent_style = tab
|
||||||
|
end_of_line = crlf
|
||||||
|
|
||||||
|
[LICENSE]
|
||||||
|
insert_final_newline = false
|
||||||
|
|
||||||
|
[Makefile]
|
||||||
|
indent_style = tab
|
@ -0,0 +1 @@
|
|||||||
|
lugito/_version.py export-subst
|
@ -0,0 +1,13 @@
|
|||||||
|
=======
|
||||||
|
Credits
|
||||||
|
=======
|
||||||
|
|
||||||
|
Development Lead
|
||||||
|
----------------
|
||||||
|
|
||||||
|
* Ben Johnston <bjohnston@neomailbox.net>
|
||||||
|
|
||||||
|
Contributors
|
||||||
|
------------
|
||||||
|
|
||||||
|
None yet. Why not be the first?
|
@ -0,0 +1,128 @@
|
|||||||
|
.. highlight:: shell
|
||||||
|
|
||||||
|
============
|
||||||
|
Contributing
|
||||||
|
============
|
||||||
|
|
||||||
|
Contributions are welcome, and they are greatly appreciated! Every little bit
|
||||||
|
helps, and credit will always be given.
|
||||||
|
|
||||||
|
You can contribute in many ways:
|
||||||
|
|
||||||
|
Types of Contributions
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
Report Bugs
|
||||||
|
~~~~~~~~~~~
|
||||||
|
|
||||||
|
Report bugs at https://github.com/doc-E-brown/lugito/issues.
|
||||||
|
|
||||||
|
If you are reporting a bug, please include:
|
||||||
|
|
||||||
|
* Your operating system name and version.
|
||||||
|
* Any details about your local setup that might be helpful in troubleshooting.
|
||||||
|
* Detailed steps to reproduce the bug.
|
||||||
|
|
||||||
|
Fix Bugs
|
||||||
|
~~~~~~~~
|
||||||
|
|
||||||
|
Look through the GitHub issues for bugs. Anything tagged with "bug" and "help
|
||||||
|
wanted" is open to whoever wants to implement it.
|
||||||
|
|
||||||
|
Implement Features
|
||||||
|
~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Look through the GitHub issues for features. Anything tagged with "enhancement"
|
||||||
|
and "help wanted" is open to whoever wants to implement it.
|
||||||
|
|
||||||
|
Write Documentation
|
||||||
|
~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
lugito could always use more documentation, whether as part of the
|
||||||
|
official lugito docs, in docstrings, or even on the web in blog posts,
|
||||||
|
articles, and such.
|
||||||
|
|
||||||
|
Submit Feedback
|
||||||
|
~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
The best way to send feedback is to file an issue at https://github.com/doc-E-brown/lugito/issues.
|
||||||
|
|
||||||
|
If you are proposing a feature:
|
||||||
|
|
||||||
|
* Explain in detail how it would work.
|
||||||
|
* Keep the scope as narrow as possible, to make it easier to implement.
|
||||||
|
* Remember that this is a volunteer-driven project, and that contributions
|
||||||
|
are welcome :)
|
||||||
|
|
||||||
|
Get Started!
|
||||||
|
------------
|
||||||
|
|
||||||
|
Ready to contribute? Here's how to set up `lugito` for local development.
|
||||||
|
|
||||||
|
1. Fork the `lugito` repo on GitHub.
|
||||||
|
2. Clone your fork locally::
|
||||||
|
|
||||||
|
$ git clone git@github.com:your_name_here/lugito.git
|
||||||
|
|
||||||
|
3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development::
|
||||||
|
|
||||||
|
$ mkvirtualenv lugito
|
||||||
|
$ cd lugito/
|
||||||
|
$ python setup.py develop
|
||||||
|
|
||||||
|
4. Create a branch for local development::
|
||||||
|
|
||||||
|
$ git checkout -b name-of-your-bugfix-or-feature
|
||||||
|
|
||||||
|
Now you can make your changes locally.
|
||||||
|
|
||||||
|
5. When you're done making changes, check that your changes pass flake8 and the
|
||||||
|
tests, including testing other Python versions with tox::
|
||||||
|
|
||||||
|
$ flake8 lugito tests
|
||||||
|
$ python setup.py test or py.test
|
||||||
|
$ tox
|
||||||
|
|
||||||
|
To get flake8 and tox, just pip install them into your virtualenv.
|
||||||
|
|
||||||
|
6. Commit your changes and push your branch to GitHub::
|
||||||
|
|
||||||
|
$ git add .
|
||||||
|
$ git commit -m "Your detailed description of your changes."
|
||||||
|
$ git push origin name-of-your-bugfix-or-feature
|
||||||
|
|
||||||
|
7. Submit a pull request through the GitHub website.
|
||||||
|
|
||||||
|
Pull Request Guidelines
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
Before you submit a pull request, check that it meets these guidelines:
|
||||||
|
|
||||||
|
1. The pull request should include tests.
|
||||||
|
2. If the pull request adds functionality, the docs should be updated. Put
|
||||||
|
your new functionality into a function with a docstring, and add the
|
||||||
|
feature to the list in README.rst.
|
||||||
|
3. The pull request should work for Python 2.7, 3.4, 3.5 and 3.6, and for PyPy. Check
|
||||||
|
https://travis-ci.org/doc-E-brown/lugito/pull_requests
|
||||||
|
and make sure that the tests pass for all supported Python versions.
|
||||||
|
|
||||||
|
Tips
|
||||||
|
----
|
||||||
|
|
||||||
|
To run a subset of tests::
|
||||||
|
|
||||||
|
$ py.test tests.test_lugito
|
||||||
|
|
||||||
|
|
||||||
|
Deploying
|
||||||
|
---------
|
||||||
|
|
||||||
|
A reminder for the maintainers on how to deploy.
|
||||||
|
Make sure all your changes are committed (including an entry in HISTORY.rst).
|
||||||
|
Then run::
|
||||||
|
|
||||||
|
$ bumpversion patch # possible: major / minor / patch
|
||||||
|
$ git push
|
||||||
|
$ git push --tags
|
||||||
|
|
||||||
|
Travis will then deploy to PyPI if tests pass.
|
@ -0,0 +1,8 @@
|
|||||||
|
=======
|
||||||
|
History
|
||||||
|
=======
|
||||||
|
|
||||||
|
0.1.0 (2018-11-07)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
* First release on PyPI.
|
@ -0,0 +1,13 @@
|
|||||||
|
include AUTHORS.rst
|
||||||
|
include CONTRIBUTING.rst
|
||||||
|
include HISTORY.rst
|
||||||
|
include LICENSE
|
||||||
|
include README.rst
|
||||||
|
|
||||||
|
recursive-include tests *
|
||||||
|
recursive-exclude * __pycache__
|
||||||
|
recursive-exclude * *.py[co]
|
||||||
|
|
||||||
|
recursive-include docs *.rst conf.py Makefile make.bat *.jpg *.png *.gif
|
||||||
|
include versioneer.py
|
||||||
|
include lugito/_version.py
|
@ -0,0 +1,88 @@
|
|||||||
|
.PHONY: clean clean-test clean-pyc clean-build docs help
|
||||||
|
.DEFAULT_GOAL := help
|
||||||
|
|
||||||
|
define BROWSER_PYSCRIPT
|
||||||
|
import os, webbrowser, sys
|
||||||
|
|
||||||
|
try:
|
||||||
|
from urllib import pathname2url
|
||||||
|
except:
|
||||||
|
from urllib.request import pathname2url
|
||||||
|
|
||||||
|
webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1])))
|
||||||
|
endef
|
||||||
|
export BROWSER_PYSCRIPT
|
||||||
|
|
||||||
|
define PRINT_HELP_PYSCRIPT
|
||||||
|
import re, sys
|
||||||
|
|
||||||
|
for line in sys.stdin:
|
||||||
|
match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line)
|
||||||
|
if match:
|
||||||
|
target, help = match.groups()
|
||||||
|
print("%-20s %s" % (target, help))
|
||||||
|
endef
|
||||||
|
export PRINT_HELP_PYSCRIPT
|
||||||
|
|
||||||
|
BROWSER := python -c "$$BROWSER_PYSCRIPT"
|
||||||
|
|
||||||
|
help:
|
||||||
|
@python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST)
|
||||||
|
|
||||||
|
clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts
|
||||||
|
|
||||||
|
clean-build: ## remove build artifacts
|
||||||
|
rm -fr build/
|
||||||
|
rm -fr dist/
|
||||||
|
rm -fr .eggs/
|
||||||
|
find . -name '*.egg-info' -exec rm -fr {} +
|
||||||
|
find . -name '*.egg' -exec rm -f {} +
|
||||||
|
|
||||||
|
clean-pyc: ## remove Python file artifacts
|
||||||
|
find . -name '*.pyc' -exec rm -f {} +
|
||||||
|
find . -name '*.pyo' -exec rm -f {} +
|
||||||
|
find . -name '*~' -exec rm -f {} +
|
||||||
|
find . -name '__pycache__' -exec rm -fr {} +
|
||||||
|
|
||||||
|
clean-test: ## remove test and coverage artifacts
|
||||||
|
rm -fr .tox/
|
||||||
|
rm -f .coverage
|
||||||
|
rm -fr htmlcov/
|
||||||
|
rm -fr .pytest_cache
|
||||||
|
|
||||||
|
lint: ## check style with flake8
|
||||||
|
flake8 lugito tests
|
||||||
|
|
||||||
|
test: ## run tests quickly with the default Python
|
||||||
|
py.test
|
||||||
|
|
||||||
|
test-all: ## run tests on every Python version with tox
|
||||||
|
tox
|
||||||
|
|
||||||
|
coverage: ## check code coverage quickly with the default Python
|
||||||
|
coverage run --source lugito -m pytest
|
||||||
|
coverage report -m
|
||||||
|
coverage html
|
||||||
|
$(BROWSER) htmlcov/index.html
|
||||||
|
|
||||||
|
docs: ## generate Sphinx HTML documentation, including API docs
|
||||||
|
rm -f docs/lugito.rst
|
||||||
|
rm -f docs/modules.rst
|
||||||
|
sphinx-apidoc -o docs/ lugito
|
||||||
|
$(MAKE) -C docs clean
|
||||||
|
$(MAKE) -C docs html
|
||||||
|
$(BROWSER) docs/_build/html/index.html
|
||||||
|
|
||||||
|
servedocs: docs ## compile the docs watching for changes
|
||||||
|
watchmedo shell-command -p '*.rst' -c '$(MAKE) -C docs html' -R -D .
|
||||||
|
|
||||||
|
release: dist ## package and upload a release
|
||||||
|
twine upload dist/*
|
||||||
|
|
||||||
|
dist: clean ## builds source and wheel package
|
||||||
|
python setup.py sdist
|
||||||
|
python setup.py bdist_wheel
|
||||||
|
ls -l dist
|
||||||
|
|
||||||
|
install: clean ## install the package to the active Python's site-packages
|
||||||
|
python setup.py install
|
@ -0,0 +1,37 @@
|
|||||||
|
======
|
||||||
|
lugito
|
||||||
|
======
|
||||||
|
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/pypi/v/lugito.svg
|
||||||
|
:target: https://pypi.python.org/pypi/lugito
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/travis/doc-E-brown/lugito.svg
|
||||||
|
:target: https://travis-ci.org/doc-E-brown/lugito
|
||||||
|
|
||||||
|
.. image:: https://readthedocs.org/projects/lugito/badge/?version=latest
|
||||||
|
:target: https://lugito.readthedocs.io/en/latest/?badge=latest
|
||||||
|
:alt: Documentation Status
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Python Boilerplate contains all the boilerplate you need to create a Python package.
|
||||||
|
|
||||||
|
|
||||||
|
* Free software: BSD license
|
||||||
|
* Documentation: https://lugito.readthedocs.io.
|
||||||
|
|
||||||
|
|
||||||
|
Features
|
||||||
|
--------
|
||||||
|
|
||||||
|
* TODO
|
||||||
|
|
||||||
|
Credits
|
||||||
|
-------
|
||||||
|
|
||||||
|
This package was created with Cookiecutter_ and the `audreyr/cookiecutter-pypackage`_ project template.
|
||||||
|
|
||||||
|
.. _Cookiecutter: https://github.com/audreyr/cookiecutter
|
||||||
|
.. _`audreyr/cookiecutter-pypackage`: https://github.com/audreyr/cookiecutter-pypackage
|
@ -0,0 +1,20 @@
|
|||||||
|
# Minimal makefile for Sphinx documentation
|
||||||
|
#
|
||||||
|
|
||||||
|
# You can set these variables from the command line.
|
||||||
|
SPHINXOPTS =
|
||||||
|
SPHINXBUILD = python -msphinx
|
||||||
|
SPHINXPROJ = lugito
|
||||||
|
SOURCEDIR = .
|
||||||
|
BUILDDIR = _build
|
||||||
|
|
||||||
|
# Put it first so that "make" without argument is like "make help".
|
||||||
|
help:
|
||||||
|
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||||
|
|
||||||
|
.PHONY: help Makefile
|
||||||
|
|
||||||
|
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||||
|
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||||
|
%: Makefile
|
||||||
|
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
@ -0,0 +1 @@
|
|||||||
|
.. include:: ../AUTHORS.rst
|
@ -0,0 +1,163 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# lugito documentation build configuration file, created by
|
||||||
|
# sphinx-quickstart on Fri Jun 9 13:47:02 2017.
|
||||||
|
#
|
||||||
|
# This file is execfile()d with the current directory set to its
|
||||||
|
# containing dir.
|
||||||
|
#
|
||||||
|
# Note that not all possible configuration values are present in this
|
||||||
|
# autogenerated file.
|
||||||
|
#
|
||||||
|
# All configuration values have a default; values that are commented out
|
||||||
|
# serve to show the default.
|
||||||
|
|
||||||
|
# If extensions (or modules to document with autodoc) are in another
|
||||||
|
# directory, add these directories to sys.path here. If the directory is
|
||||||
|
# relative to the documentation root, use os.path.abspath to make it
|
||||||
|
# absolute, like shown here.
|
||||||
|
#
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
sys.path.insert(0, os.path.abspath('..'))
|
||||||
|
|
||||||
|
import lugito
|
||||||
|
|
||||||
|
# -- General configuration ---------------------------------------------
|
||||||
|
|
||||||
|
# If your documentation needs a minimal Sphinx version, state it here.
|
||||||
|
#
|
||||||
|
# needs_sphinx = '1.0'
|
||||||
|
|
||||||
|
# Add any Sphinx extension module names here, as strings. They can be
|
||||||
|
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
||||||
|
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode']
|
||||||
|
|
||||||
|
# Add any paths that contain templates here, relative to this directory.
|
||||||
|
templates_path = ['_templates']
|
||||||
|
|
||||||
|
# The suffix(es) of source filenames.
|
||||||
|
# You can specify multiple suffix as a list of string:
|
||||||
|
#
|
||||||
|
# source_suffix = ['.rst', '.md']
|
||||||
|
source_suffix = '.rst'
|
||||||
|
|
||||||
|
# The master toctree document.
|
||||||
|
master_doc = 'index'
|
||||||
|
|
||||||
|
# General information about the project.
|
||||||
|
project = u'lugito'
|
||||||
|
copyright = u"2018, Ben Johnston"
|
||||||
|
author = u"Ben Johnston"
|
||||||
|
|
||||||
|
# The version info for the project you're documenting, acts as replacement
|
||||||
|
# for |version| and |release|, also used in various other places throughout
|
||||||
|
# the built documents.
|
||||||
|
#
|
||||||
|
# The short X.Y version.
|
||||||
|
version = lugito.__version__
|
||||||
|
# The full version, including alpha/beta/rc tags.
|
||||||
|
release = lugito.__version__
|
||||||
|
|
||||||
|
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||||
|
# for a list of supported languages.
|
||||||
|
#
|
||||||
|
# This is also used if you do content translation via gettext catalogs.
|
||||||
|
# Usually you set "language" from the command line for these cases.
|
||||||
|
language = None
|
||||||
|
|
||||||
|
# List of patterns, relative to source directory, that match files and
|
||||||
|
# directories to ignore when looking for source files.
|
||||||
|
# This patterns also effect to html_static_path and html_extra_path
|
||||||
|
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
||||||
|
|
||||||
|
# The name of the Pygments (syntax highlighting) style to use.
|
||||||
|
pygments_style = 'sphinx'
|
||||||
|
|
||||||
|
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
||||||
|
todo_include_todos = False
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for HTML output -------------------------------------------
|
||||||
|
|
||||||
|
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||||
|
# a list of builtin themes.
|
||||||
|
#
|
||||||
|
html_theme = 'alabaster'
|
||||||
|
|
||||||
|
# Theme options are theme-specific and customize the look and feel of a
|
||||||
|
# theme further. For a list of options available for each theme, see the
|
||||||
|
# documentation.
|
||||||
|
#
|
||||||
|
# html_theme_options = {}
|
||||||
|
|
||||||
|
# Add any paths that contain custom static files (such as style sheets) here,
|
||||||
|
# relative to this directory. They are copied after the builtin static files,
|
||||||
|
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||||
|
html_static_path = ['_static']
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for HTMLHelp output ---------------------------------------
|
||||||
|
|
||||||
|
# Output file base name for HTML help builder.
|
||||||
|
htmlhelp_basename = 'lugitodoc'
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for LaTeX output ------------------------------------------
|
||||||
|
|
||||||
|
latex_elements = {
|
||||||
|
# The paper size ('letterpaper' or 'a4paper').
|
||||||
|
#
|
||||||
|
# 'papersize': 'letterpaper',
|
||||||
|
|
||||||
|
# The font size ('10pt', '11pt' or '12pt').
|
||||||
|
#
|
||||||
|
# 'pointsize': '10pt',
|
||||||
|
|
||||||
|
# Additional stuff for the LaTeX preamble.
|
||||||
|
#
|
||||||
|
# 'preamble': '',
|
||||||
|
|
||||||
|
# Latex figure (float) alignment
|
||||||
|
#
|
||||||
|
# 'figure_align': 'htbp',
|
||||||
|
}
|
||||||
|
|
||||||
|
# Grouping the document tree into LaTeX files. List of tuples
|
||||||
|
# (source start file, target name, title, author, documentclass
|
||||||
|
# [howto, manual, or own class]).
|
||||||
|
latex_documents = [
|
||||||
|
(master_doc, 'lugito.tex',
|
||||||
|
u'lugito Documentation',
|
||||||
|
u'Ben Johnston', 'manual'),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for manual page output ------------------------------------
|
||||||
|
|
||||||
|
# One entry per manual page. List of tuples
|
||||||
|
# (source start file, name, description, authors, manual section).
|
||||||
|
man_pages = [
|
||||||
|
(master_doc, 'lugito',
|
||||||
|
u'lugito Documentation',
|
||||||
|
[author], 1)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for Texinfo output ----------------------------------------
|
||||||
|
|
||||||
|
# Grouping the document tree into Texinfo files. List of tuples
|
||||||
|
# (source start file, target name, title, author,
|
||||||
|
# dir menu entry, description, category)
|
||||||
|
texinfo_documents = [
|
||||||
|
(master_doc, 'lugito',
|
||||||
|
u'lugito Documentation',
|
||||||
|
author,
|
||||||
|
'lugito',
|
||||||
|
'One line description of project.',
|
||||||
|
'Miscellaneous'),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1 @@
|
|||||||
|
.. include:: ../CONTRIBUTING.rst
|
@ -0,0 +1 @@
|
|||||||
|
.. include:: ../HISTORY.rst
|
@ -0,0 +1,20 @@
|
|||||||
|
Welcome to lugito's documentation!
|
||||||
|
======================================
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 2
|
||||||
|
:caption: Contents:
|
||||||
|
|
||||||
|
readme
|
||||||
|
installation
|
||||||
|
usage
|
||||||
|
modules
|
||||||
|
contributing
|
||||||
|
authors
|
||||||
|
history
|
||||||
|
|
||||||
|
Indices and tables
|
||||||
|
==================
|
||||||
|
* :ref:`genindex`
|
||||||
|
* :ref:`modindex`
|
||||||
|
* :ref:`search`
|
@ -0,0 +1,51 @@
|
|||||||
|
.. highlight:: shell
|
||||||
|
|
||||||
|
============
|
||||||
|
Installation
|
||||||
|
============
|
||||||
|
|
||||||
|
|
||||||
|
Stable release
|
||||||
|
--------------
|
||||||
|
|
||||||
|
To install lugito, run this command in your terminal:
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
$ pip install lugito
|
||||||
|
|
||||||
|
This is the preferred method to install lugito, as it will always install the most recent stable release.
|
||||||
|
|
||||||
|
If you don't have `pip`_ installed, this `Python installation guide`_ can guide
|
||||||
|
you through the process.
|
||||||
|
|
||||||
|
.. _pip: https://pip.pypa.io
|
||||||
|
.. _Python installation guide: http://docs.python-guide.org/en/latest/starting/installation/
|
||||||
|
|
||||||
|
|
||||||
|
From sources
|
||||||
|
------------
|
||||||
|
|
||||||
|
The sources for lugito can be downloaded from the `Github repo`_.
|
||||||
|
|
||||||
|
You can either clone the public repository:
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
$ git clone git://github.com/doc-E-brown/lugito
|
||||||
|
|
||||||
|
Or download the `tarball`_:
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
$ curl -OL https://github.com/doc-E-brown/lugito/tarball/master
|
||||||
|
|
||||||
|
Once you have a copy of the source, you can install it with:
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
$ python setup.py install
|
||||||
|
|
||||||
|
|
||||||
|
.. _Github repo: https://github.com/doc-E-brown/lugito
|
||||||
|
.. _tarball: https://github.com/doc-E-brown/lugito/tarball/master
|
@ -0,0 +1,36 @@
|
|||||||
|
@ECHO OFF
|
||||||
|
|
||||||
|
pushd %~dp0
|
||||||
|
|
||||||
|
REM Command file for Sphinx documentation
|
||||||
|
|
||||||
|
if "%SPHINXBUILD%" == "" (
|
||||||
|
set SPHINXBUILD=python -msphinx
|
||||||
|
)
|
||||||
|
set SOURCEDIR=.
|
||||||
|
set BUILDDIR=_build
|
||||||
|
set SPHINXPROJ=lugito
|
||||||
|
|
||||||
|
if "%1" == "" goto help
|
||||||
|
|
||||||
|
%SPHINXBUILD% >NUL 2>NUL
|
||||||
|
if errorlevel 9009 (
|
||||||
|
echo.
|
||||||
|
echo.The Sphinx module was not found. Make sure you have Sphinx installed,
|
||||||
|
echo.then set the SPHINXBUILD environment variable to point to the full
|
||||||
|
echo.path of the 'sphinx-build' executable. Alternatively you may add the
|
||||||
|
echo.Sphinx directory to PATH.
|
||||||
|
echo.
|
||||||
|
echo.If you don't have Sphinx installed, grab it from
|
||||||
|
echo.http://sphinx-doc.org/
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
|
||||||
|
goto end
|
||||||
|
|
||||||
|
:help
|
||||||
|
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
|
||||||
|
|
||||||
|
:end
|
||||||
|
popd
|
@ -0,0 +1 @@
|
|||||||
|
.. include:: ../README.rst
|
@ -0,0 +1,7 @@
|
|||||||
|
=====
|
||||||
|
Usage
|
||||||
|
=====
|
||||||
|
|
||||||
|
To use lugito in a project::
|
||||||
|
|
||||||
|
import lugito
|
@ -0,0 +1,16 @@
|
|||||||
|
from lugito.lugito import (
|
||||||
|
Lugito,
|
||||||
|
)
|
||||||
|
|
||||||
|
from lugito.connectors.irc import (
|
||||||
|
IRCConnector,
|
||||||
|
)
|
||||||
|
|
||||||
|
from lugito.connectors.launchpad import (
|
||||||
|
LPConnectook,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
from ._version import get_versions
|
||||||
|
__version__ = get_versions()['version']
|
||||||
|
del get_versions
|
@ -0,0 +1,520 @@
|
|||||||
|
|
||||||
|
# This file helps to compute a version number in source trees obtained from
|
||||||
|
# git-archive tarball (such as those provided by githubs download-from-tag
|
||||||
|
# feature). Distribution tarballs (built by setup.py sdist) and build
|
||||||
|
# directories (produced by setup.py build) will contain a much shorter file
|
||||||
|
# that just contains the computed version number.
|
||||||
|
|
||||||
|
# This file is released into the public domain. Generated by
|
||||||
|
# versioneer-0.18 (https://github.com/warner/python-versioneer)
|
||||||
|
|
||||||
|
"""Git implementation of _version.py."""
|
||||||
|
|
||||||
|
import errno
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def get_keywords():
|
||||||
|
"""Get the keywords needed to look up the version information."""
|
||||||
|
# these strings will be replaced by git during git-archive.
|
||||||
|
# setup.py/versioneer.py will grep for the variable names, so they must
|
||||||
|
# each be defined on a line of their own. _version.py will just call
|
||||||
|
# get_keywords().
|
||||||
|
git_refnames = "$Format:%d$"
|
||||||
|
git_full = "$Format:%H$"
|
||||||
|
git_date = "$Format:%ci$"
|
||||||
|
keywords = {"refnames": git_refnames, "full": git_full, "date": git_date}
|
||||||
|
return keywords
|
||||||
|
|
||||||
|
|
||||||
|
class VersioneerConfig:
|
||||||
|
"""Container for Versioneer configuration parameters."""
|
||||||
|
|
||||||
|
|
||||||
|
def get_config():
|
||||||
|
"""Create, populate and return the VersioneerConfig() object."""
|
||||||
|
# these strings are filled in when 'setup.py versioneer' creates
|
||||||
|
# _version.py
|
||||||
|
cfg = VersioneerConfig()
|
||||||
|
cfg.VCS = "git"
|
||||||
|
cfg.style = "pep440"
|
||||||
|
cfg.tag_prefix = ""
|
||||||
|
cfg.parentdir_prefix = "lugito-"
|
||||||
|
cfg.versionfile_source = "lugito/_version.py"
|
||||||
|
cfg.verbose = False
|
||||||
|
return cfg
|
||||||
|
|
||||||
|
|
||||||
|
class NotThisMethod(Exception):
|
||||||
|
"""Exception raised if a method is not valid for the current scenario."""
|
||||||
|
|
||||||
|
|
||||||
|
LONG_VERSION_PY = {}
|
||||||
|
HANDLERS = {}
|
||||||
|
|
||||||
|
|
||||||
|
def register_vcs_handler(vcs, method): # decorator
|
||||||
|
"""Decorator to mark a method as the handler for a particular VCS."""
|
||||||
|
def decorate(f):
|
||||||
|
"""Store f in HANDLERS[vcs][method]."""
|
||||||
|
if vcs not in HANDLERS:
|
||||||
|
HANDLERS[vcs] = {}
|
||||||
|
HANDLERS[vcs][method] = f
|
||||||
|
return f
|
||||||
|
return decorate
|
||||||
|
|
||||||
|
|
||||||
|
def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False,
|
||||||
|
env=None):
|
||||||
|
"""Call the given command(s)."""
|
||||||
|
assert isinstance(commands, list)
|
||||||
|
p = None
|
||||||
|
for c in commands:
|
||||||
|
try:
|
||||||
|
dispcmd = str([c] + args)
|
||||||
|
# remember shell=False, so use git.cmd on windows, not just git
|
||||||
|
p = subprocess.Popen([c] + args, cwd=cwd, env=env,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=(subprocess.PIPE if hide_stderr
|
||||||
|
else None))
|
||||||
|
break
|
||||||
|
except EnvironmentError:
|
||||||
|
e = sys.exc_info()[1]
|
||||||
|
if e.errno == errno.ENOENT:
|
||||||
|
continue
|
||||||
|
if verbose:
|
||||||
|
print("unable to run %s" % dispcmd)
|
||||||
|
print(e)
|
||||||
|
return None, None
|
||||||
|
else:
|
||||||
|
if verbose:
|
||||||
|
print("unable to find command, tried %s" % (commands,))
|
||||||
|
return None, None
|
||||||
|
stdout = p.communicate()[0].strip()
|
||||||
|
if sys.version_info[0] >= 3:
|
||||||
|
stdout = stdout.decode()
|
||||||
|
if p.returncode != 0:
|
||||||
|
if verbose:
|
||||||
|
print("unable to run %s (error)" % dispcmd)
|
||||||
|
print("stdout was %s" % stdout)
|
||||||
|
return None, p.returncode
|
||||||
|
return stdout, p.returncode
|
||||||
|
|
||||||
|
|
||||||
|
def versions_from_parentdir(parentdir_prefix, root, verbose):
|
||||||
|
"""Try to determine the version from the parent directory name.
|
||||||
|
|
||||||
|
Source tarballs conventionally unpack into a directory that includes both
|
||||||
|
the project name and a version string. We will also support searching up
|
||||||
|
two directory levels for an appropriately named parent directory
|
||||||
|
"""
|
||||||
|
rootdirs = []
|
||||||
|
|
||||||
|
for i in range(3):
|
||||||
|
dirname = os.path.basename(root)
|
||||||
|
if dirname.startswith(parentdir_prefix):
|
||||||
|
return {"version": dirname[len(parentdir_prefix):],
|
||||||
|
"full-revisionid": None,
|
||||||
|
"dirty": False, "error": None, "date": None}
|
||||||
|
else:
|
||||||
|
rootdirs.append(root)
|
||||||
|
root = os.path.dirname(root) # up a level
|
||||||
|
|
||||||
|
if verbose:
|
||||||
|
print("Tried directories %s but none started with prefix %s" %
|
||||||
|
(str(rootdirs), parentdir_prefix))
|
||||||
|
raise NotThisMethod("rootdir doesn't start with parentdir_prefix")
|
||||||
|
|
||||||
|
|
||||||
|
@register_vcs_handler("git", "get_keywords")
|
||||||
|
def git_get_keywords(versionfile_abs):
|
||||||
|
"""Extract version information from the given file."""
|
||||||
|
# the code embedded in _version.py can just fetch the value of these
|
||||||
|
# keywords. When used from setup.py, we don't want to import _version.py,
|
||||||
|
# so we do it with a regexp instead. This function is not used from
|
||||||
|
# _version.py.
|
||||||
|
keywords = {}
|
||||||
|
try:
|
||||||
|
f = open(versionfile_abs, "r")
|
||||||
|
for line in f.readlines():
|
||||||
|
if line.strip().startswith("git_refnames ="):
|
||||||
|
mo = re.search(r'=\s*"(.*)"', line)
|
||||||
|
if mo:
|
||||||
|
keywords["refnames"] = mo.group(1)
|
||||||
|
if line.strip().startswith("git_full ="):
|
||||||
|
mo = re.search(r'=\s*"(.*)"', line)
|
||||||
|
if mo:
|
||||||
|
keywords["full"] = mo.group(1)
|
||||||
|
if line.strip().startswith("git_date ="):
|
||||||
|
mo = re.search(r'=\s*"(.*)"', line)
|
||||||
|
if mo:
|
||||||
|
keywords["date"] = mo.group(1)
|
||||||
|
f.close()
|
||||||
|
except EnvironmentError:
|
||||||
|
pass
|
||||||
|
return keywords
|
||||||
|
|
||||||
|
|
||||||
|
@register_vcs_handler("git", "keywords")
|
||||||
|
def git_versions_from_keywords(keywords, tag_prefix, verbose):
|
||||||
|
"""Get version information from git keywords."""
|
||||||
|
if not keywords:
|
||||||
|
raise NotThisMethod("no keywords at all, weird")
|
||||||
|
date = keywords.get("date")
|
||||||
|
if date is not None:
|
||||||
|
# git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant
|
||||||
|
# datestamp. However we prefer "%ci" (which expands to an "ISO-8601
|
||||||
|
# -like" string, which we must then edit to make compliant), because
|
||||||
|
# it's been around since git-1.5.3, and it's too difficult to
|
||||||
|
# discover which version we're using, or to work around using an
|
||||||
|
# older one.
|
||||||
|
date = date.strip().replace(" ", "T", 1).replace(" ", "", 1)
|
||||||
|
refnames = keywords["refnames"].strip()
|
||||||
|
if refnames.startswith("$Format"):
|
||||||
|
if verbose:
|
||||||
|
print("keywords are unexpanded, not using")
|
||||||
|
raise NotThisMethod("unexpanded keywords, not a git-archive tarball")
|
||||||
|
refs = set([r.strip() for r in refnames.strip("()").split(",")])
|
||||||
|
# starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of
|
||||||
|
# just "foo-1.0". If we see a "tag: " prefix, prefer those.
|
||||||
|
TAG = "tag: "
|
||||||
|
tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)])
|
||||||
|
if not tags:
|
||||||
|
# Either we're using git < 1.8.3, or there really are no tags. We use
|
||||||
|
# a heuristic: assume all version tags have a digit. The old git %d
|
||||||
|
# expansion behaves like git log --decorate=short and strips out the
|
||||||
|
# refs/heads/ and refs/tags/ prefixes that would let us distinguish
|
||||||
|
# between branches and tags. By ignoring refnames without digits, we
|
||||||
|
# filter out many common branch names like "release" and
|
||||||
|
# "stabilization", as well as "HEAD" and "master".
|
||||||
|
tags = set([r for r in refs if re.search(r'\d', r)])
|
||||||
|
if verbose:
|
||||||
|
print("discarding '%s', no digits" % ",".join(refs - tags))
|
||||||
|
if verbose:
|
||||||
|
print("likely tags: %s" % ",".join(sorted(tags)))
|
||||||
|
for ref in sorted(tags):
|
||||||
|
# sorting will prefer e.g. "2.0" over "2.0rc1"
|
||||||
|
if ref.startswith(tag_prefix):
|
||||||
|
r = ref[len(tag_prefix):]
|
||||||
|
if verbose:
|
||||||
|
print("picking %s" % r)
|
||||||
|
return {"version": r,
|
||||||
|
"full-revisionid": keywords["full"].strip(),
|
||||||
|
"dirty": False, "error": None,
|
||||||
|
"date": date}
|
||||||
|
# no suitable tags, so version is "0+unknown", but full hex is still there
|
||||||
|
if verbose:
|
||||||
|
print("no suitable tags, using unknown + full revision id")
|
||||||
|
return {"version": "0+unknown",
|
||||||
|
"full-revisionid": keywords["full"].strip(),
|
||||||
|
"dirty": False, "error": "no suitable tags", "date": None}
|
||||||
|
|
||||||
|
|
||||||
|
@register_vcs_handler("git", "pieces_from_vcs")
|
||||||
|
def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command):
|
||||||
|
"""Get version from 'git describe' in the root of the source tree.
|
||||||
|
|
||||||
|
This only gets called if the git-archive 'subst' keywords were *not*
|
||||||
|
expanded, and _version.py hasn't already been rewritten with a short
|
||||||
|
version string, meaning we're inside a checked out source tree.
|
||||||
|
"""
|
||||||
|
GITS = ["git"]
|
||||||
|
if sys.platform == "win32":
|
||||||
|
GITS = ["git.cmd", "git.exe"]
|
||||||
|
|
||||||
|
out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root,
|
||||||
|
hide_stderr=True)
|
||||||
|
if rc != 0:
|
||||||
|
if verbose:
|
||||||
|
print("Directory %s not under git control" % root)
|
||||||
|
raise NotThisMethod("'git rev-parse --git-dir' returned error")
|
||||||
|
|
||||||
|
# if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty]
|
||||||
|
# if there isn't one, this yields HEX[-dirty] (no NUM)
|
||||||
|
describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty",
|
||||||
|
"--always", "--long",
|
||||||
|
"--match", "%s*" % tag_prefix],
|
||||||
|
cwd=root)
|
||||||
|
# --long was added in git-1.5.5
|
||||||
|
if describe_out is None:
|
||||||
|
raise NotThisMethod("'git describe' failed")
|
||||||
|
describe_out = describe_out.strip()
|
||||||
|
full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root)
|
||||||
|
if full_out is None:
|
||||||
|
raise NotThisMethod("'git rev-parse' failed")
|
||||||
|
full_out = full_out.strip()
|
||||||
|
|
||||||
|
pieces = {}
|
||||||
|
pieces["long"] = full_out
|
||||||
|
pieces["short"] = full_out[:7] # maybe improved later
|
||||||
|
pieces["error"] = None
|
||||||
|
|
||||||
|
# parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty]
|
||||||
|
# TAG might have hyphens.
|
||||||
|
git_describe = describe_out
|
||||||
|
|
||||||
|
# look for -dirty suffix
|
||||||
|
dirty = git_describe.endswith("-dirty")
|
||||||
|
pieces["dirty"] = dirty
|
||||||
|
if dirty:
|
||||||
|
git_describe = git_describe[:git_describe.rindex("-dirty")]
|
||||||
|
|
||||||
|
# now we have TAG-NUM-gHEX or HEX
|
||||||
|
|
||||||
|
if "-" in git_describe:
|
||||||
|
# TAG-NUM-gHEX
|
||||||
|
mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe)
|
||||||
|
if not mo:
|
||||||
|
# unparseable. Maybe git-describe is misbehaving?
|
||||||
|
pieces["error"] = ("unable to parse git-describe output: '%s'"
|
||||||
|
% describe_out)
|
||||||
|
return pieces
|
||||||
|
|
||||||
|
# tag
|
||||||
|
full_tag = mo.group(1)
|
||||||
|
if not full_tag.startswith(tag_prefix):
|
||||||
|
if verbose:
|
||||||
|
fmt = "tag '%s' doesn't start with prefix '%s'"
|
||||||
|
print(fmt % (full_tag, tag_prefix))
|
||||||
|
pieces["error"] = ("tag '%s' doesn't start with prefix '%s'"
|
||||||
|
% (full_tag, tag_prefix))
|
||||||
|
return pieces
|
||||||
|
pieces["closest-tag"] = full_tag[len(tag_prefix):]
|
||||||
|
|
||||||
|
# distance: number of commits since tag
|
||||||
|
pieces["distance"] = int(mo.group(2))
|
||||||
|
|
||||||
|
# commit: short hex revision ID
|
||||||
|
pieces["short"] = mo.group(3)
|
||||||
|
|
||||||
|
else:
|
||||||
|
# HEX: no tags
|
||||||
|
pieces["closest-tag"] = None
|
||||||
|
count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"],
|
||||||
|
cwd=root)
|
||||||
|
pieces["distance"] = int(count_out) # total number of commits
|
||||||
|
|
||||||
|
# commit date: see ISO-8601 comment in git_versions_from_keywords()
|
||||||
|
date = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"],
|
||||||
|
cwd=root)[0].strip()
|
||||||
|
pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1)
|
||||||
|
|
||||||
|
return pieces
|
||||||
|
|
||||||
|
|
||||||
|
def plus_or_dot(pieces):
|
||||||
|
"""Return a + if we don't already have one, else return a ."""
|
||||||
|
if "+" in pieces.get("closest-tag", ""):
|
||||||
|
return "."
|
||||||
|
return "+"
|
||||||
|
|
||||||
|
|
||||||
|
def render_pep440(pieces):
|
||||||
|
"""Build up version string, with post-release "local version identifier".
|
||||||
|
|
||||||
|
Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you
|
||||||
|
get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty
|
||||||
|
|
||||||
|
Exceptions:
|
||||||
|
1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty]
|
||||||
|
"""
|
||||||
|
if pieces["closest-tag"]:
|
||||||
|
rendered = pieces["closest-tag"]
|
||||||
|
if pieces["distance"] or pieces["dirty"]:
|
||||||
|
rendered += plus_or_dot(pieces)
|
||||||
|
rendered += "%d.g%s" % (pieces["distance"], pieces["short"])
|
||||||
|
if pieces["dirty"]:
|
||||||
|
rendered += ".dirty"
|
||||||
|
else:
|
||||||
|
# exception #1
|
||||||
|
rendered = "0+untagged.%d.g%s" % (pieces["distance"],
|
||||||
|
pieces["short"])
|
||||||
|
if pieces["dirty"]:
|
||||||
|
rendered += ".dirty"
|
||||||
|
return rendered
|
||||||
|
|
||||||
|
|
||||||
|
def render_pep440_pre(pieces):
|
||||||
|
"""TAG[.post.devDISTANCE] -- No -dirty.
|
||||||
|
|
||||||
|
Exceptions:
|
||||||
|
1: no tags. 0.post.devDISTANCE
|
||||||
|
"""
|
||||||
|
if pieces["closest-tag"]:
|
||||||
|
rendered = pieces["closest-tag"]
|
||||||
|
if pieces["distance"]:
|
||||||
|
rendered += ".post.dev%d" % pieces["distance"]
|
||||||
|
else:
|
||||||
|
# exception #1
|
||||||
|
rendered = "0.post.dev%d" % pieces["distance"]
|
||||||
|
return rendered
|
||||||
|
|
||||||
|
|
||||||
|
def render_pep440_post(pieces):
|
||||||
|
"""TAG[.postDISTANCE[.dev0]+gHEX] .
|
||||||
|
|
||||||
|
The ".dev0" means dirty. Note that .dev0 sorts backwards
|
||||||
|
(a dirty tree will appear "older" than the corresponding clean one),
|
||||||
|
but you shouldn't be releasing software with -dirty anyways.
|
||||||
|
|
||||||
|
Exceptions:
|
||||||
|
1: no tags. 0.postDISTANCE[.dev0]
|
||||||
|
"""
|
||||||
|
if pieces["closest-tag"]:
|
||||||
|
rendered = pieces["closest-tag"]
|
||||||
|
if pieces["distance"] or pieces["dirty"]:
|
||||||
|
rendered += ".post%d" % pieces["distance"]
|
||||||
|
if pieces["dirty"]:
|
||||||
|
rendered += ".dev0"
|
||||||
|
rendered += plus_or_dot(pieces)
|
||||||
|
rendered += "g%s" % pieces["short"]
|
||||||
|
else:
|
||||||
|
# exception #1
|
||||||
|
rendered = "0.post%d" % pieces["distance"]
|
||||||
|
if pieces["dirty"]:
|
||||||
|
rendered += ".dev0"
|
||||||
|
rendered += "+g%s" % pieces["short"]
|
||||||
|
return rendered
|
||||||
|
|
||||||
|
|
||||||
|
def render_pep440_old(pieces):
|
||||||
|
"""TAG[.postDISTANCE[.dev0]] .
|
||||||
|
|
||||||
|
The ".dev0" means dirty.
|
||||||
|
|
||||||
|
Eexceptions:
|
||||||
|
1: no tags. 0.postDISTANCE[.dev0]
|
||||||
|
"""
|
||||||
|
if pieces["closest-tag"]:
|
||||||
|
rendered = pieces["closest-tag"]
|
||||||
|
if pieces["distance"] or pieces["dirty"]:
|
||||||
|
rendered += ".post%d" % pieces["distance"]
|
||||||
|
if pieces["dirty"]:
|
||||||
|
rendered += ".dev0"
|
||||||
|
else:
|
||||||
|
# exception #1
|
||||||
|
rendered = "0.post%d" % pieces["distance"]
|
||||||
|
if pieces["dirty"]:
|
||||||
|
rendered += ".dev0"
|
||||||
|
return rendered
|
||||||
|
|
||||||
|
|
||||||
|
def render_git_describe(pieces):
|
||||||
|
"""TAG[-DISTANCE-gHEX][-dirty].
|
||||||
|
|
||||||
|
Like 'git describe --tags --dirty --always'.
|
||||||
|
|
||||||
|
Exceptions:
|
||||||
|
1: no tags. HEX[-dirty] (note: no 'g' prefix)
|
||||||
|
"""
|
||||||
|
if pieces["closest-tag"]:
|
||||||
|
rendered = pieces["closest-tag"]
|
||||||
|
if pieces["distance"]:
|
||||||
|
rendered += "-%d-g%s" % (pieces["distance"], pieces["short"])
|
||||||
|
else:
|
||||||
|
# exception #1
|
||||||
|
rendered = pieces["short"]
|
||||||
|
if pieces["dirty"]:
|
||||||
|
rendered += "-dirty"
|
||||||
|
return rendered
|
||||||
|
|
||||||
|
|
||||||
|
def render_git_describe_long(pieces):
|
||||||
|
"""TAG-DISTANCE-gHEX[-dirty].
|
||||||
|
|
||||||
|
Like 'git describe --tags --dirty --always -long'.
|
||||||
|
The distance/hash is unconditional.
|
||||||
|
|
||||||
|
Exceptions:
|
||||||
|
1: no tags. HEX[-dirty] (note: no 'g' prefix)
|
||||||
|
"""
|
||||||
|
if pieces["closest-tag"]:
|
||||||
|
rendered = pieces["closest-tag"]
|
||||||
|
rendered += "-%d-g%s" % (pieces["distance"], pieces["short"])
|
||||||
|
else:
|
||||||
|
# exception #1
|
||||||
|
rendered = pieces["short"]
|
||||||
|
if pieces["dirty"]:
|
||||||
|
rendered += "-dirty"
|
||||||
|
return rendered
|
||||||
|
|
||||||
|
|
||||||
|
def render(pieces, style):
|
||||||
|
"""Render the given version pieces into the requested style."""
|
||||||
|
if pieces["error"]:
|
||||||
|
return {"version": "unknown",
|
||||||
|
"full-revisionid": pieces.get("long"),
|
||||||
|
"dirty": None,
|
||||||
|
"error": pieces["error"],
|
||||||
|
"date": None}
|
||||||
|
|
||||||
|
if not style or style == "default":
|
||||||
|
style = "pep440" # the default
|
||||||
|
|
||||||
|
if style == "pep440":
|
||||||
|
rendered = render_pep440(pieces)
|
||||||
|
elif style == "pep440-pre":
|
||||||
|
rendered = render_pep440_pre(pieces)
|
||||||
|
elif style == "pep440-post":
|
||||||
|
rendered = render_pep440_post(pieces)
|
||||||
|
elif style == "pep440-old":
|
||||||
|
rendered = render_pep440_old(pieces)
|
||||||
|
elif style == "git-describe":
|
||||||
|
rendered = render_git_describe(pieces)
|
||||||
|
elif style == "git-describe-long":
|
||||||
|
rendered = render_git_describe_long(pieces)
|
||||||
|
else:
|
||||||
|
raise ValueError("unknown style '%s'" % style)
|
||||||
|
|
||||||
|
return {"version": rendered, "full-revisionid": pieces["long"],
|
||||||
|
"dirty": pieces["dirty"], "error": None,
|
||||||
|
"date": pieces.get("date")}
|
||||||
|
|
||||||
|
|
||||||
|
def get_versions():
|
||||||
|
"""Get version information or return default if unable to do so."""
|
||||||
|
# I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have
|
||||||
|
# __file__, we can work backwards from there to the root. Some
|
||||||
|
# py2exe/bbfreeze/non-CPython implementations don't do __file__, in which
|
||||||
|
# case we can only use expanded keywords.
|
||||||
|
|
||||||
|
cfg = get_config()
|
||||||
|
verbose = cfg.verbose
|
||||||
|
|
||||||
|
try:
|
||||||
|
return git_versions_from_keywords(get_keywords(), cfg.tag_prefix,
|
||||||
|
verbose)
|
||||||
|
except NotThisMethod:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
root = os.path.realpath(__file__)
|
||||||
|
# versionfile_source is the relative path from the top of the source
|
||||||
|
# tree (where the .git directory might live) to this file. Invert
|
||||||
|
# this to find the root from __file__.
|
||||||
|
for i in cfg.versionfile_source.split('/'):
|
||||||
|
root = os.path.dirname(root)
|
||||||
|
except NameError:
|
||||||
|
return {"version": "0+unknown", "full-revisionid": None,
|
||||||
|
"dirty": None,
|
||||||
|
"error": "unable to find root of source tree",
|
||||||
|
"date": None}
|
||||||
|
|
||||||
|
try:
|
||||||
|
pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose)
|
||||||
|
return render(pieces, cfg.style)
|
||||||
|
except NotThisMethod:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
if cfg.parentdir_prefix:
|
||||||
|
return versions_from_parentdir(cfg.parentdir_prefix, root, verbose)
|
||||||
|
except NotThisMethod:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return {"version": "0+unknown", "full-revisionid": None,
|
||||||
|
"dirty": None,
|
||||||
|
"error": "unable to compute version", "date": None}
|
@ -0,0 +1,19 @@
|
|||||||
|
#! /usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# S.D.G
|
||||||
|
|
||||||
|
"""Doc string for module
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
:author: Ben Johnston
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Imports
|
||||||
|
|
||||||
|
|
||||||
|
from lugito.webhooks import run
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
run()
|
@ -0,0 +1,3 @@
|
|||||||
|
#! /usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# S.D.G
|
@ -0,0 +1,239 @@
|
|||||||
|
#! /usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# S.D.G
|
||||||
|
|
||||||
|
"""
|
||||||
|
:mod:`lugito.connectors.irc`
|
||||||
|
======================================
|
||||||
|
|
||||||
|
Define an irc connector class
|
||||||
|
|
||||||
|
.. currentmodule:: lugito.connectors.irc
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Imports
|
||||||
|
import ssl
|
||||||
|
import http
|
||||||
|
import socket
|
||||||
|
import logging
|
||||||
|
import threading
|
||||||
|
import phabricator
|
||||||
|
from time import sleep
|
||||||
|
|
||||||
|
|
||||||
|
class IRCConnector(object):
|
||||||
|
|
||||||
|
def __init__(self, log_level=logging.DEBUG):
|
||||||
|
|
||||||
|
# IRC info
|
||||||
|
# Read the configuration out of the .arcconfig file
|
||||||
|
self.host = phabricator.ARCRC['irc']['host']
|
||||||
|
self.port = int(phabricator.ARCRC['irc']['port'])
|
||||||
|
self.username = phabricator.ARCRC['irc']['username']
|
||||||
|
self.password = phabricator.ARCRC['irc']['password']
|
||||||
|
self.channel = phabricator.ARCRC['irc']['channel']
|
||||||
|
|
||||||
|
# Phabricator info
|
||||||
|
self.phab = phabricator.Phabricator()
|
||||||
|
self.phab_host = phabricator.ARCRC['config']['default'].replace(
|
||||||
|
'api/', '')
|
||||||
|
|
||||||
|
self.logger = logging.getLogger('lugito.connector.IRCConnector')
|
||||||
|
|
||||||
|
# Add log level
|
||||||
|
ch = logging.StreamHandler()
|
||||||
|
|
||||||
|
formatter = logging.Formatter(
|
||||||
|
'%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||||
|
ch.setFormatter(formatter)
|
||||||
|
|
||||||
|
self.logger.addHandler(ch)
|
||||||
|
self.logger.setLevel(log_level)
|
||||||
|
|
||||||
|
|
||||||
|
def _send_raw(self, message):
|
||||||
|
"""Low level send"""
|
||||||
|
|
||||||
|
self.conn.send(message.encode('utf-8'))
|
||||||
|
|
||||||
|
|
||||||
|
def _socket_conn(self):
|
||||||
|
self.conn = ssl.wrap_socket(
|
||||||
|
socket.socket(socket.AF_INET, socket.SOCK_STREAM))
|
||||||
|
self.conn.connect((self.host, self.port))
|
||||||
|
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
|
"""Connect"""
|
||||||
|
|
||||||
|
self._socket_conn()
|
||||||
|
|
||||||
|
setup = False
|
||||||
|
usersuffix = 0
|
||||||
|
self.logger.info("Connecting to IRC.")
|
||||||
|
|
||||||
|
while not setup:
|
||||||
|
response = self.conn.recv(512).decode("utf-8")
|
||||||
|
self.logger.debug(response)
|
||||||
|
|
||||||
|
if "No Ident response" in response:
|
||||||
|
self._send_raw("NICK {}\r\n".format(self.username))
|
||||||
|
self._send_raw("USER {} * * :{}\r\n".format(
|
||||||
|
self.username, self.username))
|
||||||
|
self._send_raw("PRIVMSG nickserv :identify {} {}\r\n".format(
|
||||||
|
self.username, self.password))
|
||||||
|
|
||||||
|
if "You are now identified" in response:
|
||||||
|
sleep(5)
|
||||||
|
self._send_raw("JOIN {}\r\n".format(self.channel))
|
||||||
|
|
||||||
|
if "477" in response:
|
||||||
|
sleep(5)
|
||||||
|
self._send_raw("JOIN {}\r\n".format(self.channel))
|
||||||
|
|
||||||
|
if "433" in response:
|
||||||
|
usersuffix = usersuffix + 1
|
||||||
|
self.username = self.username + str(usersuffix)
|
||||||
|
self._send_raw("NICK {}\r\n".format(self.username))
|
||||||
|
self._send_raw("USER {} * * :{}\r\n".format(
|
||||||
|
self.username, self.username))
|
||||||
|
|
||||||
|
if "PING" in response:
|
||||||
|
self._send_raw("PONG :{}\r\n".format(response.split(":")[1]))
|
||||||
|
|
||||||
|
if "366" in response:
|
||||||
|
setup = True
|
||||||
|
|
||||||
|
self.logger.info("Successfully connected to the IRC server.")
|
||||||
|
|
||||||
|
def send_notice(self, message):
|
||||||
|
self._send_raw("NOTICE {} :{}\r\n".format(self.channel, message))
|
||||||
|
|
||||||
|
def send(self, objectstr, who, body, link):
|
||||||
|
"""Send a formatted message"""
|
||||||
|
|
||||||
|
# e.g. [T31: Better IRC integration]
|
||||||
|
message = "\x033[\x03\x0313" + objectstr + "\x03\x033]\x03 "
|
||||||
|
# e.g. tsimonq2 (Simon Quigley)
|
||||||
|
message = message + "\x0315" + who + "\x03 "
|
||||||
|
# e.g. commented on the task:
|
||||||
|
message = message + body + ": "
|
||||||
|
# e.g. https://phab.lubuntu.me/T40#779
|
||||||
|
message = message + "\x032" + link + "\x03"
|
||||||
|
# Make sure we can debug this if it goes haywire
|
||||||
|
self.logger.debug(message)
|
||||||
|
# Sleep for a fifth of a second, so when we have a bunch of messages we have a buffer
|
||||||
|
sleep(0.2)
|
||||||
|
# Aaaaand, send it off!
|
||||||
|
self.send_notice(message)
|
||||||
|
|
||||||
|
def gettaskinfo(self, task):
|
||||||
|
|
||||||
|
sendmessage = ""
|
||||||
|
|
||||||
|
# Strip out anchor link
|
||||||
|
# This will prevent invalid task / diff references
|
||||||
|
anchor = None
|
||||||
|
if '#' in task:
|
||||||
|
task, anchor = task.split('#')
|
||||||
|
|
||||||
|
try:
|
||||||
|
# We only need the task number.
|
||||||
|
if len(task.split("T")) > 1:
|
||||||
|
taskinfo = self.phab.maniphest.info(
|
||||||
|
task_id=int(task.split("T")[1]))
|
||||||
|
|
||||||
|
# or the diff number
|
||||||
|
elif len(task.split("D")) > 1:
|
||||||
|
taskinfo = self.phab.differential.query(
|
||||||
|
ids=[int(task.split("D")[1])])[0]
|
||||||
|
taskinfo['priorityColor'] = None
|
||||||
|
|
||||||
|
sendmessage += "\x033[\x03"
|
||||||
|
|
||||||
|
# The color of the priority text should correspond to its value.
|
||||||
|
color = taskinfo["priorityColor"]
|
||||||
|
if color == "violet":
|
||||||
|
sendmessage += "\x036Needs Triage"
|
||||||
|
elif color == "pink":
|
||||||
|
sendmessage += "\x035Unbreak Now!"
|
||||||
|
elif color == "red":
|
||||||
|
sendmessage += "\x034High"
|
||||||
|
elif color == "orange":
|
||||||
|
sendmessage += "\x037Medium"
|
||||||
|
elif color == "yellow":
|
||||||
|
sendmessage += "\x038Low"
|
||||||
|
elif color == "sky":
|
||||||
|
sendmessage += "\x037Wishlist"
|
||||||
|
|
||||||
|
# Put the task status in the message.
|
||||||
|
if color is not None:
|
||||||
|
sendmessage += ", "
|
||||||
|
|
||||||
|
sendmessage += taskinfo["statusName"] + "\x03\x033]\x03 "
|
||||||
|
|
||||||
|
# Put the title in there as well.
|
||||||
|
sendmessage += taskinfo["title"].strip() + ": "
|
||||||
|
|
||||||
|
# And the link.
|
||||||
|
sendmessage += "\x032" + taskinfo["uri"]
|
||||||
|
|
||||||
|
# Add the anchor back if it was present
|
||||||
|
if anchor is not None:
|
||||||
|
sendmessage += '#{}'.format(anchor)
|
||||||
|
|
||||||
|
sendmessage += '\x03'
|
||||||
|
|
||||||
|
# Send it off!
|
||||||
|
self.send_notice(sendmessage)
|
||||||
|
|
||||||
|
# If someone wrote something like "Tblah", obviously that's not right.
|
||||||
|
except ValueError:
|
||||||
|
self.send_notice("\x034Error: " + task.strip() + "is an invalid task reference.\x03")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def bot(self, message, msgtype):
|
||||||
|
|
||||||
|
if msgtype == "info":
|
||||||
|
message = message.split(" :" + self.username + ": info")[1]
|
||||||
|
|
||||||
|
for item in message.split():
|
||||||
|
if item.startswith("T") or item.startwith("D"):
|
||||||
|
self.gettaskinfo(item.strip())
|
||||||
|
|
||||||
|
elif msgtype == "link":
|
||||||
|
|
||||||
|
for item in message.split(self.phab_host):
|
||||||
|
if (item.split()[0].strip().startswith("T")) or \
|
||||||
|
(item.split()[0].strip().startswith("D")):
|
||||||
|
|
||||||
|
self.gettaskinfo(item.split()[0].strip())
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.sendnotice("\x034Error: unknown command.\x03")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def listen(self):
|
||||||
|
|
||||||
|
while True:
|
||||||
|
ircmsg = self.conn.recv(512)
|
||||||
|
|
||||||
|
if len(ircmsg) == 0:
|
||||||
|
# logger.warn is deprecated, use .warning.
|
||||||
|
self.logger.warning("Connection lost, reconnecting!")
|
||||||
|
self.connect()
|
||||||
|
continue
|
||||||
|
|
||||||
|
ircmsg = ircmsg.decode("UTF-8").strip('\n\r')
|
||||||
|
self.logger.debug(ircmsg)
|
||||||
|
|
||||||
|
if ircmsg.find("PING :") != -1:
|
||||||
|
self.conn.send(bytes("PONG :pingis\n", "UTF-8"))
|
||||||
|
|
||||||
|
elif ircmsg.find(" :" + self.username + ": info") != -1:
|
||||||
|
self.bot(ircmsg, "info")
|
||||||
|
|
||||||
|
elif ircmsg.find("{}".format(self.phab_host)) != -1:
|
||||||
|
self.bot(ircmsg, "link")
|
@ -0,0 +1,66 @@
|
|||||||
|
#! /usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# S.D.G
|
||||||
|
"""
|
||||||
|
:mod:`lugito.connectors.launchpad`
|
||||||
|
======================================
|
||||||
|
|
||||||
|
Define a launchpad connector class
|
||||||
|
|
||||||
|
.. currentmodule:: lugito.connectors.launchpad
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Imports
|
||||||
|
import logging
|
||||||
|
import phabricator
|
||||||
|
from launchpadlib.launchpad import Launchpad
|
||||||
|
|
||||||
|
|
||||||
|
class LPConnector(object):
|
||||||
|
|
||||||
|
def __init__(self, log_level=logging.DEBUG):
|
||||||
|
|
||||||
|
# Launchpad info
|
||||||
|
# Read the configuration out of the .arcconfig file
|
||||||
|
self.application = phabricator.ARCRC['launchpad']['application']
|
||||||
|
self.staging = phabricator.ARCRC['launchpad']['staging']
|
||||||
|
self.version = phabricator.ARCRC['launchpad']['version']
|
||||||
|
self.supported_vers =\
|
||||||
|
phabricator.ARCRC['launchpad']['supported_versions']
|
||||||
|
|
||||||
|
# Phabricator info
|
||||||
|
self.phab = phabricator.Phabricator()
|
||||||
|
self.phab_host = phabricator.ARCRC['config']['default'].replace(
|
||||||
|
'api/', '')
|
||||||
|
|
||||||
|
self.logger = logging.getLogger('lugito.connector.LPConnector')
|
||||||
|
|
||||||
|
# Add log level
|
||||||
|
ch = logging.StreamHandler()
|
||||||
|
|
||||||
|
formatter = logging.Formatter(
|
||||||
|
'%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||||
|
ch.setFormatter(formatter)
|
||||||
|
|
||||||
|
self.logger.addHandler(ch)
|
||||||
|
self.logger.setLevel(log_level)
|
||||||
|
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
|
"""Connect"""
|
||||||
|
|
||||||
|
self.logger.info("Connecting to Launchpad")
|
||||||
|
|
||||||
|
|
||||||
|
def send(self, objectstr, who, body, link):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def listen(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
|
||||||
|
obj = LPConnector()
|
||||||
|
|
@ -0,0 +1,257 @@
|
|||||||
|
#! /usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
:mod:`lugito.lugito`
|
||||||
|
======================================
|
||||||
|
|
||||||
|
Defines a class to interact with Phabricator
|
||||||
|
|
||||||
|
.. currentmodule:: lugito.lugito
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import hmac
|
||||||
|
import http
|
||||||
|
import logging
|
||||||
|
import phabricator
|
||||||
|
from hashlib import sha256
|
||||||
|
|
||||||
|
PHAB_WEBHOOK_SIG = "X-Phabricator-Webhook-Signature"
|
||||||
|
|
||||||
|
COMMIT = "CMIT"
|
||||||
|
DIFF_REV = "DREV"
|
||||||
|
TASK = "TASK"
|
||||||
|
|
||||||
|
|
||||||
|
class Lugito(object):
|
||||||
|
|
||||||
|
def __init__(self, log_level=logging.DEBUG):
|
||||||
|
"""
|
||||||
|
Initialise
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.phab = phabricator.Phabricator()
|
||||||
|
self.HMAC = phabricator.ARCRC['HMAC']
|
||||||
|
self.host = phabricator.ARCRC['config']['default']
|
||||||
|
|
||||||
|
self.logger = logging.getLogger('lugito.lugito')
|
||||||
|
|
||||||
|
# Add log level
|
||||||
|
ch = logging.StreamHandler()
|
||||||
|
|
||||||
|
formatter = logging.Formatter(
|
||||||
|
'%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||||
|
ch.setFormatter(formatter)
|
||||||
|
|
||||||
|
self.logger.addHandler(ch)
|
||||||
|
self.logger.setLevel(log_level)
|
||||||
|
|
||||||
|
for key, val in self.HMAC.items():
|
||||||
|
self.HMAC[key] = bytes(u'%s' % val, 'utf-8')
|
||||||
|
|
||||||
|
|
||||||
|
def _transaction_search(self):
|
||||||
|
self.transaction = self.phab.transaction.search(objectIdentifier=
|
||||||
|
self.request_data["object"]["phid"])["data"]
|
||||||
|
|
||||||
|
def _request_data(self, request):
|
||||||
|
self.request_data = json.loads(request.data)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_HMAC(self, hmac_key, request):
|
||||||
|
"""
|
||||||
|
Check a request originated from Phabricator. This method must be called
|
||||||
|
first to validate a request is from Phabricator before any other method
|
||||||
|
can be called
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
|
||||||
|
hmac_key: str
|
||||||
|
The dictionary key corresponding to the HMAC token for the specifid webhook
|
||||||
|
|
||||||
|
request: flask request object
|
||||||
|
The request object provided by the flask route decorator
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
|
||||||
|
result: boolean
|
||||||
|
True if the request matches the specified HMAC key, False if not
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
hash_ = hmac.new(self.HMAC[hmac_key], request.data, sha256)
|
||||||
|
|
||||||
|
# check if from phabricator
|
||||||
|
if hash_.hexdigest() == request.headers[PHAB_WEBHOOK_SIG]:
|
||||||
|
self._request_data(request)
|
||||||
|
self._transaction_search()
|
||||||
|
self.logger.info('received phid: %s' %\
|
||||||
|
self.request_data["object"]["phid"])
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_object_type(self):
|
||||||
|
"""
|
||||||
|
Get object type from a request
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
|
||||||
|
request: flask request object
|
||||||
|
The request object provided by the flask route decorator
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
|
||||||
|
object_type: str or None
|
||||||
|
The object type from a request
|
||||||
|
|
||||||
|
"""
|
||||||
|
object_type = self.request_data["object"]["type"]
|
||||||
|
self.logger.debug('get_object_type: %s' % object_type)
|
||||||
|
return object_type
|
||||||
|
|
||||||
|
|
||||||
|
def get_author_fullname(self):
|
||||||
|
"""
|
||||||
|
Get author fullname from a request
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
|
||||||
|
request: flask request object
|
||||||
|
The request object provided by the flask route decorator
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
|
||||||
|
author_name: str or None
|
||||||
|
The fullname if the author object exists. If the object doesn't
|
||||||
|
exist a blank string is returned.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Find the author too.
|
||||||
|
userlookup = self.transaction[0]["authorPHID"]
|
||||||
|
who = dict(self.phab.phid.query(
|
||||||
|
phids=[userlookup]))[userlookup]["fullName"]
|
||||||
|
|
||||||
|
self.logger.debug('get_author_fullname: %s' % who)
|
||||||
|
return who
|
||||||
|
|
||||||
|
# If the object exists, no worries, let's just return a good response.
|
||||||
|
except http.client.HTTPException:
|
||||||
|
self.logger.info('get_author_fullname is None')
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_object_string(self, key): #pragma: no cover
|
||||||
|
|
||||||
|
phid = self.request_data["object"]["phid"]
|
||||||
|
return self.phab.phid.query(phids=[phid])[phid][key]
|
||||||
|
|
||||||
|
|
||||||
|
def get_commit_message(self):
|
||||||
|
"""
|
||||||
|
Get the commit message
|
||||||
|
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
|
||||||
|
commit_message: str or None
|
||||||
|
The commit message for the request if the author object exists.
|
||||||
|
If the object doesn't exist a blank string is returned.
|
||||||
|
|
||||||
|
"""
|
||||||
|
fullName = self.get_object_string(self.request_data, "fullName")
|
||||||
|
name = self.get_object_string(self.request_data, "name")
|
||||||
|
|
||||||
|
commitmessage = fullName.replace(name + ": ", "")
|
||||||
|
|
||||||
|
self.logger.debug('get_commit_message: %s' % commitmessage)
|
||||||
|
return commitmessage
|
||||||
|
|
||||||
|
|
||||||
|
def is_new_object(self):
|
||||||
|
"""
|
||||||
|
Is the request from a newly created object
|
||||||
|
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
|
||||||
|
new_object: boolean
|
||||||
|
True if a new searches else False
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
newtask = None
|
||||||
|
modified = None
|
||||||
|
for data in self.transaction:
|
||||||
|
if modified:
|
||||||
|
if (data["dateCreated"] == data["dateModified"])\
|
||||||
|
and (data["dateCreated"] == modified):
|
||||||
|
modified = data["dateCreated"]
|
||||||
|
newtask = True
|
||||||
|
else:
|
||||||
|
newtask = False
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
modified = data["dateCreated"]
|
||||||
|
|
||||||
|
return newtask
|
||||||
|
|
||||||
|
|
||||||
|
def is_comment(self, period=10):
|
||||||
|
"""
|
||||||
|
Is the request from a new or edited comment object
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
|
||||||
|
request: flask request object
|
||||||
|
The request object provided by the flask route decorator
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
|
||||||
|
is_new_comment: boolean
|
||||||
|
True if the request is from a new comment
|
||||||
|
|
||||||
|
is_edited_comment: boolean
|
||||||
|
True if the request is from an edtied comment
|
||||||
|
|
||||||
|
comment_id: string or None
|
||||||
|
The id of the comment to use as an HTML anchor or None if no comment
|
||||||
|
is referenced
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
is_new_comment = False
|
||||||
|
is_edited_comment = False
|
||||||
|
comment_id = None
|
||||||
|
for task in self.transaction:
|
||||||
|
dataepoch = self.request_data["action"]["epoch"]
|
||||||
|
datemodified = task["dateModified"]
|
||||||
|
|
||||||
|
# All comments within period seconds of the request are fair game.
|
||||||
|
if (dataepoch - period) <= datemodified <= (dataepoch + period) and\
|
||||||
|
task["comments"] != []:
|
||||||
|
|
||||||
|
comment_id = task["id"]
|
||||||
|
|
||||||
|
if datemodified != task["dateCreated"]:
|
||||||
|
is_new_comment = False
|
||||||
|
is_edited_comment = True
|
||||||
|
else:
|
||||||
|
is_new_comment = True
|
||||||
|
is_edited_comment = False
|
||||||
|
break
|
||||||
|
|
||||||
|
return (is_new_comment, is_edited_comment, comment_id)
|
@ -0,0 +1,142 @@
|
|||||||
|
#! /usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# S.D.G
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
:mod:`lugito.webhooks`
|
||||||
|
======================================
|
||||||
|
|
||||||
|
Lugito webhooks
|
||||||
|
|
||||||
|
.. currentmodule:: lugito.webhooks
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Imports
|
||||||
|
import logging
|
||||||
|
import threading
|
||||||
|
from flask import Flask, request
|
||||||
|
from lugito import Lugito
|
||||||
|
from lugito.connectors.irc import IRCConnector
|
||||||
|
|
||||||
|
# Constants
|
||||||
|
GLOBAL_LOG_LEVEL = logging.DEBUG
|
||||||
|
|
||||||
|
# Instantiate Lugito and connectors
|
||||||
|
lugito = Lugito(GLOBAL_LOG_LEVEL)
|
||||||
|
WEBSITE = lugito.host.replace('/api/', '')
|
||||||
|
|
||||||
|
irc_con = IRCConnector()
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
logger = logging.getLogger('lugito.webhooks')
|
||||||
|
|
||||||
|
# Add log level
|
||||||
|
ch = logging.StreamHandler()
|
||||||
|
|
||||||
|
formatter = logging.Formatter(
|
||||||
|
'%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||||
|
ch.setFormatter(formatter)
|
||||||
|
|
||||||
|
logger.addHandler(ch)
|
||||||
|
logger.setLevel(GLOBAL_LOG_LEVEL)
|
||||||
|
|
||||||
|
# Flask
|
||||||
|
app = Flask('lugito')
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/irc", methods=["POST"])
|
||||||
|
def _main():
|
||||||
|
"""Main route"""
|
||||||
|
|
||||||
|
if lugito.validate_HMAC('irc', request):
|
||||||
|
|
||||||
|
author = lugito.get_author_fullname()
|
||||||
|
|
||||||
|
# Without the author we can't continue
|
||||||
|
if author is None:
|
||||||
|
return 'Ok'
|
||||||
|
|
||||||
|
logger.debug("Object exists, checking to see if it's a task, diff "\
|
||||||
|
"or a commit.")
|
||||||
|
|
||||||
|
newtask = lugito.is_new_object()
|
||||||
|
is_new_comment, is_edited, comment_id = lugito.is_comment()
|
||||||
|
object_type = lugito.request_data["object"]["type"]
|
||||||
|
|
||||||
|
body = ""
|
||||||
|
link = ""
|
||||||
|
objectstr = lugito.get_object_string("fullName")
|
||||||
|
|
||||||
|
send_msg = True
|
||||||
|
# Determine what event produced the webhook call
|
||||||
|
if (object_type == "TASK") and newtask:
|
||||||
|
logger.debug("Object is a new task.")
|
||||||
|
body = "just created this task"
|
||||||
|
link = lugito.get_object_string("uri")
|
||||||
|
|
||||||
|
elif (object_type == "TASK") and (not newtask):
|
||||||
|
logger.debug("Object is NOT a new task.")
|
||||||
|
|
||||||
|
# Is it a new or edited comment
|
||||||
|
if is_new_comment and (not is_edited):
|
||||||
|
logger.debug("Object is a new comment.")
|
||||||
|
body = "commented on the task"
|
||||||
|
|
||||||
|
elif (not is_new_comment) and is_edited:
|
||||||
|
logger.debug("Object is an edited comment.")
|
||||||
|
body = "edited a message on the task"
|
||||||
|
|
||||||
|
if is_new_comment or is_edited:
|
||||||
|
link = lugito.get_object_string("uri")
|
||||||
|
link += "#" + str(comment_id)
|
||||||
|
logger.info(link)
|
||||||
|
|
||||||
|
else:
|
||||||
|
logger.debug("The object has already been processed")
|
||||||
|
send_msg = False
|
||||||
|
|
||||||
|
elif (object_type == "DREV") and newtask:
|
||||||
|
logger.debug("Object is a new diff.")
|
||||||
|
body = "just created this diff"
|
||||||
|
link = lugito.get_object_string("uri")
|
||||||
|
|
||||||
|
elif (object_type == "DREV") and (not newtask):
|
||||||
|
logger.debug("Object is NOT a new diff.")
|
||||||
|
|
||||||
|
# Is it a new or edited comment
|
||||||
|
if is_new_comment and (not is_edited):
|
||||||
|
logger.debug("Object is a new comment.")
|
||||||
|
body = "commented on the diff"
|
||||||
|
|
||||||
|
elif (not is_new_comment) and is_edited:
|
||||||
|
logger.debug("Object is an edited comment.")
|
||||||
|
body = "edited a message on the diff"
|
||||||
|
|
||||||
|
if is_new_comment or is_edited:
|
||||||
|
link = lugito.get_object_string("uri")
|
||||||
|
link += "#" + str(comment_id)
|
||||||
|
logger.info(link)
|
||||||
|
|
||||||
|
else:
|
||||||
|
logger.debug("The object has already been processed")
|
||||||
|
send_msg = False
|
||||||
|
|
||||||
|
elif object_type == "CMIT":
|
||||||
|
logger.debug("Object is a commit.")
|
||||||
|
body = "committed"
|
||||||
|
link = WEBSITE + "/" + lugito.get_object_string("name")
|
||||||
|
logger.info(link)
|
||||||
|
|
||||||
|
if send_msg:
|
||||||
|
irc_con.send(objectstr, author, body, link)
|
||||||
|
|
||||||
|
return 'Ok'
|
||||||
|
|
||||||
|
|
||||||
|
def run():
|
||||||
|
irc_con.connect()
|
||||||
|
t = threading.Thread(target=irc_con.listen)
|
||||||
|
t.daemon = True
|
||||||
|
t.start()
|
||||||
|
app.run(host="0.0.0.0", port=5000)
|
@ -0,0 +1,4 @@
|
|||||||
|
[pytest]
|
||||||
|
python_files = tests.py test_*.py *_tests.py
|
||||||
|
pytest_plugins = "pytest_cov", "pep8"
|
||||||
|
addopts = --doctest-modules --cov-config=.coveragerc --cov=lugito --cov-report=term-missing
|
@ -1,3 +1,35 @@
|
|||||||
flask
|
asn1crypto==0.24.0
|
||||||
launchpadlib
|
atomicwrites==1.2.1
|
||||||
phabricator
|
attrs==18.2.0
|
||||||
|
cffi==1.11.5
|
||||||
|
Click==7.0
|
||||||
|
coverage==4.5.2
|
||||||
|
cryptography==2.4.1
|
||||||
|
distro==1.3.0
|
||||||
|
entrypoints==0.2.3
|
||||||
|
Flask==1.0.2
|
||||||
|
httplib2==0.11.3
|
||||||
|
idna==2.7
|
||||||
|
itsdangerous==1.1.0
|
||||||
|
jeepney==0.4
|
||||||
|
Jinja2==2.10
|
||||||
|
keyring==16.0.2
|
||||||
|
launchpadlib==1.10.6
|
||||||
|
lazr.restfulclient==0.14.0
|
||||||
|
lazr.uri==1.0.3
|
||||||
|
-e git+ssh://git@phab.lubuntu.me:2222/source/lugito.git@165c866a5d5a184b0b36552334fa11aba95b5fb8#egg=lugito
|
||||||
|
MarkupSafe==1.1.0
|
||||||
|
more-itertools==4.3.0
|
||||||
|
oauthlib==2.1.0
|
||||||
|
pbr==5.1.1
|
||||||
|
phabricator==0.7.0
|
||||||
|
pluggy==0.8.0
|
||||||
|
py==1.7.0
|
||||||
|
pycparser==2.19
|
||||||
|
pytest==4.0.0
|
||||||
|
pytest-cov==2.6.0
|
||||||
|
SecretStorage==3.1.0
|
||||||
|
six==1.11.0
|
||||||
|
testresources==2.0.1
|
||||||
|
wadllib==1.3.3
|
||||||
|
Werkzeug==0.14.1
|
||||||
|
@ -0,0 +1,12 @@
|
|||||||
|
pip==18.1
|
||||||
|
bumpversion==0.5.3
|
||||||
|
wheel==0.32.1
|
||||||
|
watchdog==0.9.0
|
||||||
|
flake8==3.5.0
|
||||||
|
tox==3.5.2
|
||||||
|
coverage==4.5.1
|
||||||
|
Sphinx==1.8.1
|
||||||
|
twine==1.12.1
|
||||||
|
|
||||||
|
pytest==3.8.2
|
||||||
|
pytest-runner==4.2
|
@ -0,0 +1,38 @@
|
|||||||
|
[bumpversion]
|
||||||
|
current_version = 0.1.0
|
||||||
|
commit = True
|
||||||
|
tag = True
|
||||||
|
|
||||||
|
[bumpversion:file:setup.py]
|
||||||
|
search = version='{current_version}'
|
||||||
|
replace = version='{new_version}'
|
||||||
|
|
||||||
|
[bumpversion:file:lugito/__init__.py]
|
||||||
|
search = __version__ = '{current_version}'
|
||||||
|
replace = __version__ = '{new_version}'
|
||||||
|
|
||||||
|
[bdist_wheel]
|
||||||
|
universal = 1
|
||||||
|
|
||||||
|
[flake8]
|
||||||
|
exclude = docs
|
||||||
|
|
||||||
|
[aliases]
|
||||||
|
# Define setup.py command aliases here
|
||||||
|
test = pytest
|
||||||
|
|
||||||
|
[tool:pytest]
|
||||||
|
collect_ignore = ['setup.py']
|
||||||
|
|
||||||
|
|
||||||
|
# See the docstring in versioneer.py for instructions. Note that you must
|
||||||
|
# re-run 'versioneer.py setup' after changing this section, and commit the
|
||||||
|
# resulting files.
|
||||||
|
|
||||||
|
[versioneer]
|
||||||
|
VCS = git
|
||||||
|
style = pep440
|
||||||
|
versionfile_source = lugito/_version.py
|
||||||
|
versionfile_build = lugito/_version.py
|
||||||
|
tag_prefix = ''
|
||||||
|
parentdir_prefix = lugito-
|
@ -0,0 +1,67 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""The setup script."""
|
||||||
|
# import versioneer
|
||||||
|
from setuptools import setup, find_packages
|
||||||
|
|
||||||
|
with open('README.rst') as readme_file:
|
||||||
|
readme = readme_file.read()
|
||||||
|
|
||||||
|
with open('HISTORY.rst') as history_file:
|
||||||
|
history = history_file.read()
|
||||||
|
|
||||||
|
requirements = [
|
||||||
|
'Click>=6.0',
|
||||||
|
'Flask>=1.0.2',
|
||||||
|
'httplib2==0.11.3', # BJ - Currently the latest version of httplib2 (0.12.0)
|
||||||
|
# is not compatible with launchpadlib
|
||||||
|
'launchpadlib>=1.10.6',
|
||||||
|
'phabricator>=0.7.0',
|
||||||
|
'versioneer>=0.18',
|
||||||
|
]
|
||||||
|
|
||||||
|
setup_requirements = ['pytest-runner', ]
|
||||||
|
|
||||||
|
test_requirements = [
|
||||||
|
'pytest',
|
||||||
|
'pytest-cov',
|
||||||
|
]
|
||||||
|
|
||||||
|
setup(
|
||||||
|
author="",
|
||||||
|
author_email='',
|
||||||
|
classifiers=[
|
||||||
|
'Development Status :: 2 - Pre-Alpha',
|
||||||
|
'Intended Audience :: Developers',
|
||||||
|
'License :: OSI Approved :: BSD License',
|
||||||
|
'Natural Language :: English',
|
||||||
|
'Programming Language :: Python :: 3',
|
||||||
|
'Programming Language :: Python :: 3.4',
|
||||||
|
'Programming Language :: Python :: 3.5',
|
||||||
|
'Programming Language :: Python :: 3.6',
|
||||||
|
'Programming Language :: Python :: 3.7',
|
||||||
|
],
|
||||||
|
description="Python Boilerplate contains all the boilerplate "\
|
||||||
|
"you need to create a Python package.",
|
||||||
|
entry_points={
|
||||||
|
'console_scripts': [
|
||||||
|
'lugito=lugito.cli:run',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
install_requires=requirements,
|
||||||
|
license="BSD license",
|
||||||
|
long_description=readme + '\n\n' + history,
|
||||||
|
include_package_data=True,
|
||||||
|
keywords='lugito',
|
||||||
|
name='lugito',
|
||||||
|
packages=find_packages(exclude=['*tests']),
|
||||||
|
setup_requires=setup_requirements,
|
||||||
|
test_suite='tests',
|
||||||
|
tests_require=test_requirements,
|
||||||
|
url='',
|
||||||
|
version='0.1.0',
|
||||||
|
zip_safe=False,
|
||||||
|
# version=versioneer.get_version(),
|
||||||
|
# cmdclass=versioneer.get_cmdclass(),
|
||||||
|
)
|
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"hosts": {
|
||||||
|
"http://127.0.0.1:9091/api/": {
|
||||||
|
"token": "api-nojs2ip33hmp4zn6u6cf72w7d6yh"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"default": "http://127.0.0.1:9091/api/"
|
||||||
|
},
|
||||||
|
"HMAC": {
|
||||||
|
"diffhook": "vglzi6t4gsumnilv27r27no7rs3vgs75",
|
||||||
|
"commithook": "znkyfflbcia5gviqx5ybad7s6uyfywxi"
|
||||||
|
},
|
||||||
|
"irc": {
|
||||||
|
"host": "irc.freenode.net",
|
||||||
|
"port": "6697",
|
||||||
|
"username": "someusername",
|
||||||
|
"password": "somepassword",
|
||||||
|
"channel": "#somechannel"
|
||||||
|
},
|
||||||
|
"launchpad": {
|
||||||
|
"application": "lugito",
|
||||||
|
"staging": "production",
|
||||||
|
"version": "devel",
|
||||||
|
"supported_versions": ["Cosmic", "Bionic", "Xenial", "Trusty"]
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"object": {
|
||||||
|
"type": "DREV",
|
||||||
|
"phid": "PHID-DREV-qxuxc6eankxb7rw7iusf"
|
||||||
|
},
|
||||||
|
"triggers": [
|
||||||
|
{
|
||||||
|
"phid": "PHID-HRUL-6shsdzbrq7d3kucru3nt"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"action": {
|
||||||
|
"test": false,
|
||||||
|
"silent": false,
|
||||||
|
"secure": false,
|
||||||
|
"epoch": 1541502424
|
||||||
|
},
|
||||||
|
"transactions": [
|
||||||
|
{
|
||||||
|
"phid": "PHID-XACT-DREV-34sap7mpfbsucjw"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"object": {
|
||||||
|
"type": "DREV",
|
||||||
|
"phid": "PHID-DREV-rzehcd6ciu4d5lcqss32"
|
||||||
|
},
|
||||||
|
"triggers": [
|
||||||
|
{
|
||||||
|
"phid": "PHID-USER-5cmhaqtkggymhvbyqdcv"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"action": {
|
||||||
|
"test": true,
|
||||||
|
"silent": false,
|
||||||
|
"secure": false,
|
||||||
|
"epoch": 1542157769
|
||||||
|
},
|
||||||
|
"transactions": [
|
||||||
|
{
|
||||||
|
"phid": "PHID-XACT-DREV-ysczigtbjgxwghd"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"phid": "PHID-XACT-DREV-5yesmolevks6nk2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"phid": "PHID-XACT-DREV-wbzhl2ctk7klx33"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"phid": "PHID-XACT-DREV-kjxh5l47uj77yfw"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"phid": "PHID-XACT-DREV-vgo26mo4fpmwp2n"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"phid": "PHID-XACT-DREV-5muazco3m4qjxqo"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"phid": "PHID-XACT-DREV-q4axjuuaarmc43x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"phid": "PHID-XACT-DREV-6ayyl5ky5r5vqib"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"phid": "PHID-XACT-DREV-przqed7mheiygzq"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"object": {
|
||||||
|
"type": "DREV",
|
||||||
|
"phid": "PHID-DREV-z5l6ecfaugf6abnwvjeg"
|
||||||
|
},
|
||||||
|
"triggers": [
|
||||||
|
{
|
||||||
|
"phid": "PHID-HRUL-6shsdzbrq7d3kucru3nt"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"action": {
|
||||||
|
"test": false,
|
||||||
|
"silent": false,
|
||||||
|
"secure": false,
|
||||||
|
"epoch": 1542019737
|
||||||
|
},
|
||||||
|
"transactions": [
|
||||||
|
{
|
||||||
|
"phid": "PHID-XACT-DREV-aklgm5tv34ajmcp"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -0,0 +1,50 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": 58,
|
||||||
|
"phid": "PHID-XACT-DREV-uzd7ragh2zwabgj",
|
||||||
|
"type": "comment",
|
||||||
|
"authorPHID": "PHID-USER-5cmhaqtkggymhvbyqdcv",
|
||||||
|
"objectPHID": "PHID-DREV-qxuxc6eankxb7rw7iusf",
|
||||||
|
"dateCreated": 1541589352,
|
||||||
|
"dateModified": 1541589352,
|
||||||
|
"comments": [
|
||||||
|
{
|
||||||
|
"id": 20,
|
||||||
|
"phid": "PHID-XCMT-hkoz5viifwgvvgzzvwee",
|
||||||
|
"version": 1,
|
||||||
|
"authorPHID": "PHID-USER-5cmhaqtkggymhvbyqdcv",
|
||||||
|
"dateCreated": 1541589352,
|
||||||
|
"dateModified": 1541589352,
|
||||||
|
"removed": false,
|
||||||
|
"content": {
|
||||||
|
"raw": ",mn,mn,mn"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fields": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 57,
|
||||||
|
"phid": "PHID-XACT-DREV-s4ttw2voosilkke",
|
||||||
|
"type": "comment",
|
||||||
|
"authorPHID": "PHID-USER-5cmhaqtkggymhvbyqdcv",
|
||||||
|
"objectPHID": "PHID-DREV-qxuxc6eankxb7rw7iusf",
|
||||||
|
"dateCreated": 1541589290,
|
||||||
|
"dateModified": 1541589290,
|
||||||
|
"comments": [
|
||||||
|
{
|
||||||
|
"id": 19,
|
||||||
|
"phid": "PHID-XCMT-tedacg7scqz67lfxtyoc",
|
||||||
|
"version": 1,
|
||||||
|
"authorPHID": "PHID-USER-5cmhaqtkggymhvbyqdcv",
|
||||||
|
"dateCreated": 1541589290,
|
||||||
|
"dateModified": 1541589290,
|
||||||
|
"removed": false,
|
||||||
|
"content": {
|
||||||
|
"raw": "lkjhkjhkjh"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fields": {}
|
||||||
|
}
|
||||||
|
]
|
@ -0,0 +1,243 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": 158,
|
||||||
|
"phid": "PHID-XACT-DREV-ysczigtbjgxwghd",
|
||||||
|
"type": "comment",
|
||||||
|
"authorPHID": "PHID-USER-5cmhaqtkggymhvbyqdcv",
|
||||||
|
"objectPHID": "PHID-DREV-rzehcd6ciu4d5lcqss32",
|
||||||
|
"dateCreated": 1542050802,
|
||||||
|
"dateModified": 1542157734,
|
||||||
|
"comments": [
|
||||||
|
{
|
||||||
|
"id": 56,
|
||||||
|
"phid": "PHID-XCMT-2y6tf2nhf4gbtn7yyiid",
|
||||||
|
"version": 3,
|
||||||
|
"authorPHID": "PHID-USER-5cmhaqtkggymhvbyqdcv",
|
||||||
|
"dateCreated": 1542157734,
|
||||||
|
"dateModified": 1542157734,
|
||||||
|
"removed": false,
|
||||||
|
"content": {
|
||||||
|
"raw": "edit comment 4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 50,
|
||||||
|
"phid": "PHID-XCMT-cj7zihk3sjghqm4tnqcm",
|
||||||
|
"version": 2,
|
||||||
|
"authorPHID": "PHID-USER-5cmhaqtkggymhvbyqdcv",
|
||||||
|
"dateCreated": 1542050934,
|
||||||
|
"dateModified": 1542050934,
|
||||||
|
"removed": false,
|
||||||
|
"content": {
|
||||||
|
"raw": "editing another comment"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 49,
|
||||||
|
"phid": "PHID-XCMT-ipf5ypa52hjiddsnprjs",
|
||||||
|
"version": 1,
|
||||||
|
"authorPHID": "PHID-USER-5cmhaqtkggymhvbyqdcv",
|
||||||
|
"dateCreated": 1542050802,
|
||||||
|
"dateModified": 1542050802,
|
||||||
|
"removed": false,
|
||||||
|
"content": {
|
||||||
|
"raw": "a new comment"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fields": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 157,
|
||||||
|
"phid": "PHID-XACT-DREV-5yesmolevks6nk2",
|
||||||
|
"type": "comment",
|
||||||
|
"authorPHID": "PHID-USER-5cmhaqtkggymhvbyqdcv",
|
||||||
|
"objectPHID": "PHID-DREV-rzehcd6ciu4d5lcqss32",
|
||||||
|
"dateCreated": 1542050466,
|
||||||
|
"dateModified": 1542157769,
|
||||||
|
"comments": [
|
||||||
|
{
|
||||||
|
"id": 55,
|
||||||
|
"phid": "PHID-XCMT-7ugmycz5xjf4txw4vxeh",
|
||||||
|
"version": 5,
|
||||||
|
"authorPHID": "PHID-USER-5cmhaqtkggymhvbyqdcv",
|
||||||
|
"dateCreated": 1542157769,
|
||||||
|
"dateModified": 1542157769,
|
||||||
|
"removed": false,
|
||||||
|
"content": {
|
||||||
|
"raw": " edit comment 3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 54,
|
||||||
|
"phid": "PHID-XCMT-sjgxizrji2oova2a3rsr",
|
||||||
|
"version": 4,
|
||||||
|
"authorPHID": "PHID-USER-5cmhaqtkggymhvbyqdcv",
|
||||||
|
"dateCreated": 1542157657,
|
||||||
|
"dateModified": 1542157657,
|
||||||
|
"removed": false,
|
||||||
|
"content": {
|
||||||
|
"raw": "edit comment 3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 51,
|
||||||
|
"phid": "PHID-XCMT-5nbt6zmh3iteqozndjaz",
|
||||||
|
"version": 3,
|
||||||
|
"authorPHID": "PHID-USER-5cmhaqtkggymhvbyqdcv",
|
||||||
|
"dateCreated": 1542051079,
|
||||||
|
"dateModified": 1542051079,
|
||||||
|
"removed": false,
|
||||||
|
"content": {
|
||||||
|
"raw": "another edited comment"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 48,
|
||||||
|
"phid": "PHID-XCMT-dsiy2lhvwadrp2xl2m3y",
|
||||||
|
"version": 2,
|
||||||
|
"authorPHID": "PHID-USER-5cmhaqtkggymhvbyqdcv",
|
||||||
|
"dateCreated": 1542050506,
|
||||||
|
"dateModified": 1542050506,
|
||||||
|
"removed": false,
|
||||||
|
"content": {
|
||||||
|
"raw": "an edited new comment"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 47,
|
||||||
|
"phid": "PHID-XCMT-6wflirrwqccfkaqppwn5",
|
||||||
|
"version": 1,
|
||||||
|
"authorPHID": "PHID-USER-5cmhaqtkggymhvbyqdcv",
|
||||||
|
"dateCreated": 1542050466,
|
||||||
|
"dateModified": 1542050466,
|
||||||
|
"removed": false,
|
||||||
|
"content": {
|
||||||
|
"raw": "a new comment"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fields": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 156,
|
||||||
|
"phid": "PHID-XACT-DREV-wbzhl2ctk7klx33",
|
||||||
|
"type": "comment",
|
||||||
|
"authorPHID": "PHID-USER-5cmhaqtkggymhvbyqdcv",
|
||||||
|
"objectPHID": "PHID-DREV-rzehcd6ciu4d5lcqss32",
|
||||||
|
"dateCreated": 1542050158,
|
||||||
|
"dateModified": 1542050414,
|
||||||
|
"comments": [
|
||||||
|
{
|
||||||
|
"id": 46,
|
||||||
|
"phid": "PHID-XCMT-qzwqn672ti54rvtuz64o",
|
||||||
|
"version": 3,
|
||||||
|
"authorPHID": "PHID-USER-5cmhaqtkggymhvbyqdcv",
|
||||||
|
"dateCreated": 1542050414,
|
||||||
|
"dateModified": 1542050414,
|
||||||
|
"removed": false,
|
||||||
|
"content": {
|
||||||
|
"raw": "an edited comment 2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 45,
|
||||||
|
"phid": "PHID-XCMT-dfxoavdijxvdiacg3tuy",
|
||||||
|
"version": 2,
|
||||||
|
"authorPHID": "PHID-USER-5cmhaqtkggymhvbyqdcv",
|
||||||
|
"dateCreated": 1542050213,
|
||||||
|
"dateModified": 1542050213,
|
||||||
|
"removed": false,
|
||||||
|
"content": {
|
||||||
|
"raw": "an edited comment"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 44,
|
||||||
|
"phid": "PHID-XCMT-5lgf6nbudf4tueyggtzm",
|
||||||
|
"version": 1,
|
||||||
|
"authorPHID": "PHID-USER-5cmhaqtkggymhvbyqdcv",
|
||||||
|
"dateCreated": 1542050158,
|
||||||
|
"dateModified": 1542050158,
|
||||||
|
"removed": false,
|
||||||
|
"content": {
|
||||||
|
"raw": "a comment"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fields": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 155,
|
||||||
|
"phid": "PHID-XACT-DREV-kjxh5l47uj77yfw",
|
||||||
|
"type": null,
|
||||||
|
"authorPHID": "PHID-USER-5cmhaqtkggymhvbyqdcv",
|
||||||
|
"objectPHID": "PHID-DREV-rzehcd6ciu4d5lcqss32",
|
||||||
|
"dateCreated": 1542022249,
|
||||||
|
"dateModified": 1542022249,
|
||||||
|
"comments": [],
|
||||||
|
"fields": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 154,
|
||||||
|
"phid": "PHID-XACT-DREV-vgo26mo4fpmwp2n",
|
||||||
|
"type": null,
|
||||||
|
"authorPHID": "PHID-USER-5cmhaqtkggymhvbyqdcv",
|
||||||
|
"objectPHID": "PHID-DREV-rzehcd6ciu4d5lcqss32",
|
||||||
|
"dateCreated": 1542022249,
|
||||||
|
"dateModified": 1542022249,
|
||||||
|
"comments": [],
|
||||||
|
"fields": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 153,
|
||||||
|
"phid": "PHID-XACT-DREV-5muazco3m4qjxqo",
|
||||||
|
"type": null,
|
||||||
|
"authorPHID": "PHID-USER-5cmhaqtkggymhvbyqdcv",
|
||||||
|
"objectPHID": "PHID-DREV-rzehcd6ciu4d5lcqss32",
|
||||||
|
"dateCreated": 1542022249,
|
||||||
|
"dateModified": 1542022249,
|
||||||
|
"comments": [],
|
||||||
|
"fields": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 152,
|
||||||
|
"phid": "PHID-XACT-DREV-q4axjuuaarmc43x",
|
||||||
|
"type": "title",
|
||||||
|
"authorPHID": "PHID-USER-5cmhaqtkggymhvbyqdcv",
|
||||||
|
"objectPHID": "PHID-DREV-rzehcd6ciu4d5lcqss32",
|
||||||
|
"dateCreated": 1542022249,
|
||||||
|
"dateModified": 1542022249,
|
||||||
|
"comments": [],
|
||||||
|
"fields": {
|
||||||
|
"old": "",
|
||||||
|
"new": "pope-hamas"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 151,
|
||||||
|
"phid": "PHID-XACT-DREV-6ayyl5ky5r5vqib",
|
||||||
|
"type": "update",
|
||||||
|
"authorPHID": "PHID-USER-5cmhaqtkggymhvbyqdcv",
|
||||||
|
"objectPHID": "PHID-DREV-rzehcd6ciu4d5lcqss32",
|
||||||
|
"dateCreated": 1542022249,
|
||||||
|
"dateModified": 1542022249,
|
||||||
|
"comments": [],
|
||||||
|
"fields": {
|
||||||
|
"old": null,
|
||||||
|
"new": "PHID-DIFF-3u3lmekxjyeoq6xaxfd4",
|
||||||
|
"commitPHIDs": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 150,
|
||||||
|
"phid": "PHID-XACT-DREV-przqed7mheiygzq",
|
||||||
|
"type": "create",
|
||||||
|
"authorPHID": "PHID-USER-5cmhaqtkggymhvbyqdcv",
|
||||||
|
"objectPHID": "PHID-DREV-rzehcd6ciu4d5lcqss32",
|
||||||
|
"dateCreated": 1542022249,
|
||||||
|
"dateModified": 1542022249,
|
||||||
|
"comments": [],
|
||||||
|
"fields": {}
|
||||||
|
}
|
||||||
|
]
|
@ -0,0 +1,171 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": 136,
|
||||||
|
"phid": "PHID-XACT-DREV-wzobfd22ohrg6m4",
|
||||||
|
"type": "comment",
|
||||||
|
"authorPHID": "PHID-USER-5cmhaqtkggymhvbyqdcv",
|
||||||
|
"objectPHID": "PHID-DREV-z5l6ecfaugf6abnwvjeg",
|
||||||
|
"dateCreated": 1542020438,
|
||||||
|
"dateModified": 1542020438,
|
||||||
|
"comments": [
|
||||||
|
{
|
||||||
|
"id": 35,
|
||||||
|
"phid": "PHID-XCMT-x4n2osxvqizmnhymiq5d",
|
||||||
|
"version": 1,
|
||||||
|
"authorPHID": "PHID-USER-5cmhaqtkggymhvbyqdcv",
|
||||||
|
"dateCreated": 1542020438,
|
||||||
|
"dateModified": 1542020438,
|
||||||
|
"removed": false,
|
||||||
|
"content": {
|
||||||
|
"raw": "new comment"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fields": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 135,
|
||||||
|
"phid": "PHID-XACT-DREV-l6axinwdgij5qee",
|
||||||
|
"type": "comment",
|
||||||
|
"authorPHID": "PHID-USER-5cmhaqtkggymhvbyqdcv",
|
||||||
|
"objectPHID": "PHID-DREV-z5l6ecfaugf6abnwvjeg",
|
||||||
|
"dateCreated": 1542019934,
|
||||||
|
"dateModified": 1542019934,
|
||||||
|
"comments": [
|
||||||
|
{
|
||||||
|
"id": 34,
|
||||||
|
"phid": "PHID-XCMT-57ax765vxfwbsumfn2mb",
|
||||||
|
"version": 1,
|
||||||
|
"authorPHID": "PHID-USER-5cmhaqtkggymhvbyqdcv",
|
||||||
|
"dateCreated": 1542019934,
|
||||||
|
"dateModified": 1542019934,
|
||||||
|
"removed": false,
|
||||||
|
"content": {
|
||||||
|
"raw": "some other comment"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fields": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 134,
|
||||||
|
"phid": "PHID-XACT-DREV-dguhwsggckgiynl",
|
||||||
|
"type": "comment",
|
||||||
|
"authorPHID": "PHID-USER-5cmhaqtkggymhvbyqdcv",
|
||||||
|
"objectPHID": "PHID-DREV-z5l6ecfaugf6abnwvjeg",
|
||||||
|
"dateCreated": 1542019837,
|
||||||
|
"dateModified": 1542019837,
|
||||||
|
"comments": [
|
||||||
|
{
|
||||||
|
"id": 33,
|
||||||
|
"phid": "PHID-XCMT-yoyikx5jpuixc5baxiu4",
|
||||||
|
"version": 1,
|
||||||
|
"authorPHID": "PHID-USER-5cmhaqtkggymhvbyqdcv",
|
||||||
|
"dateCreated": 1542019837,
|
||||||
|
"dateModified": 1542019837,
|
||||||
|
"removed": false,
|
||||||
|
"content": {
|
||||||
|
"raw": "some other comment"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fields": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 133,
|
||||||
|
"phid": "PHID-XACT-DREV-aklgm5tv34ajmcp",
|
||||||
|
"type": "comment",
|
||||||
|
"authorPHID": "PHID-USER-5cmhaqtkggymhvbyqdcv",
|
||||||
|
"objectPHID": "PHID-DREV-z5l6ecfaugf6abnwvjeg",
|
||||||
|
"dateCreated": 1542019737,
|
||||||
|
"dateModified": 1542019737,
|
||||||
|
"comments": [
|
||||||
|
{
|
||||||
|
"id": 32,
|
||||||
|
"phid": "PHID-XCMT-b7k3k2tzyfrduaymeauo",
|
||||||
|
"version": 1,
|
||||||
|
"authorPHID": "PHID-USER-5cmhaqtkggymhvbyqdcv",
|
||||||
|
"dateCreated": 1542019737,
|
||||||
|
"dateModified": 1542019737,
|
||||||
|
"removed": false,
|
||||||
|
"content": {
|
||||||
|
"raw": "a comment here"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fields": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 132,
|
||||||
|
"phid": "PHID-XACT-DREV-jxajlff675zoqik",
|
||||||
|
"type": null,
|
||||||
|
"authorPHID": "PHID-USER-5cmhaqtkggymhvbyqdcv",
|
||||||
|
"objectPHID": "PHID-DREV-z5l6ecfaugf6abnwvjeg",
|
||||||
|
"dateCreated": 1542014500,
|
||||||
|
"dateModified": 1542014500,
|
||||||
|
"comments": [],
|
||||||
|
"fields": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 131,
|
||||||
|
"phid": "PHID-XACT-DREV-x6qr3lgaxukqawh",
|
||||||
|
"type": null,
|
||||||
|
"authorPHID": "PHID-USER-5cmhaqtkggymhvbyqdcv",
|
||||||
|
"objectPHID": "PHID-DREV-z5l6ecfaugf6abnwvjeg",
|
||||||
|
"dateCreated": 1542014500,
|
||||||
|
"dateModified": 1542014500,
|
||||||
|
"comments": [],
|
||||||
|
"fields": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 130,
|
||||||
|
"phid": "PHID-XACT-DREV-6eulucm7uftvapb",
|
||||||
|
"type": null,
|
||||||
|
"authorPHID": "PHID-USER-5cmhaqtkggymhvbyqdcv",
|
||||||
|
"objectPHID": "PHID-DREV-z5l6ecfaugf6abnwvjeg",
|
||||||
|
"dateCreated": 1542014500,
|
||||||
|
"dateModified": 1542014500,
|
||||||
|
"comments": [],
|
||||||
|
"fields": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 129,
|
||||||
|
"phid": "PHID-XACT-DREV-qq26utftpfwjtjo",
|
||||||
|
"type": "title",
|
||||||
|
"authorPHID": "PHID-USER-5cmhaqtkggymhvbyqdcv",
|
||||||
|
"objectPHID": "PHID-DREV-z5l6ecfaugf6abnwvjeg",
|
||||||
|
"dateCreated": 1542014500,
|
||||||
|
"dateModified": 1542014500,
|
||||||
|
"comments": [],
|
||||||
|
"fields": {
|
||||||
|
"old": "",
|
||||||
|
"new": "create diff again"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 128,
|
||||||
|
"phid": "PHID-XACT-DREV-aawvabsf6kfhksh",
|
||||||
|
"type": "update",
|
||||||
|
"authorPHID": "PHID-USER-5cmhaqtkggymhvbyqdcv",
|
||||||
|
"objectPHID": "PHID-DREV-z5l6ecfaugf6abnwvjeg",
|
||||||
|
"dateCreated": 1542014500,
|
||||||
|
"dateModified": 1542014500,
|
||||||
|
"comments": [],
|
||||||
|
"fields": {
|
||||||
|
"old": null,
|
||||||
|
"new": "PHID-DIFF-bsvtdhdz63wq65wjttqg",
|
||||||
|
"commitPHIDs": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 127,
|
||||||
|
"phid": "PHID-XACT-DREV-j2a6dzebwmdxmvh",
|
||||||
|
"type": "create",
|
||||||
|
"authorPHID": "PHID-USER-5cmhaqtkggymhvbyqdcv",
|
||||||
|
"objectPHID": "PHID-DREV-z5l6ecfaugf6abnwvjeg",
|
||||||
|
"dateCreated": 1542014500,
|
||||||
|
"dateModified": 1542014500,
|
||||||
|
"comments": [],
|
||||||
|
"fields": {}
|
||||||
|
}
|
||||||
|
]
|
@ -0,0 +1,97 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": 101,
|
||||||
|
"phid": "PHID-XACT-DREV-gfcfrpinnv4spcp",
|
||||||
|
"type": null,
|
||||||
|
"authorPHID": "PHID-USER-5cmhaqtkggymhvbyqdcv",
|
||||||
|
"objectPHID": "PHID-DREV-nwspiigk5kbatjniv5jk",
|
||||||
|
"dateCreated": 1541930934,
|
||||||
|
"dateModified": 1541930934,
|
||||||
|
"comments": [],
|
||||||
|
"fields": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 100,
|
||||||
|
"phid": "PHID-XACT-DREV-mfbe2f53t2gxwyl",
|
||||||
|
"type": null,
|
||||||
|
"authorPHID": "PHID-USER-5cmhaqtkggymhvbyqdcv",
|
||||||
|
"objectPHID": "PHID-DREV-nwspiigk5kbatjniv5jk",
|
||||||
|
"dateCreated": 1541930934,
|
||||||
|
"dateModified": 1541930934,
|
||||||
|
"comments": [],
|
||||||
|
"fields": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 99,
|
||||||
|
"phid": "PHID-XACT-DREV-7vy7l2d5hlgxaky",
|
||||||
|
"type": null,
|
||||||
|
"authorPHID": "PHID-USER-5cmhaqtkggymhvbyqdcv",
|
||||||
|
"objectPHID": "PHID-DREV-nwspiigk5kbatjniv5jk",
|
||||||
|
"dateCreated": 1541930934,
|
||||||
|
"dateModified": 1541930934,
|
||||||
|
"comments": [],
|
||||||
|
"fields": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 98,
|
||||||
|
"phid": "PHID-XACT-DREV-jhjbnikjbbwwfbw",
|
||||||
|
"type": null,
|
||||||
|
"authorPHID": "PHID-USER-5cmhaqtkggymhvbyqdcv",
|
||||||
|
"objectPHID": "PHID-DREV-nwspiigk5kbatjniv5jk",
|
||||||
|
"dateCreated": 1541930934,
|
||||||
|
"dateModified": 1541930934,
|
||||||
|
"comments": [],
|
||||||
|
"fields": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 97,
|
||||||
|
"phid": "PHID-XACT-DREV-l3ixndqztyuoocw",
|
||||||
|
"type": null,
|
||||||
|
"authorPHID": "PHID-USER-5cmhaqtkggymhvbyqdcv",
|
||||||
|
"objectPHID": "PHID-DREV-nwspiigk5kbatjniv5jk",
|
||||||
|
"dateCreated": 1541930934,
|
||||||
|
"dateModified": 1541930934,
|
||||||
|
"comments": [],
|
||||||
|
"fields": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 96,
|
||||||
|
"phid": "PHID-XACT-DREV-v32dun652f2hcbc",
|
||||||
|
"type": "title",
|
||||||
|
"authorPHID": "PHID-USER-5cmhaqtkggymhvbyqdcv",
|
||||||
|
"objectPHID": "PHID-DREV-nwspiigk5kbatjniv5jk",
|
||||||
|
"dateCreated": 1541930934,
|
||||||
|
"dateModified": 1541930934,
|
||||||
|
"comments": [],
|
||||||
|
"fields": {
|
||||||
|
"old": "",
|
||||||
|
"new": "a new diff erv"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 95,
|
||||||
|
"phid": "PHID-XACT-DREV-f2ijnff2fzpafef",
|
||||||
|
"type": "update",
|
||||||
|
"authorPHID": "PHID-USER-5cmhaqtkggymhvbyqdcv",
|
||||||
|
"objectPHID": "PHID-DREV-nwspiigk5kbatjniv5jk",
|
||||||
|
"dateCreated": 1541930934,
|
||||||
|
"dateModified": 1541930934,
|
||||||
|
"comments": [],
|
||||||
|
"fields": {
|
||||||
|
"old": null,
|
||||||
|
"new": "PHID-DIFF-7s75ei7xmgmgselnszd5",
|
||||||
|
"commitPHIDs": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 94,
|
||||||
|
"phid": "PHID-XACT-DREV-spddbfytwd6ziqi",
|
||||||
|
"type": "create",
|
||||||
|
"authorPHID": "PHID-USER-5cmhaqtkggymhvbyqdcv",
|
||||||
|
"objectPHID": "PHID-DREV-nwspiigk5kbatjniv5jk",
|
||||||
|
"dateCreated": 1541930934,
|
||||||
|
"dateModified": 1541930934,
|
||||||
|
"comments": [],
|
||||||
|
"fields": {}
|
||||||
|
}
|
||||||
|
]
|
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"object":
|
||||||
|
{
|
||||||
|
"type": "DREV",
|
||||||
|
"phid": "PHID-DREV-qxuxc6eankxb7rw7iusf"
|
||||||
|
},
|
||||||
|
"triggers": [{"phid": "PHID-HRUL-6shsdzbrq7d3kucru3nt"}],
|
||||||
|
"action": {
|
||||||
|
"test": "False",
|
||||||
|
"silent": "False",
|
||||||
|
"secure": "False",
|
||||||
|
"epoch": 1541503686
|
||||||
|
},
|
||||||
|
"transactions": [{"phid": "PHID-XACT-DREV-2swyqdhax7hkc5r"}]
|
||||||
|
}
|
@ -0,0 +1,49 @@
|
|||||||
|
#! /usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# S.D.G
|
||||||
|
|
||||||
|
"""
|
||||||
|
Test IRC connector
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Imports
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import phabricator
|
||||||
|
from lugito import IRCConnector
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
# Setup ###############################################################
|
||||||
|
|
||||||
|
TEST_DIR = os.path.dirname(__file__)
|
||||||
|
|
||||||
|
# Force phabricator to use the ./tests/.arcconfig file
|
||||||
|
TEST_CONFIG = os.path.join(TEST_DIR, '.arcconfig')
|
||||||
|
|
||||||
|
with open(TEST_CONFIG, 'r') as f:
|
||||||
|
phabricator.ARCRC = json.load(f)
|
||||||
|
|
||||||
|
# Tests ###############################################################
|
||||||
|
|
||||||
|
def test_init():
|
||||||
|
"""Test initialise irc connector"""
|
||||||
|
|
||||||
|
obj = IRCConnector()
|
||||||
|
|
||||||
|
assert('irc.freenode.net' == obj.host)
|
||||||
|
assert(6697 == obj.port)
|
||||||
|
assert('someusername' == obj.username)
|
||||||
|
assert('somepassword' == obj.password)
|
||||||
|
assert('#somechannel' == obj.channel)
|
||||||
|
assert('http://127.0.0.1:9091/' == obj.phab_host)
|
||||||
|
|
||||||
|
|
||||||
|
def test_connect():
|
||||||
|
"""Test initial connection"""
|
||||||
|
|
||||||
|
obj = IRCConnector()
|
||||||
|
|
||||||
|
obj._socket_conn = MagicMock()
|
||||||
|
|
||||||
|
# obj.conn.recv = MagicMock(side_effect=[
|
||||||
|
|
@ -0,0 +1,27 @@
|
|||||||
|
#! /usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# S.D.G
|
||||||
|
|
||||||
|
"""
|
||||||
|
Test launchpad connector
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Imports
|
||||||
|
import json
|
||||||
|
import phabricator
|
||||||
|
from lugito.connectors.launchpad import LPConnector
|
||||||
|
|
||||||
|
# Setup ###############################################################
|
||||||
|
|
||||||
|
TEST_DIR = os.path.dirname(__file__)
|
||||||
|
|
||||||
|
# Force phabricator to use the ./tests/.arcconfig file
|
||||||
|
TEST_CONFIG = os.path.join(TEST_DIR, '.arcconfig')
|
||||||
|
|
||||||
|
with open(TEST_CONFIG, 'r') as f:
|
||||||
|
phabricator.ARCRC = json.load(f)
|
||||||
|
|
||||||
|
# Tests ###############################################################
|
||||||
|
|
||||||
|
|
||||||
|
def test_init()
|
@ -0,0 +1,208 @@
|
|||||||
|
#! /usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# S.D.G
|
||||||
|
|
||||||
|
"""
|
||||||
|
lugito tests
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Imports
|
||||||
|
import os
|
||||||
|
import pytest
|
||||||
|
import phabricator
|
||||||
|
import json
|
||||||
|
import http
|
||||||
|
from lugito import Lugito
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
# Setup ###############################################################
|
||||||
|
|
||||||
|
TEST_DIR = os.path.dirname(__file__)
|
||||||
|
|
||||||
|
# Force phabricator to use the ./tests/.arcconfig file
|
||||||
|
TEST_CONFIG = os.path.join(TEST_DIR, '.arcconfig')
|
||||||
|
|
||||||
|
with open(TEST_CONFIG, 'r') as f:
|
||||||
|
phabricator.ARCRC = json.load(f)
|
||||||
|
|
||||||
|
# Pre-prepared request
|
||||||
|
FAKE_REQUEST = os.path.join(TEST_DIR, 'request.json')
|
||||||
|
|
||||||
|
FAKE_REQ_DATA = os.path.join(TEST_DIR, 'fake_req_data.json')
|
||||||
|
FAKE_TRANSACTION = os.path.join(TEST_DIR, 'fake_transaction.json')
|
||||||
|
FAKE_TRANSACTION_NEW_OBJECT = os.path.join(
|
||||||
|
TEST_DIR, 'fake_transaction_new_object.json')
|
||||||
|
|
||||||
|
FAKE_REQ_NEW_COMMENT = os.path.join(TEST_DIR, 'fake_req_data_new_comment.json')
|
||||||
|
FAKE_NEW_COMMENT = os.path.join(TEST_DIR, 'fake_transaction_new_comment.json')
|
||||||
|
|
||||||
|
FAKE_REQ_EDITED_COMMENT = os.path.join(TEST_DIR,
|
||||||
|
'fake_req_data_edit_comment.json')
|
||||||
|
FAKE_EDITED_COMMENT = os.path.join(TEST_DIR,
|
||||||
|
'fake_transaction_edit_comment.json')
|
||||||
|
|
||||||
|
# Tests ###############################################################
|
||||||
|
|
||||||
|
def test_init():
|
||||||
|
"""Test initialise lugito"""
|
||||||
|
|
||||||
|
obj = Lugito()
|
||||||
|
|
||||||
|
assert('http://127.0.0.1:9091/api/' in obj.phab.host)
|
||||||
|
assert('api-nojs2ip33hmp4zn6u6cf72w7d6yh' in obj.phab.token)
|
||||||
|
assert(obj.HMAC['diffhook'] == bytes(u'vglzi6t4gsumnilv27r27no7rs3vgs75',
|
||||||
|
'utf-8'))
|
||||||
|
assert(obj.HMAC['commithook'] == bytes(u'znkyfflbcia5gviqx5ybad7s6uyfywxi',
|
||||||
|
'utf-8'))
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_HMAC():
|
||||||
|
"""Test validating HMAC"""
|
||||||
|
|
||||||
|
obj = Lugito()
|
||||||
|
|
||||||
|
request_mock = MagicMock()
|
||||||
|
|
||||||
|
with open(FAKE_REQ_DATA, 'r') as f:
|
||||||
|
request_mock.data = f.read().encode()
|
||||||
|
|
||||||
|
request_mock.headers = {
|
||||||
|
"X-Phabricator-Webhook-Signature":
|
||||||
|
"a8f636f03ed4464ddb398ea873ffab409d941f87396f28fa9d22bb58cfbedc9f"
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(obj.validate_HMAC('diffhook', request_mock))
|
||||||
|
|
||||||
|
|
||||||
|
def test_invalid_HMAC():
|
||||||
|
"""Test validating HMAC"""
|
||||||
|
|
||||||
|
obj = Lugito()
|
||||||
|
|
||||||
|
request_mock = MagicMock()
|
||||||
|
|
||||||
|
with open(FAKE_REQ_DATA, 'r') as f:
|
||||||
|
request_mock.data = f.read().encode()
|
||||||
|
|
||||||
|
request_mock.headers = {
|
||||||
|
"X-Phabricator-Webhook-Signature":
|
||||||
|
"a8f6364464ddb398ea873ffab409d941f87396f28fa9d22bb58cfbedc9f"
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(not obj.validate_HMAC('diffhook', request_mock))
|
||||||
|
|
||||||
|
|
||||||
|
def test_author_fullname():
|
||||||
|
"""Test get the author name"""
|
||||||
|
|
||||||
|
obj = Lugito()
|
||||||
|
|
||||||
|
with open(FAKE_REQ_DATA, 'r') as f:
|
||||||
|
obj.request_data = json.load(f)
|
||||||
|
|
||||||
|
with open(FAKE_TRANSACTION, 'r') as f:
|
||||||
|
obj.transaction = json.load(f)
|
||||||
|
|
||||||
|
obj.phab = MagicMock()
|
||||||
|
obj.phab.phid.query = MagicMock(return_value={
|
||||||
|
'PHID-USER-5cmhaqtkggymhvbyqdcv':{
|
||||||
|
'fullName': 'AuthorName',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
author_name = obj.get_author_fullname()
|
||||||
|
assert(author_name == 'AuthorName')
|
||||||
|
|
||||||
|
|
||||||
|
def test_author_fullname_error():
|
||||||
|
"""Test unable to get the author name"""
|
||||||
|
|
||||||
|
obj = Lugito()
|
||||||
|
|
||||||
|
with open(FAKE_REQ_DATA, 'r') as f:
|
||||||
|
obj.request_data = json.load(f)
|
||||||
|
|
||||||
|
with open(FAKE_TRANSACTION, 'r') as f:
|
||||||
|
obj.transaction = json.load(f)
|
||||||
|
|
||||||
|
obj.phab = MagicMock()
|
||||||
|
obj.phab.phid.query = MagicMock(side_effect=http.client.HTTPException)
|
||||||
|
|
||||||
|
author_name = obj.get_author_fullname()
|
||||||
|
assert(author_name is None)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_object_type():
|
||||||
|
"""Test get object type"""
|
||||||
|
|
||||||
|
obj = Lugito()
|
||||||
|
|
||||||
|
with open(FAKE_REQ_DATA, 'r') as f:
|
||||||
|
obj.request_data = json.load(f)
|
||||||
|
|
||||||
|
obj._transaction_search()
|
||||||
|
|
||||||
|
assert(obj.get_object_type() == 'DREV')
|
||||||
|
|
||||||
|
def test_is_new_object_false():
|
||||||
|
"""Test is new task - false"""
|
||||||
|
|
||||||
|
obj = Lugito()
|
||||||
|
|
||||||
|
with open(FAKE_REQ_DATA, 'r') as f:
|
||||||
|
obj.request_data = json.load(f)
|
||||||
|
|
||||||
|
with open(FAKE_TRANSACTION, 'r') as f:
|
||||||
|
obj.transaction = json.load(f)
|
||||||
|
|
||||||
|
assert (not obj.is_new_object())
|
||||||
|
|
||||||
|
def test_is_new_object_true():
|
||||||
|
"""Test is new task - true"""
|
||||||
|
|
||||||
|
obj = Lugito()
|
||||||
|
|
||||||
|
with open(FAKE_REQ_DATA, 'r') as f:
|
||||||
|
obj.request_data = json.load(f)
|
||||||
|
|
||||||
|
with open(FAKE_TRANSACTION_NEW_OBJECT, 'r') as f:
|
||||||
|
obj.transaction = json.load(f)
|
||||||
|
|
||||||
|
assert (obj.is_new_object())
|
||||||
|
|
||||||
|
|
||||||
|
def test_is_new_comment():
|
||||||
|
"""Test checking for new - using transaction search"""
|
||||||
|
|
||||||
|
obj = Lugito()
|
||||||
|
|
||||||
|
with open(FAKE_REQ_NEW_COMMENT, 'r') as f:
|
||||||
|
obj.request_data = json.load(f)
|
||||||
|
|
||||||
|
with open(FAKE_NEW_COMMENT, 'r') as f:
|
||||||
|
obj.transaction = json.load(f)
|
||||||
|
|
||||||
|
new_comment, edited, _id = obj.is_comment()
|
||||||
|
|
||||||
|
assert(new_comment)
|
||||||
|
assert(not edited)
|
||||||
|
assert(_id == 133)
|
||||||
|
|
||||||
|
|
||||||
|
def test_is_edited_comment():
|
||||||
|
"""Test checking for edited comment"""
|
||||||
|
|
||||||
|
obj = Lugito()
|
||||||
|
|
||||||
|
with open(FAKE_REQ_EDITED_COMMENT, 'r') as f:
|
||||||
|
obj.request_data = json.load(f)
|
||||||
|
|
||||||
|
with open(FAKE_EDITED_COMMENT, 'r') as f:
|
||||||
|
obj.transaction = json.load(f)
|
||||||
|
|
||||||
|
new_comment, edited, _id = obj.is_comment()
|
||||||
|
|
||||||
|
assert(not new_comment)
|
||||||
|
assert(edited)
|
||||||
|
assert(_id == 157)
|
@ -0,0 +1,28 @@
|
|||||||
|
[tox]
|
||||||
|
envlist = py27, py34, py35, py36, flake8
|
||||||
|
|
||||||
|
[travis]
|
||||||
|
python =
|
||||||
|
3.6: py36
|
||||||
|
3.5: py35
|
||||||
|
3.4: py34
|
||||||
|
2.7: py27
|
||||||
|
|
||||||
|
[testenv:flake8]
|
||||||
|
basepython = python
|
||||||
|
deps = flake8
|
||||||
|
commands = flake8 lugito
|
||||||
|
|
||||||
|
[testenv]
|
||||||
|
setenv =
|
||||||
|
PYTHONPATH = {toxinidir}
|
||||||
|
deps =
|
||||||
|
-r{toxinidir}/requirements_dev.txt
|
||||||
|
; If you want to make tox run the tests with the same versions, create a
|
||||||
|
; requirements.txt with the pinned versions and uncomment the following line:
|
||||||
|
; -r{toxinidir}/requirements.txt
|
||||||
|
commands =
|
||||||
|
pip install -U pip
|
||||||
|
py.test --basetemp={envtmpdir}
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in new issue