Initial fork from gertty -> boartty

Change-Id: I8c0ce5550f2287f77fb31c790c3923d3d1b80481
This commit is contained in:
James E. Blair
2016-10-15 07:31:19 -07:00
parent 6b8f18331f
commit e4c972b803
75 changed files with 4316 additions and 8643 deletions

2
.gitignore vendored
View File

@ -1,5 +1,5 @@
*.pyc
*.egg*
gertty-env
boartty-env
.tox
doc/build

View File

@ -1,4 +1,4 @@
[gerrit]
host=review.openstack.org
port=29418
project=openstack/gertty.git
project=openstack/boartty.git

View File

@ -1,10 +1,10 @@
Contributing
============
To browse the latest code, see: https://git.openstack.org/cgit/stackforge/gertty/tree/
To clone the latest code, use `git clone git://git.openstack.org/stackforge/gertty`
To browse the latest code, see: https://git.openstack.org/cgit/openstack/boartty/tree/
To clone the latest code, use `git clone git://git.openstack.org/openstack/boartty`
Bugs are handled at: https://storyboard.openstack.org/#!/project/698
Bugs are handled at: https://storyboard.openstack.org/
Code reviews are handled by gerrit at: https://review.openstack.org
@ -18,26 +18,19 @@ that links to your launchpad account). Example::
Philosophy
----------
Gertty is based on the following precepts which should inform changes
Boartty is based on the following precepts which should inform changes
to the program:
* Support large numbers of review requests across large numbers of
projects. Help the user prioritize those reviews.
* Support large numbers of stories across large numbers of projects.
* Adopt a news/mailreader-like workflow in support of the above.
Being able to subscribe to projects, mark reviews as "read" without
reviewing, etc, are all useful concepts to support a heavy review
load (they have worked extremely well in supporting people who
read/write a lot of mail/news).
* Support off-line use. Gertty should be completely usable off-line
with reliable syncing between local data and Gerrit when a
* Support off-line use. Boartty should be completely usable off-line
with reliable syncing between local data and Storyboard when a
connection is available (just like git or mail or news).
* Ample use of color. Unlike a web interface, a good text interface
relies mostly on color and precise placement rather than whitespace
and decoration to indicate to the user the purpose of a given piece
of information. Gertty should degrade well to 16 colors, but more
of information. Boartty should degrade well to 16 colors, but more
(88 or 256) may be used.
* Keyboard navigation (with easy-to-remember commands) should be
@ -51,10 +44,9 @@ to the program:
messages or comments) and navigating back intuitive (it matches
expectations set by the web browsers).
* Support a wide variety of Gerrit installations. The initial
development of Gertty is against the OpenStack project's Gerrit, and
many of the features are intended to help its developers with their
workflow, however, those features should be implemented in a generic
way so that the system does not require a specific Gerrit
configuration.
* Support a wide variety of Storyboard installations. The initial
development of Boartty is against the OpenStack project's
Storyboard, and many of the features are intended to help its
developers with their workflow, however, those features should be
implemented in a generic way so that the system does not require a
specific Storyboard configuration.

View File

@ -1,137 +1,84 @@
Gertty
======
Boartty
=======
Gertty is a console-based interface to the Gerrit Code Review system.
Boartty is a console-based interface to the Storyboard task-tracking
system.
As compared to the web interface, the main advantages are:
* Workflow -- the interface is designed to support a workflow similar
to reading network news or mail. In particular, it is designed to
deal with a large number of review requests across a large number
of projects.
deal with a large number of stories across a large number of
projects.
* Offline Use -- Gertty syncs information about changes in subscribed
projects to a local database and local git repos. All review
operations are performed against that database and then synced back
to Gerrit.
* Offline Use -- Boartty syncs information about changes in
subscribed projects to a local database. All review operations are
performed against that database and then synced back to Storyboard.
* Speed -- user actions modify locally cached content and need not
wait for server interaction.
* Convenience -- because Gertty downloads all changes to local git
repos, a single command instructs it to checkout a change into that
repo for detailed examination or testing of larger changes.
Installation
------------
Debian
~~~~~~
Gertty is packaged in Debian and is currently available in:
* unstable
* testing
* stable
You can install it with::
apt-get install gertty
Fedora
~~~~~~
Gertty is packaged starting in Fedora 21. You can install it with::
yum install python-gertty
openSUSE
~~~~~~~~
Gertty is packaged for openSUSE 13.1 onwards. You can install it via
`1-click install from the Open Build Service <http://software.opensuse.org/package/python-gertty>`_.
Gentoo
~~~~~~
Gertty is available in the main Gentoo repository. You can install it with::
emerge gertty
Arch Linux
~~~~~~~~~~
Gertty packages are available in the Arch User Repository packages. You
can get the package from::
https://aur.archlinux.org/packages/python2-gertty/
Source
~~~~~~
When installing from source, it is recommended (but not required) to
install Gertty in a virtualenv. To set one up::
install Boartty in a virtualenv. To set one up::
virtualenv gertty-env
source gertty-env/bin/activate
virtualenv boartty-env
source boartty-env/bin/activate
To install the latest version from the cheeseshop::
pip install gertty
pip install boartty
To install from a git checkout::
pip install .
Gertty uses a YAML based configuration file that it looks for at
``~/.gertty.yaml``. Several sample configuration files are included.
Boartty uses a YAML based configuration file that it looks for at
``~/.boartty.yaml``. Several sample configuration files are included.
You can find them in the examples/ directory of the
`source distribution <https://git.openstack.org/cgit/openstack/gertty/tree/examples>`_
or the share/gertty/examples directory after installation.
`source distribution <https://git.openstack.org/cgit/openstack/boartty/tree/examples>`_
or the share/boartty/examples directory after installation.
Select one of the sample config files, copy it to ~/.gertty.yaml and
Select one of the sample config files, copy it to ~/.boartty.yaml and
edit as necessary. Search for ``CHANGEME`` to find parameters that
need to be supplied. The sample config files are as follows:
**minimal-gertty.yaml**
Only contains the parameters required for Gertty to actually run.
**minimal-boartty.yaml**
Only contains the parameters required for Boartty to actually run.
**reference-gertty.yaml**
**reference-boartty.yaml**
An exhaustive list of all supported options with examples.
**openstack-gertty.yaml**
**openstack-boartty.yaml**
A configuration designed for use with OpenStack's installation of
Gerrit.
**googlesource-gertty.yaml**
A configuration designed for use with installations of Gerrit
running on googlesource.com.
You will need a Storyboard authentication token which you can generate
or retrieve by navigating to ``Profile``, then ``Tokens`` (the "key"
icon), or visiting the `/#!/profile/tokens` URI in your Storyboard
installation. Issue a new token if you have not done so before, and
give it a sufficiently long lifetime (for example, one decade). Copy
and paste the resulting token in your ``~/.boartty.yaml`` file.
You will need your Gerrit password which you can generate or retrieve
by navigating to ``Settings``, then ``HTTP Password``.
Gertty uses local git repositories to perform much of its work. These
can be the same git repositories that you use when developing a
project. Gertty will not alter the working directory or index unless
you request it to (and even then, the usual git safeguards against
accidentally losing work remain in place). You will need to supply
the name of a directory where Gertty will find or clone git
repositories for your projects as the ``git-root`` parameter.
The config file is designed to support multiple Gerrit instances. The
first one is used by default, but others can be specified by supplying
the name on the command line.
The config file is designed to support multiple Storyboard instances.
The first one is used by default, but others can be specified by
supplying the name on the command line.
Usage
-----
After installing Gertty, you should be able to run it by invoking
``gertty``. If you installed it in a virtualenv, you can invoke it
without activating the virtualenv with ``/path/to/venv/bin/gertty``
which you may wish to add to your shell aliases. Use ``gertty
After installing Boartty, you should be able to run it by invoking
``boartty``. If you installed it in a virtualenv, you can invoke it
without activating the virtualenv with ``/path/to/venv/bin/boartty``
which you may wish to add to your shell aliases. Use ``boartty
--help`` to see a list of command line options available.
Once Gertty is running, you will need to start by subscribing to some
Once Boartty is running, you will need to start by subscribing to some
projects. Use 'L' to list all of the projects and then 's' to
subscribe to the ones you are interested in. Hit 'L' again to shrink
the list to your subscribed projects.
@ -139,37 +86,27 @@ the list to your subscribed projects.
In general, pressing the F1 key will show help text on any screen, and
ESC will take you to the previous screen.
Gertty works seamlessly offline or online. All of the actions that it
performs are first recorded in a local database (in ``~/.gertty.db``
by default), and are then transmitted to Gerrit. If Gertty is unable
to contact Gerrit for any reason, it will continue to operate against
the local database, and once it re-establishes contact, it will
process any pending changes.
Boartty works seamlessly offline or online. All of the actions that
it performs are first recorded in a local database (in
``~/.boartty.db`` by default), and are then transmitted to Storyboard.
If Boartty is unable to contact Storyboard for any reason, it will
continue to operate against the local database, and once it
re-establishes contact, it will process any pending changes.
The status bar at the top of the screen displays the current number of
outstanding tasks that Gertty must perform in order to be fully up to
outstanding tasks that Boartty must perform in order to be fully up to
date. Some of these tasks are more complicated than others, and some
of them will end up creating new tasks (for instance, one task may be
to search for new changes in a project which will then produce 5 new
tasks if there are 5 new changes).
to search for new stories in a project which will then produce 5 new
tasks if there are 5 new stories).
If Gertty is offline, it will so indicate in the status bar. It will
If Boartty is offline, it will so indicate in the status bar. It will
retry requests if needed, and will switch between offline and online
mode automatically.
If you review a change while offline with a positive vote, and someone
else leaves a negative vote on that change in the same category before
Gertty is able to upload your review, Gertty will detect the situation
and mark the change as "held" so that you may re-inspect the change
and any new comments before uploading the review. The status bar will
alert you to any held changes and direct you to a list of them (the
`F12` key by default). When viewing a change, the "held" flag may be
toggled with the exclamation key (`!`). Once held, a change must be
explicitly un-held in this manner for your review to be uploaded.
If Gertty encounters an error, this will also be indicated in the
status bar. You may wish to examine ~/.gertty.log to see what the
error was. In many cases, Gertty can continue after encountering an
If Boartty encounters an error, this will also be indicated in the
status bar. You may wish to examine ~/.boartty.log to see what the
error was. In many cases, Boartty can continue after encountering an
error. The error flag will be cleared when you leave the current
screen.
@ -180,28 +117,28 @@ Terminal Integration
--------------------
If you use rxvt-unicode, you can add something like the following to
``.Xresources`` to make Gerrit URLs that are displayed in your
``.Xresources`` to make Storyboard URLs that are displayed in your
terminal (perhaps in an email or irc client) clickable links that open
in Gertty::
in Boartty::
URxvt.perl-ext: default,matcher
URxvt.url-launcher: sensible-browser
URxvt.keysym.C-Delete: perl:matcher:last
URxvt.keysym.M-Delete: perl:matcher:list
URxvt.matcher.button: 1
URxvt.matcher.pattern.1: https:\/\/review.example.org/(\\#\/c\/)?(\\d+)[\w]*
URxvt.matcher.launcher.1: gertty --open $0
URxvt.matcher.pattern.1: https:\/\/storyboard.example.org/#!/story/(\\d+)[\w]*
URxvt.matcher.launcher.1: boartty --open $0
You will want to adjust the pattern to match the review site you are
interested in; multiple patterns may be added as needed.
You will want to adjust the pattern to match the Storyboard site you
are interested in; multiple patterns may be added as needed.
Contributing
------------
For information on how to contribute to Gertty, please see the
For information on how to contribute to Boartty, please see the
contents of the CONTRIBUTING.rst file.
Bugs
----
Bugs are handled at: https://storyboard.openstack.org/#!/project/698
Bugs are handled at: https://storyboard.openstack.org/

View File

@ -20,7 +20,7 @@ script_location = alembic
# versions/ directory
# sourceless = false
sqlalchemy.url = sqlite:////tmp/gertty.db
sqlalchemy.url = sqlite:////tmp/boartty.db
# Logging configuration

View File

@ -15,8 +15,8 @@ config = context.config
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
import gertty.db
target_metadata = gertty.db.metadata
import boartty.db
target_metadata = boartty.db.metadata
# other values from the config, defined by the needs of env.py,
# can be acquired:

View File

@ -0,0 +1,193 @@
"""initial schema
Revision ID: 183755ac91df
Revises: None
Create Date: 2016-10-31 08:54:59.399741
"""
# revision identifiers, used by Alembic.
revision = '183755ac91df'
down_revision = None
from alembic import op
import sqlalchemy as sa
def upgrade():
### commands auto generated by Alembic - please adjust! ###
op.create_table('comment',
sa.Column('key', sa.Integer(), nullable=False),
sa.Column('id', sa.Integer(), nullable=True),
sa.Column('parent_comment_key', sa.Integer(), nullable=True),
sa.Column('content', sa.Text(), nullable=True),
sa.Column('draft', sa.Boolean(), nullable=False),
sa.Column('pending', sa.Boolean(), nullable=False),
sa.Column('pending_delete', sa.Boolean(), nullable=False),
sa.ForeignKeyConstraint(['parent_comment_key'], ['comment.key'], ),
sa.PrimaryKeyConstraint('key')
)
op.create_index(op.f('ix_comment_draft'), 'comment', ['draft'], unique=False)
op.create_index(op.f('ix_comment_id'), 'comment', ['id'], unique=False)
op.create_index(op.f('ix_comment_pending'), 'comment', ['pending'], unique=False)
op.create_index(op.f('ix_comment_pending_delete'), 'comment', ['pending_delete'], unique=False)
op.create_table('project',
sa.Column('key', sa.Integer(), nullable=False),
sa.Column('id', sa.Integer(), nullable=True),
sa.Column('name', sa.String(length=255), nullable=False),
sa.Column('subscribed', sa.Boolean(), nullable=True),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('updated', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('key')
)
op.create_index(op.f('ix_project_id'), 'project', ['id'], unique=False)
op.create_index(op.f('ix_project_name'), 'project', ['name'], unique=False)
op.create_index(op.f('ix_project_subscribed'), 'project', ['subscribed'], unique=False)
op.create_index(op.f('ix_project_updated'), 'project', ['updated'], unique=False)
op.create_table('sync_query',
sa.Column('key', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=255), nullable=False),
sa.Column('updated', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('key')
)
op.create_index(op.f('ix_sync_query_name'), 'sync_query', ['name'], unique=True)
op.create_index(op.f('ix_sync_query_updated'), 'sync_query', ['updated'], unique=False)
op.create_table('system',
sa.Column('key', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=True),
sa.PrimaryKeyConstraint('key')
)
op.create_table('tag',
sa.Column('key', sa.Integer(), nullable=False),
sa.Column('id', sa.Integer(), nullable=True),
sa.Column('name', sa.String(length=255), nullable=False),
sa.PrimaryKeyConstraint('key')
)
op.create_index(op.f('ix_tag_id'), 'tag', ['id'], unique=False)
op.create_index(op.f('ix_tag_name'), 'tag', ['name'], unique=False)
op.create_table('topic',
sa.Column('key', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=255), nullable=False),
sa.Column('sequence', sa.Integer(), nullable=False),
sa.PrimaryKeyConstraint('key')
)
op.create_index(op.f('ix_topic_name'), 'topic', ['name'], unique=False)
op.create_index(op.f('ix_topic_sequence'), 'topic', ['sequence'], unique=True)
op.create_table('user',
sa.Column('key', sa.Integer(), nullable=False),
sa.Column('id', sa.Integer(), nullable=True),
sa.Column('name', sa.String(length=255), nullable=True),
sa.Column('email', sa.String(length=255), nullable=True),
sa.PrimaryKeyConstraint('key')
)
op.create_index(op.f('ix_user_email'), 'user', ['email'], unique=False)
op.create_index(op.f('ix_user_id'), 'user', ['id'], unique=False)
op.create_index(op.f('ix_user_name'), 'user', ['name'], unique=False)
op.create_table('project_topic',
sa.Column('key', sa.Integer(), nullable=False),
sa.Column('project_key', sa.Integer(), nullable=True),
sa.Column('topic_key', sa.Integer(), nullable=True),
sa.Column('sequence', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['project_key'], ['project.key'], ),
sa.ForeignKeyConstraint(['topic_key'], ['topic.key'], ),
sa.PrimaryKeyConstraint('key'),
sa.UniqueConstraint('topic_key', 'sequence', name='topic_key_sequence_const')
)
op.create_index(op.f('ix_project_topic_project_key'), 'project_topic', ['project_key'], unique=False)
op.create_index(op.f('ix_project_topic_topic_key'), 'project_topic', ['topic_key'], unique=False)
op.create_table('story',
sa.Column('key', sa.Integer(), nullable=False),
sa.Column('id', sa.Integer(), nullable=True),
sa.Column('user_key', sa.Integer(), nullable=True),
sa.Column('status', sa.String(length=16), nullable=False),
sa.Column('hidden', sa.Boolean(), nullable=False),
sa.Column('subscribed', sa.Boolean(), nullable=False),
sa.Column('title', sa.String(length=255), nullable=True),
sa.Column('private', sa.Boolean(), nullable=False),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('created', sa.DateTime(), nullable=True),
sa.Column('updated', sa.DateTime(), nullable=True),
sa.Column('last_seen', sa.DateTime(), nullable=True),
sa.Column('outdated', sa.Boolean(), nullable=False),
sa.Column('pending', sa.Boolean(), nullable=False),
sa.Column('pending_delete', sa.Boolean(), nullable=False),
sa.ForeignKeyConstraint(['user_key'], ['user.key'], ),
sa.PrimaryKeyConstraint('key')
)
op.create_index(op.f('ix_story_created'), 'story', ['created'], unique=False)
op.create_index(op.f('ix_story_hidden'), 'story', ['hidden'], unique=False)
op.create_index(op.f('ix_story_id'), 'story', ['id'], unique=False)
op.create_index(op.f('ix_story_last_seen'), 'story', ['last_seen'], unique=False)
op.create_index(op.f('ix_story_outdated'), 'story', ['outdated'], unique=False)
op.create_index(op.f('ix_story_pending'), 'story', ['pending'], unique=False)
op.create_index(op.f('ix_story_pending_delete'), 'story', ['pending_delete'], unique=False)
op.create_index(op.f('ix_story_status'), 'story', ['status'], unique=False)
op.create_index(op.f('ix_story_subscribed'), 'story', ['subscribed'], unique=False)
op.create_index(op.f('ix_story_title'), 'story', ['title'], unique=False)
op.create_index(op.f('ix_story_updated'), 'story', ['updated'], unique=False)
op.create_index(op.f('ix_story_user_key'), 'story', ['user_key'], unique=False)
op.create_table('event',
sa.Column('key', sa.Integer(), nullable=False),
sa.Column('id', sa.Integer(), nullable=True),
sa.Column('type', sa.String(length=255), nullable=False),
sa.Column('user_key', sa.Integer(), nullable=True),
sa.Column('story_key', sa.Integer(), nullable=True),
sa.Column('created', sa.DateTime(), nullable=True),
sa.Column('comment_key', sa.Integer(), nullable=True),
sa.Column('info', sa.Text(), nullable=True),
sa.ForeignKeyConstraint(['comment_key'], ['comment.key'], ),
sa.ForeignKeyConstraint(['story_key'], ['story.key'], ),
sa.ForeignKeyConstraint(['user_key'], ['user.key'], ),
sa.PrimaryKeyConstraint('key')
)
op.create_index(op.f('ix_event_created'), 'event', ['created'], unique=False)
op.create_index(op.f('ix_event_id'), 'event', ['id'], unique=False)
op.create_index(op.f('ix_event_type'), 'event', ['type'], unique=False)
op.create_index(op.f('ix_event_user_key'), 'event', ['user_key'], unique=False)
op.create_table('story_tag',
sa.Column('key', sa.Integer(), nullable=False),
sa.Column('story_key', sa.Integer(), nullable=True),
sa.Column('tag_key', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['story_key'], ['story.key'], ),
sa.ForeignKeyConstraint(['tag_key'], ['tag.key'], ),
sa.PrimaryKeyConstraint('key')
)
op.create_index(op.f('ix_story_tag_story_key'), 'story_tag', ['story_key'], unique=False)
op.create_index(op.f('ix_story_tag_tag_key'), 'story_tag', ['tag_key'], unique=False)
op.create_table('task',
sa.Column('key', sa.Integer(), nullable=False),
sa.Column('id', sa.Integer(), nullable=True),
sa.Column('title', sa.String(length=255), nullable=True),
sa.Column('status', sa.String(length=16), nullable=True),
sa.Column('creator_user_key', sa.Integer(), nullable=True),
sa.Column('story_key', sa.Integer(), nullable=True),
sa.Column('project_key', sa.Integer(), nullable=True),
sa.Column('assignee_user_key', sa.Integer(), nullable=True),
sa.Column('priority', sa.String(length=16), nullable=True),
sa.Column('link', sa.Text(), nullable=True),
sa.Column('created', sa.DateTime(), nullable=True),
sa.Column('updated', sa.DateTime(), nullable=True),
sa.Column('pending', sa.Boolean(), nullable=False),
sa.Column('pending_delete', sa.Boolean(), nullable=False),
sa.ForeignKeyConstraint(['assignee_user_key'], ['user.key'], ),
sa.ForeignKeyConstraint(['creator_user_key'], ['user.key'], ),
sa.ForeignKeyConstraint(['project_key'], ['project.key'], ),
sa.ForeignKeyConstraint(['story_key'], ['story.key'], ),
sa.PrimaryKeyConstraint('key')
)
op.create_index(op.f('ix_task_assignee_user_key'), 'task', ['assignee_user_key'], unique=False)
op.create_index(op.f('ix_task_created'), 'task', ['created'], unique=False)
op.create_index(op.f('ix_task_creator_user_key'), 'task', ['creator_user_key'], unique=False)
op.create_index(op.f('ix_task_id'), 'task', ['id'], unique=False)
op.create_index(op.f('ix_task_pending'), 'task', ['pending'], unique=False)
op.create_index(op.f('ix_task_pending_delete'), 'task', ['pending_delete'], unique=False)
op.create_index(op.f('ix_task_project_key'), 'task', ['project_key'], unique=False)
op.create_index(op.f('ix_task_status'), 'task', ['status'], unique=False)
op.create_index(op.f('ix_task_story_key'), 'task', ['story_key'], unique=False)
op.create_index(op.f('ix_task_title'), 'task', ['title'], unique=False)
op.create_index(op.f('ix_task_updated'), 'task', ['updated'], unique=False)
### end Alembic commands ###
def downgrade():
pass

View File

@ -35,28 +35,27 @@ from six.moves.urllib import parse as urlparse
import sqlalchemy.exc
import urwid
from gertty import db
from gertty import config
from gertty import gitrepo
from gertty import keymap
from gertty import mywid
from gertty import palette
from gertty import sync
from gertty import search
from gertty import requestsexceptions
from gertty.view import change_list as view_change_list
from gertty.view import project_list as view_project_list
from gertty.view import change as view_change
import gertty.view
import gertty.version
from boartty import db
from boartty import config
from boartty import keymap
from boartty import mywid
from boartty import palette
from boartty import sync
from boartty import search
from boartty import requestsexceptions
from boartty.view import story_list as view_story_list
from boartty.view import project_list as view_project_list
from boartty.view import story as view_story
import boartty.view
import boartty.version
WELCOME_TEXT = """\
Welcome to Gertty!
Welcome to Boartty!
To get started, you should subscribe to some projects. Press the "L"
key (shift-L) to list all the projects, navigate to the ones you are
interested in, and then press "s" to subscribe to them. Gertty will
automatically sync changes in your subscribed projects.
interested in, and then press "s" to subscribe to them. Boardtty will
automatically sync stories in your subscribed projects.
Press the F1 key anywhere to get help. Your terminal emulator may
require you to press function-F1 or alt-F1 instead.
@ -233,8 +232,8 @@ class ProjectCache(object):
def get(self, project):
if project.key not in self.projects:
self.projects[project.key] = dict(
unreviewed_changes = len(project.unreviewed_changes),
open_changes = len(project.open_changes),
active_stories = len(project.active_stories),
stories = len(project.stories),
)
return self.projects[project.key]
@ -272,14 +271,14 @@ class App(object):
req_logger.setLevel(level)
else:
req_logger.setLevel(req_level_name)
self.log = logging.getLogger('gertty.App')
self.log = logging.getLogger('boartty.App')
self.log.debug("Starting")
self.lock_fd = open(self.config.lock_file, 'w')
try:
fcntl.lockf(self.lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
except IOError:
print("error: another instance of gertty is running for: %s" % self.config.server['name'])
print("error: another instance of boartty is running for: %s" % self.config.server['name'])
sys.exit(1)
self.project_cache = ProjectCache()
@ -291,6 +290,7 @@ class App(object):
self.config.keymap.updateCommandMap()
self.search = search.SearchCompiler(self.config.username)
self.db = db.Database(self, self.config.dburi, self.search)
self.initSystemData()
self.sync = sync.Sync(self, disable_background_sync)
self.status = StatusHeader(self)
@ -405,6 +405,7 @@ class App(object):
if not self.screens:
return
while self.screens:
self.log.debug("screens %s" % (target_widget,))
widget = self.screens.pop()
if (not target_widget) or (widget is target_widget):
break
@ -415,9 +416,9 @@ class App(object):
self.frame.body = widget
self.refresh(force=True)
def findChangeList(self):
def findStoryList(self):
for widget in reversed(self.screens):
if isinstance(widget, view_change_list.ChangeListView):
if isinstance(widget, view_story_list.StoryListView):
return widget
return None
@ -450,6 +451,7 @@ class App(object):
self.status.refresh()
def updateStatusQueries(self):
return # TODO: storyboard
with self.db.getSession() as session:
held = len(session.getHeld())
self.status.update(held=held)
@ -515,6 +517,7 @@ class App(object):
lambda button: self.backScreen())
self.popup(dialog, min_width=76, min_height=len(lines)+4)
#storyboard
def _syncOneChangeFromQuery(self, query):
number = changeid = restid = None
if query.startswith("change:"):
@ -574,7 +577,7 @@ class App(object):
with self.db.getSession() as session:
try:
changes = session.getChanges(query)
except gertty.search.SearchSyntaxError as e:
except boartty.search.SearchSyntaxError as e:
return self.error(e.message)
except sqlalchemy.exc.OperationalError as e:
return self.error(e.message)
@ -587,9 +590,9 @@ class App(object):
if change_key:
view = view_change.ChangeView(self, change_key)
else:
view = view_change_list.ChangeListView(self, query)
view = view_story_list.StoryListView(self, query)
self.changeScreen(view)
except gertty.view.DisplayError as e:
except boartty.view.DisplayError as e:
return self.error(e.message)
def searchDialog(self, default):
@ -677,15 +680,17 @@ class App(object):
self.help()
elif keymap.QUIT in commands:
self.quit()
elif keymap.CHANGE_SEARCH in commands:
elif keymap.STORY_SEARCH in commands:
self.searchDialog('')
elif keymap.NEW_STORY in commands:
self.newStory()
elif keymap.LIST_HELD in commands:
self.doSearch("is:held")
elif key in self.config.dashboards:
d = self.config.dashboards[key]
view = view_change_list.ChangeListView(self, d['query'], d['name'],
sort_by=d.get('sort-by'),
reverse=d.get('reverse'))
view = view_story_list.StoryListView(self, d['query'], d['name'],
sort_by=d.get('sort-by'),
reverse=d.get('reverse'))
self.changeScreen(view)
elif keymap.FURTHER_INPUT in commands:
self.input_buffer.append(key)
@ -711,6 +716,8 @@ class App(object):
self.loop.screen.clear()
def time(self, dt):
if dt is None:
return None
utc = dt.replace(tzinfo=dateutil.tz.tzutc())
if self.config.utc:
return utc
@ -753,6 +760,7 @@ class App(object):
else:
self.log.error("Unable to parse command %s with data %s" % (command, data))
#storyboard
def toggleHeldChange(self, change_key):
with self.db.getSession() as session:
change = session.getChange(change_key)
@ -767,88 +775,54 @@ class App(object):
self.updateStatusQueries()
return ret
def localCheckoutCommit(self, project_name, commit_sha):
repo = gitrepo.get_repo(project_name, self.config)
try:
repo.checkout(commit_sha)
dialog = mywid.MessageDialog('Checkout', 'Change checked out in %s' % repo.path)
min_height=8
except gitrepo.GitCheckoutError as e:
dialog = mywid.MessageDialog('Error', e.msg)
min_height=12
urwid.connect_signal(dialog, 'close',
lambda button: self.backScreen())
self.popup(dialog, min_height=min_height)
def newStory(self):
dialog = view_story.NewStoryDialog(self)
urwid.connect_signal(dialog, 'save',
lambda button: self.saveNewStory(dialog))
urwid.connect_signal(dialog, 'cancel',
lambda button: self.cancelNewStory(dialog))
self.popup(dialog,
relative_width=50, relative_height=25,
min_width=60, min_height=8)
def localCherryPickCommit(self, project_name, commit_sha):
repo = gitrepo.get_repo(project_name, self.config)
try:
repo.cherryPick(commit_sha)
dialog = mywid.MessageDialog('Cherry-Pick', 'Change cherry-picked in %s' % repo.path)
min_height=8
except gitrepo.GitCheckoutError as e:
dialog = mywid.MessageDialog('Error', e.msg)
min_height=12
urwid.connect_signal(dialog, 'close',
lambda button: self.backScreen())
self.popup(dialog, min_height=min_height)
def cancelNewStory(self, dialog):
self.backScreen()
def saveReviews(self, revision_keys, approvals, message, upload, submit):
message_keys = []
def saveNewStory(self, dialog):
with self.db.getSession() as session:
account = session.getAccountByUsername(self.config.username)
for revision_key in revision_keys:
k = self._saveReview(session, account, revision_key,
approvals, message, upload, submit)
if k:
message_keys.append(k)
return message_keys
story = session.createStory(
title=dialog.title_field.edit_text,
description=dialog.description_field.edit_text,
pending=True)
task = story.addTask(
project=session.getProjectByID(dialog.project_button.key),
title=dialog.title_field.edit_text,
pending=True)
def _saveReview(self, session, account, revision_key,
approvals, message, upload, submit):
message_key = None
revision = session.getRevision(revision_key)
change = revision.change
draft_approvals = {}
for approval in change.draft_approvals:
draft_approvals[approval.category] = approval
self.sync.submitTask(
sync.UpdateStoryTask(story.key, sync.HIGH_PRIORITY))
self.sync.submitTask(
sync.UpdateTaskTask(task.key, sync.HIGH_PRIORITY))
self.backScreen()
categories = set()
for label in change.permitted_labels:
categories.add(label.category)
for category in categories:
value = approvals.get(category, 0)
approval = draft_approvals.get(category)
if not approval:
approval = change.createApproval(account, category, 0, draft=True)
draft_approvals[category] = approval
approval.value = value
draft_message = revision.getPendingMessage()
if not draft_message:
draft_message = revision.getDraftMessage()
if not draft_message:
if message or upload:
draft_message = revision.createMessage(None, account,
datetime.datetime.utcnow(),
'', draft=True)
if draft_message:
draft_message.created = datetime.datetime.utcnow()
draft_message.message = message
draft_message.pending = upload
message_key = draft_message.key
if upload:
change.reviewed = True
self.project_cache.clear(change.project)
if submit:
change.status = 'SUBMITTED'
change.pending_status = True
change.pending_status_message = None
return message_key
def initSystemData(self):
with self.db.getSession() as session:
system = session.getSystem()
if system is None:
self.user_id = None
else:
self.user_id = system.user_id
def setUserID(self, user_id):
with self.db.getSession() as session:
system = session.getSystem()
if system is None:
system = session.createSystem()
system.user_id = self.user_id = user_id
def version():
return "Gertty version: %s" % gertty.version.version_info.release_string()
return "Boardtty version: %s" % boartty.version.version_info.release_string()
class PrintKeymapAction(argparse.Action):
def __call__(self, parser, namespace, values, option_string=None):
@ -900,10 +874,10 @@ def main():
help='print the palette attribute names to stdout')
parser.add_argument('--open', nargs=1, action=OpenChangeAction,
metavar='URL',
help='open the given URL in a running Gertty')
help='open the given URL in a running Boardtty')
parser.add_argument('--version', dest='version', action='version',
version=version(),
help='show Gertty\'s version')
help='show Boardtty\'s version')
parser.add_argument('-p', dest='palette', default='default',
help='color palette to use')
parser.add_argument('-k', dest='keymap', default='default',

View File

@ -22,7 +22,7 @@ import re
import six
import urwid
from gertty import mywid
from boartty import mywid
try:
OrderedDict = collections.OrderedDict

View File

@ -26,27 +26,24 @@ import yaml
from six.moves.urllib import parse as urlparse
import voluptuous as v
import gertty.commentlink
import gertty.palette
import gertty.keymap
import boartty.commentlink
import boartty.palette
import boartty.keymap
try:
OrderedDict = collections.OrderedDict
except AttributeError:
OrderedDict = ordereddict.OrderedDict
DEFAULT_CONFIG_PATH='~/.gertty.yaml'
DEFAULT_CONFIG_PATH='~/.boartty.yaml'
class ConfigSchema(object):
server = {v.Required('name'): str,
v.Required('url'): str,
v.Required('username'): str,
'password': str,
v.Required('token'): str,
'verify-ssl': bool,
'ssl-ca-path': str,
'dburi': str,
v.Required('git-root'): str,
'git-url': str,
'log-file': str,
'socket': str,
'auth-type': v.Any('basic', 'digest', 'form'),
@ -100,7 +97,7 @@ class ConfigSchema(object):
hide_comments = [hide_comment]
change_list_options = {'sort-by': sort_by,
story_list_options = {'sort-by': sort_by,
'reverse': bool}
keymap = {v.Required('name'): str,
@ -117,14 +114,13 @@ class ConfigSchema(object):
'commentlinks': self.commentlinks,
'dashboards': self.dashboards,
'reviewkeys': self.reviewkeys,
'change-list-query': str,
'story-list-query': str,
'diff-view': str,
'hide-comments': self.hide_comments,
'thread-changes': bool,
'display-times-in-utc': bool,
'handle-mouse': bool,
'breadcrumbs': bool,
'change-list-options': self.change_list_options,
'story-list-options': self.story_list_options,
'expire-age': str,
})
return schema
@ -149,21 +145,17 @@ class Config(object):
self.url = url
result = urlparse.urlparse(url)
self.hostname = result.netloc
self.username = server['username']
self.password = server.get('password')
if self.password is None:
self.password = getpass.getpass("Password for %s (%s): "
% (self.url, self.username))
else:
# Ensure file is only readable by user as password is stored in
# file.
mode = os.stat(self.path).st_mode & 0o0777
if not mode == 0o600:
print (
"Error: Config file '{}' contains a password and does "
"not have permissions set to 0600.\n"
"Permissions are: {}".format(self.path, oct(mode)))
exit(1)
self.token = server['token']
self.username = '' # TODO: storyboard
# Ensure file is only readable by user as password is stored in
# file.
mode = os.stat(self.path).st_mode & 0o0777
if not mode == 0o600:
print (
"Error: Config file '{}' contains an api key and does "
"not have permissions set to 0600.\n"
"Permissions are: {}".format(self.path, oct(mode)))
exit(1)
self.auth_type = server.get('auth-type', 'digest')
self.verify_ssl = server.get('verify-ssl', True)
if not self.verify_ssl:
@ -171,54 +163,49 @@ class Config(object):
self.ssl_ca_path = server.get('ssl-ca-path', None)
if self.ssl_ca_path is not None:
self.ssl_ca_path = os.path.expanduser(self.ssl_ca_path)
# Gertty itself uses the Requests library
# Boardtty itself uses the Requests library
os.environ['REQUESTS_CA_BUNDLE'] = self.ssl_ca_path
# And this is to allow Git callouts
os.environ['GIT_SSL_CAINFO'] = self.ssl_ca_path
self.git_root = os.path.expanduser(server['git-root'])
git_url = server.get('git-url', self.url + 'p/')
if not git_url.endswith('/'):
git_url += '/'
self.git_url = git_url
self.dburi = server.get('dburi',
'sqlite:///' + os.path.expanduser('~/.gertty.db'))
socket_path = server.get('socket', '~/.gertty.sock')
'sqlite:///' + os.path.expanduser('~/.boartty.db'))
socket_path = server.get('socket', '~/.boartty.sock')
self.socket_path = os.path.expanduser(socket_path)
log_file = server.get('log-file', '~/.gertty.log')
log_file = server.get('log-file', '~/.boartty.log')
self.log_file = os.path.expanduser(log_file)
lock_file = server.get('lock-file', '~/.gertty.%s.lock' % server['name'])
lock_file = server.get('lock-file', '~/.boartty.%s.lock' % server['name'])
self.lock_file = os.path.expanduser(lock_file)
self.palettes = {'default': gertty.palette.Palette({}),
'light': gertty.palette.Palette(gertty.palette.LIGHT_PALETTE),
self.palettes = {'default': boartty.palette.Palette({}),
'light': boartty.palette.Palette(boartty.palette.LIGHT_PALETTE),
}
for p in self.config.get('palettes', []):
if p['name'] not in self.palettes:
self.palettes[p['name']] = gertty.palette.Palette(p)
self.palettes[p['name']] = boartty.palette.Palette(p)
else:
self.palettes[p['name']].update(p)
self.palette = self.palettes[self.config.get('palette', palette)]
self.keymaps = {'default': gertty.keymap.KeyMap({}),
'vi': gertty.keymap.KeyMap(gertty.keymap.VI_KEYMAP)}
self.keymaps = {'default': boartty.keymap.KeyMap({}),
'vi': boartty.keymap.KeyMap(boartty.keymap.VI_KEYMAP)}
for p in self.config.get('keymaps', []):
if p['name'] not in self.keymaps:
self.keymaps[p['name']] = gertty.keymap.KeyMap(p)
self.keymaps[p['name']] = boartty.keymap.KeyMap(p)
else:
self.keymaps[p['name']].update(p)
self.keymap = self.keymaps[self.config.get('keymap', keymap)]
self.commentlinks = [gertty.commentlink.CommentLink(c)
self.commentlinks = [boartty.commentlink.CommentLink(c)
for c in self.config.get('commentlinks', [])]
self.commentlinks.append(
gertty.commentlink.CommentLink(dict(
boartty.commentlink.CommentLink(dict(
match="(?P<url>https?://\\S*)",
replacements=[
dict(link=dict(
text="{url}",
url="{url}"))])))
self.project_change_list_query = self.config.get('change-list-query', 'status:open')
self.project_story_list_query = self.config.get('story-list-query', '')
self.diff_view = self.config.get('diff-view', 'side-by-side')
@ -235,15 +222,14 @@ class Config(object):
for h in self.config.get('hide-comments', []):
self.hide_comments.append(re.compile(h['author']))
self.thread_changes = self.config.get('thread-changes', True)
self.utc = self.config.get('display-times-in-utc', False)
self.breadcrumbs = self.config.get('breadcrumbs', True)
self.handle_mouse = self.config.get('handle-mouse', True)
change_list_options = self.config.get('change-list-options', {})
self.change_list_options = {
'sort-by': change_list_options.get('sort-by', 'number'),
'reverse': change_list_options.get('reverse', False)}
story_list_options = self.config.get('story-list-options', {})
self.story_list_options = {
'sort-by': story_list_options.get('sort-by', 'number'),
'reverse': story_list_options.get('reverse', False)}
self.expire_age = self.config.get('expire-age', '2 months')
@ -254,11 +240,11 @@ class Config(object):
return None
def printSample(self):
filename = 'share/gertty/examples'
print("""Gertty requires a configuration file at ~/.gertty.yaml
filename = 'share/boartty/examples'
print("""Boardtty requires a configuration file at ~/.boartty.yaml
If the file contains a password then permissions must be set to 0600.
Several sample configuration files were installed with Gertty and are
Several sample configuration files were installed with Boardtty and are
available in %s in the root of the installation.
For more information, please see the README.

676
boartty/db.py Normal file
View File

@ -0,0 +1,676 @@
# Copyright 2014 OpenStack Foundation
# Copyright 2014 Hewlett-Packard Development Company, L.P.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import datetime
import re
import time
import logging
import threading
import alembic
import alembic.config
import six
import sqlalchemy
from sqlalchemy import create_engine, MetaData, Table, Column, Integer, String, Boolean, DateTime, Text, UniqueConstraint
from sqlalchemy.schema import ForeignKey
from sqlalchemy.orm import mapper, sessionmaker, relationship, scoped_session, joinedload
from sqlalchemy.orm.session import Session
from sqlalchemy.sql import exists
from sqlalchemy.sql.expression import and_
metadata = MetaData()
system_table = Table(
'system', metadata,
Column('key', Integer, primary_key=True),
Column('user_id', Integer),
)
project_table = Table(
'project', metadata,
Column('key', Integer, primary_key=True),
Column('id', Integer, index=True),
Column('name', String(255), index=True, nullable=False),
Column('subscribed', Boolean, index=True, default=False),
Column('description', Text),
Column('updated', DateTime, index=True),
)
topic_table = Table(
'topic', metadata,
Column('key', Integer, primary_key=True),
Column('name', String(255), index=True, nullable=False),
Column('sequence', Integer, index=True, unique=True, nullable=False),
)
project_topic_table = Table(
'project_topic', metadata,
Column('key', Integer, primary_key=True),
Column('project_key', Integer, ForeignKey("project.key"), index=True),
Column('topic_key', Integer, ForeignKey("topic.key"), index=True),
Column('sequence', Integer, nullable=False),
UniqueConstraint('topic_key', 'sequence', name='topic_key_sequence_const'),
)
story_table = Table(
'story', metadata,
Column('key', Integer, primary_key=True),
Column('id', Integer, index=True),
Column('user_key', Integer, ForeignKey("user.key"), index=True),
Column('status', String(16), index=True, nullable=False),
Column('hidden', Boolean, index=True, nullable=False),
Column('subscribed', Boolean, index=True, nullable=False),
Column('title', String(255), index=True),
Column('private', Boolean, nullable=False),
Column('description', Text),
Column('created', DateTime, index=True),
# TODO: make sure updated is never null in storyboard
Column('updated', DateTime, index=True),
Column('last_seen', DateTime, index=True),
Column('outdated', Boolean, index=True, nullable=False),
Column('pending', Boolean, index=True, nullable=False),
Column('pending_delete', Boolean, index=True, nullable=False),
)
tag_table = Table(
'tag', metadata,
Column('key', Integer, primary_key=True),
Column('id', Integer, index=True),
Column('name', String(255), index=True, nullable=False),
)
story_tag_table = Table(
'story_tag', metadata,
Column('key', Integer, primary_key=True),
Column('story_key', Integer, ForeignKey("story.key"), index=True),
Column('tag_key', Integer, ForeignKey("tag.key"), index=True),
)
task_table = Table(
'task', metadata,
Column('key', Integer, primary_key=True),
Column('id', Integer, index=True),
Column('title', String(255), index=True),
Column('status', String(16), index=True),
Column('creator_user_key', Integer, ForeignKey("user.key"), index=True),
Column('story_key', Integer, ForeignKey("story.key"), index=True),
Column('project_key', Integer, ForeignKey("project.key"), index=True),
Column('assignee_user_key', Integer, ForeignKey("user.key"), index=True),
Column('priority', String(16)),
Column('link', Text),
Column('created', DateTime, index=True),
# TODO: make sure updated is never null in storyboard
Column('updated', DateTime, index=True),
Column('pending', Boolean, index=True, nullable=False),
Column('pending_delete', Boolean, index=True, nullable=False),
)
event_table = Table(
'event', metadata,
Column('key', Integer, primary_key=True),
Column('id', Integer, index=True),
Column('type', String(255), index=True, nullable=False),
Column('user_key', Integer, ForeignKey("user.key"), index=True),
Column('story_key', Integer, ForeignKey('story.key'), nullable=True),
#Column('worklist_key', Integer, ForeignKey('worklist.key'), nullable=True),
#Column('board_key', Integer, ForeignKey('board.key'), nullable=True),
Column('created', DateTime, index=True),
Column('comment_key', Integer, ForeignKey('comment.key'), nullable=True),
Column('user_key', ForeignKey('user.key'), nullable=True),
Column('info', Text),
)
comment_table = Table(
'comment', metadata,
Column('key', Integer, primary_key=True),
Column('id', Integer, index=True),
Column('parent_comment_key', Integer, ForeignKey('comment.key'), nullable=True),
Column('content', Text),
Column('draft', Boolean, index=True, nullable=False),
Column('pending', Boolean, index=True, nullable=False),
Column('pending_delete', Boolean, index=True, nullable=False),
)
user_table = Table(
'user', metadata,
Column('key', Integer, primary_key=True),
Column('id', Integer, index=True),
Column('name', String(255), index=True),
Column('email', String(255), index=True),
)
sync_query_table = Table(
'sync_query', metadata,
Column('key', Integer, primary_key=True),
Column('name', String(255), index=True, unique=True, nullable=False),
Column('updated', DateTime, index=True),
)
class System(object):
def __init__(self, user_id=None):
self.user_id = user_id
class User(object):
def __init__(self, id, name=None, email=None):
self.id = id
self.name = name
self.email = email
class Project(object):
def __init__(self, id, name, subscribed=False, description=''):
self.id = id
self.name = name
self.subscribed = subscribed
self.description = description
def createChange(self, *args, **kw):
session = Session.object_session(self)
args = [self] + list(args)
c = Change(*args, **kw)
self.changes.append(c)
session.add(c)
session.flush()
return c
def createBranch(self, *args, **kw):
session = Session.object_session(self)
args = [self] + list(args)
b = Branch(*args, **kw)
self.branches.append(b)
session.add(b)
session.flush()
return b
class ProjectTopic(object):
def __init__(self, project, topic, sequence):
self.project_key = project.key
self.topic_key = topic.key
self.sequence = sequence
class Topic(object):
def __init__(self, name, sequence):
self.name = name
self.sequence = sequence
def addProject(self, project):
session = Session.object_session(self)
seq = max([x.sequence for x in self.project_topics] + [0])
pt = ProjectTopic(project, self, seq+1)
self.project_topics.append(pt)
self.projects.append(project)
session.add(pt)
session.flush()
def removeProject(self, project):
session = Session.object_session(self)
for pt in self.project_topics:
if pt.project_key == project.key:
self.project_topics.remove(pt)
session.delete(pt)
self.projects.remove(project)
session.flush()
def format_name(self):
name = 'Anonymous Coward'
if self.creator:
if self.creator.name:
name = self.creator.name
elif self.creator.email:
name = self.creator.email
return name
class Story(object):
def __init__(self, id=None, creator=None, created=None, title=None,
description=None, pending=False):
self.id = id
self.creator = creator
self.title = title
self.description = description
self.status = 'active'
self.created = created
self.private = False
self.outdated = False
self.hidden = False
self.subscribed = False
self.pending = pending
self.pending_delete = False
@property
def creator_name(self):
return format_name(self)
def addEvent(self, *args, **kw):
session = Session.object_session(self)
e = Event(*args, **kw)
e.story_key = self.key
self.events.append(e)
session.add(e)
session.flush()
return e
def addTask(self, *args, **kw):
session = Session.object_session(self)
t = Task(*args, **kw)
t.story_key = self.key
self.tasks.append(t)
session.add(t)
session.flush()
return t
def getDraftCommentEvent(self, parent):
for event in self.events:
if (event.comment and event.comment.draft and
event.comment.parent==parent):
return event
return None
def setDraftComment(self, creator, parent, content):
event = self.getDraftCommentEvent(parent)
if event is None:
event = self.addEvent(type='user_comment', creator=creator)
event.addComment()
event.comment.content = content
event.comment.draft = True
event.comment.parent = parent
return event
class Tag(object):
def __init__(self, id, name):
self.id = id
self.name = name
class StoryTag(object):
def __init__(self, story, tag):
self.story_key = story.key
self.tag_key = tag.key
class Task(object):
def __init__(self, id=None, title=None, status=None, creator=None,
created=None, pending=False, pending_delete=False,
project=None):
self.id = id
self.title = title
self.status = status
self.pending = pending
self.pending_delete = pending_delete
self.creator = creator
self.created = created
self.project = project
class Event(object):
def __init__(self, id=None, type=None, creator=None, created=None, info=None):
self.id = id
self.type = type
self.creator = creator
if created is None:
created = datetime.datetime.utcnow()
self.created = created
self.info = info
@property
def creator_name(self):
return format_name(self)
@property
def description(self):
return re.sub('_', ' ', self.type)
def addComment(self, *args, **kw):
session = Session.object_session(self)
c = Comment(*args, **kw)
session.add(c)
session.flush()
self.comment_key = c.key
return c
class Comment(object):
def __init__(self, id=None, content=None, parent=None, draft=False,
pending=False, pending_delete=False):
self.id = id
self.content = content
self.parent = parent
self.pending = pending
self.pending_delete = pending_delete
self.draft = draft
class SyncQuery(object):
def __init__(self, name):
self.name = name
mapper(System, system_table)
mapper(User, user_table)
mapper(Project, project_table, properties=dict(
topics=relationship(Topic,
secondary=project_topic_table,
order_by=topic_table.c.name,
viewonly=True),
active_stories=relationship(Story,
secondary=task_table,
primaryjoin=and_(project_table.c.key==task_table.c.project_key,
story_table.c.key==task_table.c.story_key,
story_table.c.status=='active'),
order_by=story_table.c.id,
),
stories=relationship(Story,
secondary=task_table,
order_by=story_table.c.id,
),
))
mapper(Topic, topic_table, properties=dict(
projects=relationship(Project,
secondary=project_topic_table,
order_by=project_table.c.name,
viewonly=True),
project_topics=relationship(ProjectTopic),
))
mapper(ProjectTopic, project_topic_table)
mapper(Story, story_table, properties=dict(
creator=relationship(User),
tags=relationship(Tag,
secondary=story_tag_table,
order_by=tag_table.c.name,
#viewonly=True
),
tasks=relationship(Task, backref='story',
cascade='all, delete-orphan'),
events=relationship(Event, backref='story',
cascade='all, delete-orphan'),
))
mapper(Tag, tag_table)
mapper(StoryTag, story_tag_table)
mapper(Task, task_table, properties=dict(
project=relationship(Project),
assignee=relationship(User, foreign_keys=task_table.c.assignee_user_key),
creator=relationship(User, foreign_keys=task_table.c.creator_user_key),
))
mapper(Event, event_table, properties=dict(
creator=relationship(User),
comment=relationship(Comment, backref='event'),
))
mapper(Comment, comment_table, properties=dict(
parent=relationship(Comment, remote_side=[comment_table.c.key],backref='children'),
))
mapper(SyncQuery, sync_query_table)
def match(expr, item):
if item is None:
return False
return re.match(expr, item) is not None
@sqlalchemy.event.listens_for(sqlalchemy.engine.Engine, "connect")
def add_sqlite_match(dbapi_connection, connection_record):
dbapi_connection.create_function("matches", 2, match)
class Database(object):
def __init__(self, app, dburi, search):
self.log = logging.getLogger('boartty.db')
self.dburi = dburi
self.search = search
self.engine = create_engine(self.dburi)
#metadata.create_all(self.engine)
self.migrate(app)
# If we want the objects returned from query() to be usable
# outside of the session, we need to expunge them from the session,
# and since the DatabaseSession always calls commit() on the session
# when the context manager exits, we need to inform the session to
# expire objects when it does so.
self.session_factory = sessionmaker(bind=self.engine,
expire_on_commit=False,
autoflush=False)
self.session = scoped_session(self.session_factory)
self.lock = threading.Lock()
def getSession(self):
return DatabaseSession(self)
def migrate(self, app):
conn = self.engine.connect()
context = alembic.migration.MigrationContext.configure(conn)
current_rev = context.get_current_revision()
self.log.debug('Current migration revision: %s' % current_rev)
has_table = self.engine.dialect.has_table(conn, "project")
config = alembic.config.Config()
config.set_main_option("script_location", "boartty:alembic")
config.set_main_option("sqlalchemy.url", self.dburi)
config.boartty_app = app
if current_rev is None and has_table:
self.log.debug('Stamping database as initial revision')
alembic.command.stamp(config, "44402069e137")
alembic.command.upgrade(config, 'head')
class DatabaseSession(object):
def __init__(self, database):
self.database = database
self.session = database.session
self.search = database.search
def __enter__(self):
self.database.lock.acquire()
self.start = time.time()
return self
def __exit__(self, etype, value, tb):
if etype:
self.session().rollback()
else:
self.session().commit()
self.session().close()
self.session = None
end = time.time()
self.database.log.debug("Database lock held %s seconds" % (end-self.start,))
self.database.lock.release()
def abort(self):
self.session().rollback()
def commit(self):
self.session().commit()
def delete(self, obj):
self.session().delete(obj)
def vacuum(self):
self.session().execute("VACUUM")
def getProjects(self, subscribed=False, active=False, topicless=False):
"""Retrieve projects.
:param subscribed: If True limit to only subscribed projects.
:param active: If True limit to only projects with active
stories.
:param topicless: If True limit to only projects without topics.
"""
query = self.session().query(Project)
if subscribed:
query = query.filter_by(subscribed=subscribed)
if active:
query = query.filter(exists().where(Project.active_stories))
if topicless:
query = query.filter_by(topics=None)
return query.order_by(Project.name).all()
def getTopics(self):
return self.session().query(Topic).order_by(Topic.sequence).all()
def getProject(self, key):
try:
return self.session().query(Project).filter_by(key=key).one()
except sqlalchemy.orm.exc.NoResultFound:
return None
def getProjectByName(self, name):
try:
return self.session().query(Project).filter_by(name=name).one()
except sqlalchemy.orm.exc.NoResultFound:
return None
def getProjectByID(self, id):
try:
return self.session().query(Project).filter_by(id=id).one()
except sqlalchemy.orm.exc.NoResultFound:
return None
def getTopic(self, key):
try:
return self.session().query(Topic).filter_by(key=key).one()
except sqlalchemy.orm.exc.NoResultFound:
return None
def getTopicByName(self, name):
try:
return self.session().query(Topic).filter_by(name=name).one()
except sqlalchemy.orm.exc.NoResultFound:
return None
def getSyncQueryByName(self, name):
try:
return self.session().query(SyncQuery).filter_by(name=name).one()
except sqlalchemy.orm.exc.NoResultFound:
return self.createSyncQuery(name)
def getStory(self, key):
query = self.session().query(Story).filter_by(key=key)
try:
return query.one()
except sqlalchemy.orm.exc.NoResultFound:
return None
def getStoryByID(self, id):
try:
return self.session().query(Story).filter_by(id=id).one()
except sqlalchemy.orm.exc.NoResultFound:
return None
def getStories(self, query, active, sort_by='number'):
self.database.log.debug("Search query: %s sort: %s" % (query, sort_by))
q = self.session().query(Story)
if query:
q = q.filter(self.search.parse(query))
if active:
q = q.filter(story_table.c.hidden==False, story_table.c.status=='active')
if sort_by == 'updated':
q = q.order_by(story_table.c.updated)
elif sort_by == 'last-seen':
q = q.order_by(story_table.c.last_seen)
else:
q = q.order_by(story_table.c.id)
self.database.log.debug("Search SQL: %s" % q)
try:
return q.all()
except sqlalchemy.orm.exc.NoResultFound:
return []
def getTagByID(self, id):
try:
return self.session().query(Tag).filter_by(id=id).one()
except sqlalchemy.orm.exc.NoResultFound:
return None
def getTask(self, key):
try:
return self.session().query(Task).filter_by(key=key).one()
except sqlalchemy.orm.exc.NoResultFound:
return None
def getTaskByID(self, id):
try:
return self.session().query(Task).filter_by(id=id).one()
except sqlalchemy.orm.exc.NoResultFound:
return None
def getComment(self, key):
try:
return self.session().query(Comment).filter_by(key=key).one()
except sqlalchemy.orm.exc.NoResultFound:
return None
def getCommentByID(self, id):
try:
return self.session().query(Comment).filter_by(id=id).one()
except sqlalchemy.orm.exc.NoResultFound:
return None
def getHeld(self):
return self.session().query(Story).filter_by(held=True).all()
def getOutdated(self):
return self.session().query(Story).filter_by(outdated=True).all()
def getPendingStories(self):
return self.session().query(Story).filter_by(pending=True).all()
def getPendingTasks(self):
return self.session().query(Task).filter_by(pending=True).all()
def getUsers(self):
return self.session().query(User).all()
def getUser(self, key):
try:
return self.session().query(User).filter_by(key=key).one()
except sqlalchemy.orm.exc.NoResultFound:
return None
def getUserByID(self, id):
try:
return self.session().query(User).filter_by(id=id).one()
except sqlalchemy.orm.exc.NoResultFound:
return None
def getSystem(self):
try:
return self.session().query(System).one()
except sqlalchemy.orm.exc.NoResultFound:
return None
def getEvent(self, key):
try:
return self.session().query(Event).filter_by(key=key).one()
except sqlalchemy.orm.exc.NoResultFound:
return None
def createProject(self, *args, **kw):
o = Project(*args, **kw)
self.session().add(o)
self.session().flush()
return o
def createStory(self, *args, **kw):
s = Story(*args, **kw)
self.session().add(s)
self.session().flush()
return s
def createUser(self, *args, **kw):
a = User(*args, **kw)
self.session().add(a)
self.session().flush()
return a
def createSyncQuery(self, *args, **kw):
o = SyncQuery(*args, **kw)
self.session().add(o)
self.session().flush()
return o
def createTopic(self, *args, **kw):
o = Topic(*args, **kw)
self.session().add(o)
self.session().flush()
return o
def createTag(self, *args, **kw):
o = Tag(*args, **kw)
self.session().add(o)
self.session().flush()
return o
def createSystem(self, *args, **kw):
o = System(*args, **kw)
self.session().add(o)
self.session().flush()
return o

View File

@ -135,17 +135,7 @@ def sqlite_drop_columns(table_name, drop_columns):
for key in meta.tables[table_name].foreign_keys:
# If this is a single column constraint for a dropped column,
# don't copy it.
if isinstance(key.constraint.columns, sqlalchemy.sql.base.ColumnCollection):
# This is needed for SQLAlchemy >= 1.0.4
columns = [c.name for c in key.constraint.columns]
else:
# This is needed for SQLAlchemy <= 0.9.9. This is
# backwards compat code just in case someone updates
# Gertty without updating SQLAlchemy. This is simple
# enough to check and will hopefully avoid leaving the
# user's db in an inconsistent state. Remove this after
# Gertty 1.2.0.
columns = key.constraint.columns
columns = [c.name for c in key.constraint.columns]
if (len(columns)==1 and columns[0] in drop_columns):
continue
# Otherwise, recreate the constraint.

View File

@ -30,7 +30,7 @@ CURSOR_PAGE_DOWN = urwid.CURSOR_PAGE_DOWN
CURSOR_MAX_LEFT = urwid.CURSOR_MAX_LEFT
CURSOR_MAX_RIGHT = urwid.CURSOR_MAX_RIGHT
ACTIVATE = urwid.ACTIVATE
# Global gertty commands:
# Global boartty commands:
KILL = 'kill'
YANK = 'yank'
YANK_POP = 'yank pop'
@ -38,37 +38,31 @@ PREV_SCREEN = 'previous screen'
TOP_SCREEN = 'top screen'
HELP = 'help'
QUIT = 'quit'
CHANGE_SEARCH = 'change search'
REFINE_CHANGE_SEARCH = 'refine change search'
LIST_HELD = 'list held changes'
# Change screen:
TOGGLE_REVIEWED = 'toggle reviewed'
STORY_SEARCH = 'story search'
REFINE_STORY_SEARCH = 'refine story search'
LIST_HELD = 'list held stories'
NEW_STORY = 'new story'
# Story screen:
TOGGLE_HIDDEN = 'toggle hidden'
TOGGLE_STARRED = 'toggle starred'
TOGGLE_HELD = 'toggle held'
TOGGLE_MARK = 'toggle process mark'
REVIEW = 'review'
DIFF = 'diff'
LOCAL_CHECKOUT = 'local checkout'
LOCAL_CHERRY_PICK = 'local cherry pick'
LEAVE_COMMENT = 'leave comment'
SEARCH_RESULTS = 'search results'
NEXT_CHANGE = 'next change'
PREV_CHANGE = 'previous change'
NEXT_STORY = 'next story'
PREV_STORY = 'previous story'
TOGGLE_HIDDEN_COMMENTS = 'toggle hidden comments'
ABANDON_CHANGE = 'abandon change'
RESTORE_CHANGE = 'restore change'
REBASE_CHANGE = 'rebase change'
CHERRY_PICK_CHANGE = 'cherry pick change'
NEW_TASK = 'new task'
DELETE_TASK = 'delete task'
REFRESH = 'refresh'
EDIT_TOPIC = 'edit topic'
EDIT_COMMIT_MESSAGE = 'edit commit message'
SUBMIT_CHANGE = 'submit change'
EDIT_TITLE = 'edit title'
EDIT_DESCRIPTION = 'edit description'
SORT_BY_NUMBER = 'sort by number'
SORT_BY_UPDATED = 'sort by updated'
SORT_BY_LAST_SEEN = 'sort by last seen'
SORT_BY_REVERSE = 'reverse the sort'
# Project list screen:
TOGGLE_LIST_REVIEWED = 'toggle list reviewed'
TOGGLE_LIST_ACTIVE = 'toggle list active'
TOGGLE_LIST_SUBSCRIBED = 'toggle list subscribed'
TOGGLE_SUBSCRIBED = 'toggle subscribed'
NEW_PROJECT_TOPIC = 'new project topic'
@ -77,13 +71,11 @@ MOVE_PROJECT_TOPIC = 'move to project topic'
COPY_PROJECT_TOPIC = 'copy to project topic'
REMOVE_PROJECT_TOPIC = 'remove from project topic'
RENAME_PROJECT_TOPIC = 'rename project topic'
# Diff screens:
SELECT_PATCHSETS = 'select patchsets'
# Special:
FURTHER_INPUT = 'further input'
NEXT_SELECTABLE = 'next selectable'
PREV_SELECTABLE = 'prev selectable'
INTERACTIVE_SEARCH = 'interactive search'
# Special:
FURTHER_INPUT = 'further input'
DEFAULT_KEYMAP = {
REDRAW_SCREEN: 'ctrl l',
@ -104,37 +96,31 @@ DEFAULT_KEYMAP = {
TOP_SCREEN: 'meta home',
HELP: ['f1', '?'],
QUIT: ['ctrl q'],
CHANGE_SEARCH: 'ctrl o',
REFINE_CHANGE_SEARCH: 'meta o',
STORY_SEARCH: 'ctrl o',
REFINE_STORY_SEARCH: 'meta o',
LIST_HELD: 'f12',
NEW_STORY: 'ctrl n',
TOGGLE_REVIEWED: 'v',
TOGGLE_HIDDEN: 'k',
TOGGLE_STARRED: '*',
TOGGLE_HELD: '!',
TOGGLE_MARK: '%',
REVIEW: 'r',
DIFF: 'd',
LOCAL_CHECKOUT: 'c',
LOCAL_CHERRY_PICK: 'x',
LEAVE_COMMENT: 'r',
SEARCH_RESULTS: 'u',
NEXT_CHANGE: 'n',
PREV_CHANGE: 'p',
NEXT_STORY: 'n',
PREV_STORY: 'p',
TOGGLE_HIDDEN_COMMENTS: 't',
ABANDON_CHANGE: 'ctrl a',
RESTORE_CHANGE: 'ctrl e',
REBASE_CHANGE: 'ctrl b',
CHERRY_PICK_CHANGE: 'ctrl x',
NEW_TASK: 'N',
DELETE_TASK: 'delete',
REFRESH: 'ctrl r',
EDIT_TOPIC: 'ctrl t',
EDIT_COMMIT_MESSAGE: 'ctrl d',
SUBMIT_CHANGE: 'ctrl u',
EDIT_TITLE: 'ctrl t',
EDIT_DESCRIPTION: 'ctrl d',
SORT_BY_NUMBER: [['S', 'n']],
SORT_BY_UPDATED: [['S', 'u']],
SORT_BY_LAST_SEEN: [['S', 's']],
SORT_BY_REVERSE: [['S', 'r']],
TOGGLE_LIST_REVIEWED: 'l',
TOGGLE_LIST_ACTIVE: 'l',
TOGGLE_LIST_SUBSCRIBED: 'L',
TOGGLE_SUBSCRIBED: 's',
NEW_PROJECT_TOPIC: [['T', 'n']],
@ -144,7 +130,6 @@ DEFAULT_KEYMAP = {
REMOVE_PROJECT_TOPIC: [['T', 'D']],
RENAME_PROJECT_TOPIC: [['T', 'r']],
SELECT_PATCHSETS: 'p',
NEXT_SELECTABLE: 'tab',
PREV_SELECTABLE: 'shift tab',
INTERACTIVE_SEARCH: 'ctrl s',

View File

@ -14,8 +14,8 @@
import urwid
from gertty import keymap
from gertty.view import mouse_scroll_decorator
from boartty import keymap
from boartty.view import mouse_scroll_decorator
GLOBAL_HELP = (
(keymap.HELP,
@ -25,11 +25,11 @@ GLOBAL_HELP = (
(keymap.TOP_SCREEN,
"Back to project list"),
(keymap.QUIT,
"Quit Gertty"),
(keymap.CHANGE_SEARCH,
"Search for changes"),
"Quit Boardtty"),
(keymap.STORY_SEARCH,
"Search for stories"),
(keymap.LIST_HELD,
"List held changes"),
"List held stories"),
(keymap.KILL,
"Kill to end of line (editing)"),
(keymap.YANK,
@ -192,7 +192,8 @@ class LineEditDialog(ButtonDialog):
class TextEditDialog(urwid.WidgetWrap, LineBoxTitlePropertyMixin):
signals = ['save', 'cancel']
def __init__(self, title, prompt, button, text, ring=None):
def __init__(self, app, title, prompt, button, text, ring=None):
self.app = app
save_button = FixedButton(button)
cancel_button = FixedButton('Cancel')
urwid.connect_signal(save_button, 'click',
@ -212,6 +213,16 @@ class TextEditDialog(urwid.WidgetWrap, LineBoxTitlePropertyMixin):
fill = urwid.Filler(pile, valign='top')
super(TextEditDialog, self).__init__(urwid.LineBox(fill, title))
def keypress(self, size, key):
if not self.app.input_buffer:
key = super(TextEditDialog, self).keypress(size, key)
keys = self.app.input_buffer + [key]
commands = self.app.config.keymap.getCommands(keys)
if keymap.PREV_SCREEN in commands:
self._emit('cancel')
return None
return key
class MessageDialog(ButtonDialog):
signals = ['close']
def __init__(self, title, message):
@ -530,3 +541,76 @@ class MyGridFlow(urwid.GridFlow):
c.focus_position = i
break
return p
class SearchSelectInnerButton(urwid.Button):
def __init__(self, key, value):
self.key = key
self.value = value
super(SearchSelectInnerButton, self).__init__(value)
class SearchSelectDialog(urwid.WidgetWrap, LineBoxTitlePropertyMixin):
"""
A dialog that allows a user to select one item from a list, and
interactively refine the list by searching.
"""
signals = ['save']
def __init__(self, app, title, current_key, values):
self.app = app
rows = []
self.key = None
self.value = None
for key, value in values():
button = SearchSelectInnerButton(key, value)
urwid.connect_signal(button, 'click',
lambda b:self.onSelected(b))
rows.append(button)
pile = urwid.Pile(rows)
fill = urwid.Filler(pile, valign='top')
super(SearchSelectDialog, self).__init__(urwid.LineBox(fill, title))
def onSelected(self, b):
self.key = b.key
self.value = b.value
self._emit('save')
self.app.backScreen()
class SearchSelectButton(TextButton):
"""
A button that displays a value; when clicked, a SearchSelectDialog
is opened to select a new value.
"""
signals = ['changed']
def __init__(self, app, title, key, value, values):
self.app = app
self.title = title
self.values = values
urwid.connect_signal(self, 'click',
lambda button:self.onClick())
super(SearchSelectButton, self).__init__(u'')
self.update(key, value)
def onClick(self):
dialog = SearchSelectDialog(self.app, self.title, self.key, self.values)
urwid.connect_signal(dialog, 'save',
lambda d:self.onChanged(d))
self.app.popup(dialog,
relative_width=30, relative_height=75,
min_width=30, min_height=20)
def update(self, key, value):
self.key = key
self.value = value
if self.value is None:
label = u'Select'
else:
label = self.value
self.text.set_text(label)
def onChanged(self, dialog):
if dialog.key is None:
return
self.update(dialog.key, dialog.value)
self._emit('changed')

103
boartty/palette.py Normal file
View File

@ -0,0 +1,103 @@
# Copyright 2014 OpenStack Foundation
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
DEFAULT_PALETTE={
'focused': ['default,standout', ''],
'header': ['white,bold', 'dark blue'],
'error': ['light red', 'dark blue'],
'table-header': ['white,bold', ''],
'link': ['dark blue', ''],
'focused-link': ['light blue', ''],
'footer': ['light gray', 'dark gray'],
'search-result': ['default,standout', ''],
# Story view
'story-data': ['dark cyan', ''],
'focused-story-data': ['light cyan', ''],
'story-header': ['light blue', ''],
'task-id': ['dark cyan', ''],
'task-title': ['light green', ''],
'task-project': ['light blue', ''],
'task-status': ['yellow', ''],
'task-assignee': ['light cyan', ''],
'task-note': ['default', ''],
'focused-task-id': ['dark cyan,standout', ''],
'focused-task-title': ['light green,standout', ''],
'focused-task-project': ['light blue,standout', ''],
'focused-task-status': ['yellow,standout', ''],
'focused-task-assignee': ['dark cyan,standout', ''],
'focused-task-note': ['default', ''],
'story-event-name': ['yellow', ''],
'story-event-own-name': ['light cyan', ''],
'story-event-header': ['brown', ''],
'story-event-own-header': ['dark cyan', ''],
'story-event-draft': ['dark red', ''],
'story-event-button': ['dark magenta', ''],
'focused-story-event-button': ['light magenta', ''],
# project list
'active-project': ['white', ''],
'subscribed-project': ['default', ''],
'unsubscribed-project': ['dark gray', ''],
'marked-project': ['light cyan', ''],
'focused-active-project': ['white,standout', ''],
'focused-subscribed-project': ['default,standout', ''],
'focused-unsubscribed-project': ['dark gray,standout', ''],
'focused-marked-project': ['light cyan,standout', ''],
# story list
'active-story': ['default', ''],
'inactive-story': ['dark gray', ''],
'focused-active-story': ['default,standout', ''],
'focused-inactive-story': ['dark gray,standout', ''],
'starred-story': ['light cyan', ''],
'focused-starred-story': ['light cyan,standout', ''],
'held-story': ['light red', ''],
'focused-held-story': ['light red,standout', ''],
'marked-story': ['dark cyan', ''],
'focused-marked-story': ['dark cyan,standout', ''],
}
# A delta from the default palette
LIGHT_PALETTE = {
'table-header': ['black,bold', ''],
'active-project': ['black', ''],
'subscribed-project': ['dark gray', ''],
'unsubscribed-project': ['dark gray', ''],
'focused-active-project': ['black,standout', ''],
'focused-subscribed-project': ['dark gray,standout', ''],
'focused-unsubscribed-project': ['dark gray,standout', ''],
'story-data': ['dark blue,bold', ''],
'focused-story-data': ['dark blue,standout', ''],
'story-event-name': ['brown', ''],
'story-event-own-name': ['dark blue,bold', ''],
'story-event-header': ['black', ''],
'story-event-own-header': ['black,bold', ''],
'focused-link': ['dark blue,bold', ''],
}
class Palette(object):
def __init__(self, config):
self.palette = {}
self.palette.update(DEFAULT_PALETTE)
self.update(config)
def update(self, config):
d = config.copy()
if 'name' in d:
del d['name']
self.palette.update(d)
def getPalette(self):
ret = []
for k,v in self.palette.items():
ret.append(tuple([k]+v))
return ret

View File

@ -15,8 +15,8 @@
import sqlalchemy.sql.expression
from sqlalchemy.sql.expression import and_
from gertty.search import tokenizer, parser
import gertty.db
from boartty.search import tokenizer, parser
import boartty.db
class SearchSyntaxError(Exception):
@ -35,7 +35,7 @@ class SearchCompiler(object):
while stack:
x = stack.pop()
if hasattr(x, 'table'):
if (x.table != gertty.db.change_table
if (x.table != boartty.db.story_table
and hasattr(x.table, 'name')):
tables.add(x.table)
for child in x.get_children():
@ -47,19 +47,19 @@ class SearchCompiler(object):
self.parser.username = self.username
result = self.parser.parse(data, lexer=self.lexer)
tables = self.findTables(result)
if gertty.db.project_table in tables:
result = and_(gertty.db.change_table.c.project_key == gertty.db.project_table.c.key,
if boartty.db.project_table in tables:
result = and_(boartty.db.story_table.c.project_key == boartty.db.project_table.c.key,
result)
tables.remove(gertty.db.project_table)
if gertty.db.account_table in tables:
result = and_(gertty.db.change_table.c.account_key == gertty.db.account_table.c.key,
tables.remove(boartty.db.project_table)
if boartty.db.user_table in tables:
result = and_(boartty.db.story_table.c.user_key == boartty.db.user_table.c.key,
result)
tables.remove(gertty.db.account_table)
if gertty.db.file_table in tables:
result = and_(gertty.db.file_table.c.revision_key == gertty.db.revision_table.c.key,
gertty.db.revision_table.c.change_key == gertty.db.change_table.c.key,
result)
tables.remove(gertty.db.file_table)
tables.remove(boartty.db.user_table)
#if boartty.db.file_table in tables:
# result = and_(boartty.db.file_table.c.revision_key == boartty.db.revision_table.c.key,
# boartty.db.revision_table.c.story_key == boartty.db.story_table.c.key,
# result)
# tables.remove(boartty.db.file_table)
if tables:
raise Exception("Unknown table in search: %s" % tables)
return result

View File

@ -18,9 +18,9 @@ import re
import ply.yacc as yacc
from sqlalchemy.sql.expression import and_, or_, not_, select, func
import gertty.db
import gertty.search
from gertty.search.tokenizer import tokens # NOQA
import boartty.db
import boartty.search
from boartty.search.tokenizer import tokens # NOQA
def age_to_delta(delta, unit):
if unit in ['seconds', 'second', 'sec', 's']:
@ -68,7 +68,7 @@ def SearchParser():
elif p[2].lower() == 'or':
p[0] = or_(p[1], p[3])
else:
raise gertty.search.SearchSyntaxError("Boolean %s not recognized" % p[2])
raise boartty.search.SearchSyntaxError("Boolean %s not recognized" % p[2])
def p_negative_expr(p):
'''negative_expr : NOT expression
@ -78,7 +78,7 @@ def SearchParser():
def p_term(p):
'''term : age_term
| recentlyseen_term
| change_term
| story_term
| owner_term
| reviewer_term
| commit_term
@ -111,102 +111,107 @@ def SearchParser():
delta = p[2]
unit = p[3]
delta = age_to_delta(delta, unit)
p[0] = gertty.db.change_table.c.updated < (now-datetime.timedelta(seconds=delta))
p[0] = boartty.db.story_table.c.updated < (now-datetime.timedelta(seconds=delta))
def p_recentlyseen_term(p):
'''recentlyseen_term : OP_RECENTLYSEEN NUMBER string'''
# A gertty extension
# A boartty extension
now = datetime.datetime.utcnow()
delta = p[2]
unit = p[3]
delta = age_to_delta(delta, unit)
s = select([func.datetime(func.max(gertty.db.change_table.c.last_seen), '-%s seconds' % delta)],
s = select([func.datetime(func.max(boartty.db.story_table.c.last_seen), '-%s seconds' % delta)],
correlate=False)
p[0] = gertty.db.change_table.c.last_seen >= s
p[0] = boartty.db.story_table.c.last_seen >= s
def p_change_term(p):
'''change_term : OP_CHANGE CHANGE_ID
| OP_CHANGE NUMBER'''
def p_story_term(p):
'''story_term : OP_STORY STORY_ID
| OP_STORY NUMBER'''
if type(p[2]) == int:
p[0] = gertty.db.change_table.c.number == p[2]
p[0] = boartty.db.story_table.c.number == p[2]
else:
p[0] = gertty.db.change_table.c.change_id == p[2]
p[0] = boartty.db.story_table.c.story_id == p[2]
def p_owner_term(p):
'''owner_term : OP_OWNER string'''
if p[2] == 'self':
username = p.parser.username
p[0] = gertty.db.account_table.c.username == username
p[0] = boartty.db.user_table.c.username == username
else:
p[0] = or_(gertty.db.account_table.c.username == p[2],
gertty.db.account_table.c.email == p[2],
gertty.db.account_table.c.name == p[2])
p[0] = or_(boartty.db.user_table.c.username == p[2],
boartty.db.user_table.c.email == p[2],
boartty.db.user_table.c.name == p[2])
def p_reviewer_term(p):
'''reviewer_term : OP_REVIEWER string
| OP_REVIEWER NUMBER'''
filters = []
filters.append(gertty.db.approval_table.c.change_key == gertty.db.change_table.c.key)
filters.append(gertty.db.approval_table.c.account_key == gertty.db.account_table.c.key)
filters.append(boartty.db.approval_table.c.story_key == boartty.db.story_table.c.key)
filters.append(boartty.db.approval_table.c.user_key == boartty.db.user_table.c.key)
try:
number = int(p[2])
except:
number = None
if number is not None:
filters.append(gertty.db.account_table.c.id == number)
filters.append(boartty.db.user_table.c.id == number)
elif p[2] == 'self':
username = p.parser.username
filters.append(gertty.db.account_table.c.username == username)
filters.append(boartty.db.user_table.c.username == username)
else:
filters.append(or_(gertty.db.account_table.c.username == p[2],
gertty.db.account_table.c.email == p[2],
gertty.db.account_table.c.name == p[2]))
s = select([gertty.db.change_table.c.key], correlate=False).where(and_(*filters))
p[0] = gertty.db.change_table.c.key.in_(s)
filters.append(or_(boartty.db.user_table.c.username == p[2],
boartty.db.user_table.c.email == p[2],
boartty.db.user_table.c.name == p[2]))
s = select([boartty.db.story_table.c.key], correlate=False).where(and_(*filters))
p[0] = boartty.db.story_table.c.key.in_(s)
def p_commit_term(p):
'''commit_term : OP_COMMIT string'''
filters = []
filters.append(gertty.db.revision_table.c.change_key == gertty.db.change_table.c.key)
filters.append(gertty.db.revision_table.c.commit == p[2])
s = select([gertty.db.change_table.c.key], correlate=False).where(and_(*filters))
p[0] = gertty.db.change_table.c.key.in_(s)
filters.append(boartty.db.revision_table.c.story_key == boartty.db.story_table.c.key)
filters.append(boartty.db.revision_table.c.commit == p[2])
s = select([boartty.db.story_table.c.key], correlate=False).where(and_(*filters))
p[0] = boartty.db.story_table.c.key.in_(s)
def p_project_term(p):
'''project_term : OP_PROJECT string'''
if p[2].startswith('^'):
p[0] = func.matches(p[2], gertty.db.project_table.c.name)
p[0] = func.matches(p[2], boartty.db.project_table.c.name)
else:
p[0] = gertty.db.project_table.c.name == p[2]
p[0] = boartty.db.project_table.c.name == p[2]
def p_projects_term(p):
'''projects_term : OP_PROJECTS string'''
p[0] = gertty.db.project_table.c.name.like('%s%%' % p[2])
p[0] = boartty.db.project_table.c.name.like('%s%%' % p[2])
def p_project_key_term(p):
'''project_key_term : OP_PROJECT_KEY NUMBER'''
p[0] = gertty.db.change_table.c.project_key == p[2]
#p[0] = boartty.db.story_table.c.project_key == p[2]
filters = []
filters.append(boartty.db.task_table.c.story_key == boartty.db.story_table.c.key)
filters.append(boartty.db.task_table.c.project_key == p[2])
s = select([boartty.db.story_table.c.key], correlate=False).where(and_(*filters))
p[0] = boartty.db.story_table.c.key.in_(s)
def p_branch_term(p):
'''branch_term : OP_BRANCH string'''
if p[2].startswith('^'):
p[0] = func.matches(p[2], gertty.db.change_table.c.branch)
p[0] = func.matches(p[2], boartty.db.story_table.c.branch)
else:
p[0] = gertty.db.change_table.c.branch == p[2]
p[0] = boartty.db.story_table.c.branch == p[2]
def p_topic_term(p):
'''topic_term : OP_TOPIC string'''
if p[2].startswith('^'):
p[0] = func.matches(p[2], gertty.db.change_table.c.topic)
p[0] = func.matches(p[2], boartty.db.story_table.c.topic)
else:
p[0] = gertty.db.change_table.c.topic == p[2]
p[0] = boartty.db.story_table.c.topic == p[2]
def p_ref_term(p):
'''ref_term : OP_REF string'''
if p[2].startswith('^'):
p[0] = func.matches(p[2], 'refs/heads/'+gertty.db.change_table.c.branch)
p[0] = func.matches(p[2], 'refs/heads/'+boartty.db.story_table.c.branch)
else:
p[0] = gertty.db.change_table.c.branch == p[2][len('refs/heads/'):]
p[0] = boartty.db.story_table.c.branch == p[2][len('refs/heads/'):]
label_re = re.compile(r'(?P<label>[a-zA-Z0-9_-]+([a-zA-Z]|((?<![-+])[0-9])))'
r'(?P<operator>[<>]?=?)(?P<value>[-+]?[0-9]+)'
@ -221,60 +226,60 @@ def SearchParser():
user = args.group('user')
filters = []
filters.append(gertty.db.approval_table.c.change_key == gertty.db.change_table.c.key)
filters.append(gertty.db.approval_table.c.category == label)
filters.append(boartty.db.approval_table.c.story_key == boartty.db.story_table.c.key)
filters.append(boartty.db.approval_table.c.category == label)
if op == '=':
filters.append(gertty.db.approval_table.c.value == value)
filters.append(boartty.db.approval_table.c.value == value)
elif op == '>=':
filters.append(gertty.db.approval_table.c.value >= value)
filters.append(boartty.db.approval_table.c.value >= value)
elif op == '<=':
filters.append(gertty.db.approval_table.c.value <= value)
filters.append(boartty.db.approval_table.c.value <= value)
if user is not None:
filters.append(gertty.db.approval_table.c.account_key == gertty.db.account_table.c.key)
filters.append(boartty.db.approval_table.c.user_key == boartty.db.user_table.c.key)
if user == 'self':
filters.append(gertty.db.account_table.c.username == p.parser.username)
filters.append(boartty.db.user_table.c.username == p.parser.username)
else:
filters.append(
or_(gertty.db.account_table.c.username == user,
gertty.db.account_table.c.email == user,
gertty.db.account_table.c.name == user))
s = select([gertty.db.change_table.c.key], correlate=False).where(and_(*filters))
p[0] = gertty.db.change_table.c.key.in_(s)
or_(boartty.db.user_table.c.username == user,
boartty.db.user_table.c.email == user,
boartty.db.user_table.c.name == user))
s = select([boartty.db.story_table.c.key], correlate=False).where(and_(*filters))
p[0] = boartty.db.story_table.c.key.in_(s)
def p_message_term(p):
'''message_term : OP_MESSAGE string'''
filters = []
filters.append(gertty.db.revision_table.c.change_key == gertty.db.change_table.c.key)
filters.append(gertty.db.revision_table.c.message.like('%%%s%%' % p[2]))
s = select([gertty.db.change_table.c.key], correlate=False).where(and_(*filters))
p[0] = gertty.db.change_table.c.key.in_(s)
filters.append(boartty.db.revision_table.c.story_key == boartty.db.story_table.c.key)
filters.append(boartty.db.revision_table.c.message.like('%%%s%%' % p[2]))
s = select([boartty.db.story_table.c.key], correlate=False).where(and_(*filters))
p[0] = boartty.db.story_table.c.key.in_(s)
def p_comment_term(p):
'''comment_term : OP_COMMENT string'''
filters = []
filters.append(gertty.db.revision_table.c.change_key == gertty.db.change_table.c.key)
filters.append(gertty.db.revision_table.c.message == p[2])
revision_select = select([gertty.db.change_table.c.key], correlate=False).where(and_(*filters))
filters.append(boartty.db.revision_table.c.story_key == boartty.db.story_table.c.key)
filters.append(boartty.db.revision_table.c.message == p[2])
revision_select = select([boartty.db.story_table.c.key], correlate=False).where(and_(*filters))
filters = []
filters.append(gertty.db.revision_table.c.change_key == gertty.db.change_table.c.key)
filters.append(gertty.db.comment_table.c.revision_key == gertty.db.revision_table.c.key)
filters.append(gertty.db.comment_table.c.message == p[2])
comment_select = select([gertty.db.change_table.c.key], correlate=False).where(and_(*filters))
p[0] = or_(gertty.db.change_table.c.key.in_(comment_select),
gertty.db.change_table.c.key.in_(revision_select))
filters.append(boartty.db.revision_table.c.story_key == boartty.db.story_table.c.key)
filters.append(boartty.db.comment_table.c.revision_key == boartty.db.revision_table.c.key)
filters.append(boartty.db.comment_table.c.message == p[2])
comment_select = select([boartty.db.story_table.c.key], correlate=False).where(and_(*filters))
p[0] = or_(boartty.db.story_table.c.key.in_(comment_select),
boartty.db.story_table.c.key.in_(revision_select))
def p_has_term(p):
'''has_term : OP_HAS string'''
#TODO: implement star
if p[2] == 'draft':
filters = []
filters.append(gertty.db.revision_table.c.change_key == gertty.db.change_table.c.key)
filters.append(gertty.db.message_table.c.revision_key == gertty.db.revision_table.c.key)
filters.append(gertty.db.message_table.c.draft == True)
s = select([gertty.db.change_table.c.key], correlate=False).where(and_(*filters))
p[0] = gertty.db.change_table.c.key.in_(s)
filters.append(boartty.db.revision_table.c.story_key == boartty.db.story_table.c.key)
filters.append(boartty.db.message_table.c.revision_key == boartty.db.revision_table.c.key)
filters.append(boartty.db.message_table.c.draft == True)
s = select([boartty.db.story_table.c.key], correlate=False).where(and_(*filters))
p[0] = boartty.db.story_table.c.key.in_(s)
else:
raise gertty.search.SearchSyntaxError('Syntax error: has:%s is not supported' % p[2])
raise boartty.search.SearchSyntaxError('Syntax error: has:%s is not supported' % p[2])
def p_is_term(p):
'''is_term : OP_IS string'''
@ -282,58 +287,58 @@ def SearchParser():
username = p.parser.username
if p[2] == 'reviewed':
filters = []
filters.append(gertty.db.approval_table.c.change_key == gertty.db.change_table.c.key)
filters.append(gertty.db.approval_table.c.value != 0)
s = select([gertty.db.change_table.c.key], correlate=False).where(and_(*filters))
p[0] = gertty.db.change_table.c.key.in_(s)
filters.append(boartty.db.approval_table.c.story_key == boartty.db.story_table.c.key)
filters.append(boartty.db.approval_table.c.value != 0)
s = select([boartty.db.story_table.c.key], correlate=False).where(and_(*filters))
p[0] = boartty.db.story_table.c.key.in_(s)
elif p[2] == 'open':
p[0] = gertty.db.change_table.c.status.notin_(['MERGED', 'ABANDONED'])
p[0] = boartty.db.story_table.c.status.notin_(['MERGED', 'ABANDONED'])
elif p[2] == 'closed':
p[0] = gertty.db.change_table.c.status.in_(['MERGED', 'ABANDONED'])
p[0] = boartty.db.story_table.c.status.in_(['MERGED', 'ABANDONED'])
elif p[2] == 'submitted':
p[0] = gertty.db.change_table.c.status == 'SUBMITTED'
p[0] = boartty.db.story_table.c.status == 'SUBMITTED'
elif p[2] == 'merged':
p[0] = gertty.db.change_table.c.status == 'MERGED'
p[0] = boartty.db.story_table.c.status == 'MERGED'
elif p[2] == 'abandoned':
p[0] = gertty.db.change_table.c.status == 'ABANDONED'
p[0] = boartty.db.story_table.c.status == 'ABANDONED'
elif p[2] == 'owner':
p[0] = gertty.db.account_table.c.username == username
p[0] = boartty.db.user_table.c.username == username
elif p[2] == 'starred':
p[0] = gertty.db.change_table.c.starred == True
p[0] = boartty.db.story_table.c.starred == True
elif p[2] == 'held':
# A gertty extension
p[0] = gertty.db.change_table.c.held == True
# A boartty extension
p[0] = boartty.db.story_table.c.held == True
elif p[2] == 'reviewer':
filters = []
filters.append(gertty.db.approval_table.c.change_key == gertty.db.change_table.c.key)
filters.append(gertty.db.approval_table.c.account_key == gertty.db.account_table.c.key)
filters.append(gertty.db.account_table.c.username == username)
s = select([gertty.db.change_table.c.key], correlate=False).where(and_(*filters))
p[0] = gertty.db.change_table.c.key.in_(s)
filters.append(boartty.db.approval_table.c.story_key == boartty.db.story_table.c.key)
filters.append(boartty.db.approval_table.c.user_key == boartty.db.user_table.c.key)
filters.append(boartty.db.user_table.c.username == username)
s = select([boartty.db.story_table.c.key], correlate=False).where(and_(*filters))
p[0] = boartty.db.story_table.c.key.in_(s)
elif p[2] == 'watched':
p[0] = gertty.db.project_table.c.subscribed == True
p[0] = boartty.db.project_table.c.subscribed == True
else:
raise gertty.search.SearchSyntaxError('Syntax error: is:%s is not supported' % p[2])
raise boartty.search.SearchSyntaxError('Syntax error: is:%s is not supported' % p[2])
def p_file_term(p):
'''file_term : OP_FILE string'''
if p[2].startswith('^'):
p[0] = and_(or_(func.matches(p[2], gertty.db.file_table.c.path),
func.matches(p[2], gertty.db.file_table.c.old_path)),
gertty.db.file_table.c.status is not None)
p[0] = and_(or_(func.matches(p[2], boartty.db.file_table.c.path),
func.matches(p[2], boartty.db.file_table.c.old_path)),
boartty.db.file_table.c.status is not None)
else:
p[0] = and_(or_(gertty.db.file_table.c.path == p[2],
gertty.db.file_table.c.old_path == p[2]),
gertty.db.file_table.c.status is not None)
p[0] = and_(or_(boartty.db.file_table.c.path == p[2],
boartty.db.file_table.c.old_path == p[2]),
boartty.db.file_table.c.status is not None)
def p_status_term(p):
'''status_term : OP_STATUS string'''
if p[2] == 'open':
p[0] = gertty.db.change_table.c.status.notin_(['MERGED', 'ABANDONED'])
p[0] = boartty.db.story_table.c.status.notin_(['MERGED', 'ABANDONED'])
elif p[2] == 'closed':
p[0] = gertty.db.change_table.c.status.in_(['MERGED', 'ABANDONED'])
p[0] = boartty.db.story_table.c.status.in_(['MERGED', 'ABANDONED'])
else:
p[0] = gertty.db.change_table.c.status == p[2].upper()
p[0] = boartty.db.story_table.c.status == p[2]
def p_limit_term(p):
'''limit_term : OP_LIMIT NUMBER'''
@ -341,7 +346,7 @@ def SearchParser():
# applied to the query operation and so can not be returned as
# part of the production here. The information would need to
# be returned out-of-band. In the mean time, since limits are
# not as important in gertty, make this a no-op for now so
# not as important in boartty, make this a no-op for now so
# that it does not produce a syntax error.
p[0] = (True == True)
@ -351,9 +356,9 @@ def SearchParser():
def p_error(p):
if p:
raise gertty.search.SearchSyntaxError('Syntax error at "%s" in search string "%s" (col %s)' % (
raise boartty.search.SearchSyntaxError('Syntax error at "%s" in search string "%s" (col %s)' % (
p.lexer.lexdata[p.lexpos:], p.lexer.lexdata, p.lexpos))
else:
raise gertty.search.SearchSyntaxError('Syntax error: EOF in search string')
raise boartty.search.SearchSyntaxError('Syntax error: EOF in search string')
return yacc.yacc(debug=0, write_tables=0)

View File

@ -17,8 +17,8 @@ import six
operators = {
'age': 'OP_AGE',
'recentlyseen': 'OP_RECENTLYSEEN', # Gertty extension
'change': 'OP_CHANGE',
'recentlyseen': 'OP_RECENTLYSEEN', # Boardtty extension
'story': 'OP_STORY',
'owner': 'OP_OWNER',
#'OP_OWNERIN', # needs local group membership
'reviewer': 'OP_REVIEWER',
@ -26,7 +26,7 @@ operators = {
'commit': 'OP_COMMIT',
'project': 'OP_PROJECT',
'projects': 'OP_PROJECTS',
'_project_key': 'OP_PROJECT_KEY', # internal gertty use only
'_project_key': 'OP_PROJECT_KEY', # internal boartty use only
'branch': 'OP_BRANCH',
'topic': 'OP_TOPIC',
'ref': 'OP_REF',
@ -56,7 +56,7 @@ tokens = [
'LPAREN',
'RPAREN',
'NUMBER',
'CHANGE_ID',
'STORY_ID',
'SSTRING',
'DSTRING',
'USTRING',
@ -75,7 +75,7 @@ def SearchTokenizer():
t.type = operators.get(t.value[:-1], 'OP')
return t
def t_CHANGE_ID(t):
def t_STORY_ID(t):
r'I[a-fA-F0-9]{7,40}'
return t

1137
boartty/sync.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -14,4 +14,4 @@
import pbr.version
version_info = pbr.version.VersionInfo('gertty')
version_info = pbr.version.VersionInfo('boartty')

View File

@ -16,11 +16,13 @@
import logging
import urwid
from gertty import keymap
from gertty import mywid
from gertty import sync
from gertty.view import change_list as view_change_list
from gertty.view import mouse_scroll_decorator
from boartty import keymap
from boartty import mywid
from boartty import sync
from boartty.view import story_list as view_story_list
from boartty.view import mouse_scroll_decorator
ACTIVE_COL_WIDTH = 7
class TopicSelectDialog(urwid.WidgetWrap):
signals = ['ok', 'cancel']
@ -59,7 +61,7 @@ class TopicSelectDialog(urwid.WidgetWrap):
class ProjectRow(urwid.Button):
project_focus_map = {None: 'focused',
'unreviewed-project': 'focused-unreviewed-project',
'active-project': 'focused-active-project',
'subscribed-project': 'focused-subscribed-project',
'unsubscribed-project': 'focused-unsubscribed-project',
'marked-project': 'focused-marked-project',
@ -94,12 +96,10 @@ class ProjectRow(urwid.Button):
self.name = mywid.SearchableText('')
self._setName(project.name, self.indent)
self.name.set_wrap_mode('clip')
self.unreviewed_changes = urwid.Text(u'', align=urwid.RIGHT)
self.open_changes = urwid.Text(u'', align=urwid.RIGHT)
self.active_stories = urwid.Text(u'', align=urwid.RIGHT)
col = urwid.Columns([
self.name,
('fixed', 11, self.unreviewed_changes),
('fixed', 5, self.open_changes),
('fixed', ACTIVE_COL_WIDTH, self.active_stories),
])
self.row_style = urwid.AttrMap(col, '')
self._w = urwid.AttrMap(self.row_style, None, focus_map=self.project_focus_map)
@ -111,18 +111,14 @@ class ProjectRow(urwid.Button):
def update(self, project):
cache = self.app.project_cache.get(project)
if project.subscribed:
if cache['unreviewed_changes'] > 0:
style = 'unreviewed-project'
else:
style = 'subscribed-project'
style = 'subscribed-project'
else:
style = 'unsubscribed-project'
self._style = style
if self.mark:
style = 'marked-project'
self.row_style.set_attr_map({None: style})
self.unreviewed_changes.set_text('%i ' % cache['unreviewed_changes'])
self.open_changes.set_text('%i ' % cache['open_changes'])
self.active_stories.set_text('%i ' % cache['active_stories'])
def toggleMark(self):
self.mark = not self.mark
@ -160,12 +156,10 @@ class TopicRow(urwid.Button):
self.name = urwid.Text('')
self._setName(topic.name)
self.name.set_wrap_mode('clip')
self.unreviewed_changes = urwid.Text(u'', align=urwid.RIGHT)
self.open_changes = urwid.Text(u'', align=urwid.RIGHT)
self.active_stories = urwid.Text(u'', align=urwid.RIGHT)
col = urwid.Columns([
self.name,
('fixed', 11, self.unreviewed_changes),
('fixed', 5, self.open_changes),
('fixed', ACTIVE_COL_WIDTH, self.active_stories),
])
self.row_style = urwid.AttrMap(col, '')
self._w = urwid.AttrMap(self.row_style, None, focus_map=self.project_focus_map)
@ -173,16 +167,12 @@ class TopicRow(urwid.Button):
self.row_style.set_attr_map({None: self._style})
self.update(topic)
def update(self, topic, unreviewed_changes=None, open_changes=None):
def update(self, topic, active_stories=None):
self._setName(topic.name)
if unreviewed_changes is None:
self.unreviewed_changes.set_text('')
if active_stories is None:
self.active_stories.set_text('')
else:
self.unreviewed_changes.set_text('%i ' % unreviewed_changes)
if open_changes is None:
self.open_changes.set_text('')
else:
self.open_changes.set_text('%i ' % open_changes)
self.active_stories.set_text('%i ' % active_stories)
def toggleMark(self):
self.mark = not self.mark
@ -196,8 +186,7 @@ class TopicRow(urwid.Button):
class ProjectListHeader(urwid.WidgetWrap):
def __init__(self):
cols = [urwid.Text(u' Project'),
(11, urwid.Text(u'Unreviewed')),
(5, urwid.Text(u'Open'))]
(ACTIVE_COL_WIDTH, urwid.Text(u'Active'))]
super(ProjectListHeader, self).__init__(urwid.Columns(cols))
@mouse_scroll_decorator.ScrollByWheel
@ -206,8 +195,8 @@ class ProjectListView(urwid.WidgetWrap, mywid.Searchable):
return [
(keymap.TOGGLE_LIST_SUBSCRIBED,
"Toggle whether only subscribed projects or all projects are listed"),
(keymap.TOGGLE_LIST_REVIEWED,
"Toggle listing of projects with unreviewed changes"),
(keymap.TOGGLE_LIST_ACTIVE,
"Toggle listing of projects with active changes"),
(keymap.TOGGLE_SUBSCRIBED,
"Toggle the subscription flag for the selected project"),
(keymap.REFRESH,
@ -237,10 +226,10 @@ class ProjectListView(urwid.WidgetWrap, mywid.Searchable):
def __init__(self, app):
super(ProjectListView, self).__init__(urwid.Pile([]))
self.log = logging.getLogger('gertty.view.project_list')
self.log = logging.getLogger('boartty.view.project_list')
self.searchInit()
self.app = app
self.unreviewed = True
self.active = True
self.subscribed = True
self.project_rows = {}
self.topic_rows = {}
@ -257,10 +246,10 @@ class ProjectListView(urwid.WidgetWrap, mywid.Searchable):
def interested(self, event):
if not (isinstance(event, sync.ProjectAddedEvent)
or
isinstance(event, sync.ChangeAddedEvent)
isinstance(event, sync.StoryAddedEvent)
or
(isinstance(event, sync.ChangeUpdatedEvent) and
(event.status_changed or event.review_flag_changed))):
(isinstance(event, sync.StoryUpdatedEvent) and
event.status_changed)):
self.log.debug("Ignoring refresh project list due to event %s" % (event,))
return False
self.log.debug("Refreshing project list due to event %s" % (event,))
@ -326,8 +315,8 @@ class ProjectListView(urwid.WidgetWrap, mywid.Searchable):
if self.subscribed:
self.title = u'Subscribed projects'
self.short_title = self.title[:]
if self.unreviewed:
self.title += u' with unreviewed changes'
if self.active:
self.title += u' with active stories'
else:
self.title = u'All projects'
self.short_title = self.title[:]
@ -335,28 +324,24 @@ class ProjectListView(urwid.WidgetWrap, mywid.Searchable):
with self.app.db.getSession() as session:
i = 0
for project in session.getProjects(topicless=True,
subscribed=self.subscribed, unreviewed=self.unreviewed):
subscribed=self.subscribed, active=self.active):
#self.log.debug("project: %s" % project.name)
i = self._projectRow(i, project, None)
for topic in session.getTopics():
#self.log.debug("topic: %s" % topic.name)
i = self._topicRow(i, topic)
topic_unreviewed = 0
topic_open = 0
topic_active = 0
for project in topic.projects:
#self.log.debug(" project: %s" % project.name)
cache = self.app.project_cache.get(project)
topic_unreviewed += cache['unreviewed_changes']
topic_open += cache['open_changes']
topic_active += cache['active_stories']
if self.subscribed:
if not project.subscribed:
continue
if self.unreviewed and not cache['unreviewed_changes']:
continue
if topic.key in self.open_topics:
i = self._projectRow(i, project, topic)
topic_row = self.topic_rows.get(topic.key)
topic_row.update(topic, topic_unreviewed, topic_open)
topic_row.update(topic, topic_active)
while i < len(self.listbox.body):
current_row = self.listbox.body[i]
self._deleteRow(current_row)
@ -370,10 +355,11 @@ class ProjectListView(urwid.WidgetWrap, mywid.Searchable):
def onSelect(self, button, data):
project_key, project_name = data
self.app.changeScreen(view_change_list.ChangeListView(
self.app,
"_project_key:%s %s" % (project_key, self.app.config.project_change_list_query),
project_name, project_key=project_key, unreviewed=True))
self.app.changeScreen(view_story_list.StoryListView(
self.app,
"_project_key:%s %s" % (project_key,
self.app.config.project_story_list_query),
project_name, project_key=project_key, active=True))
def onSelectTopic(self, button, data):
topic_key = data[0]
@ -553,8 +539,8 @@ class ProjectListView(urwid.WidgetWrap, mywid.Searchable):
return key
def handleCommands(self, commands):
if keymap.TOGGLE_LIST_REVIEWED in commands:
self.unreviewed = not self.unreviewed
if keymap.TOGGLE_LIST_ACTIVE in commands:
self.active = not self.active
self.refresh()
return True
if keymap.TOGGLE_LIST_SUBSCRIBED in commands:

837
boartty/view/story.py Normal file
View File

@ -0,0 +1,837 @@
# Copyright 2014 OpenStack Foundation
# Copyright 2014 Hewlett-Packard Development Company, L.P.
# Copyright 2016 Red Hat, Inc
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import collections
import datetime
import logging
try:
import ordereddict
except:
pass
import textwrap
from six.moves.urllib import parse as urlparse
import urwid
from boartty import keymap
from boartty import mywid
from boartty import sync
from boartty.view import mouse_scroll_decorator
import boartty.view
try:
OrderedDict = collections.OrderedDict
except AttributeError:
OrderedDict = ordereddict.OrderedDict
class NewStoryDialog(urwid.WidgetWrap, mywid.LineBoxTitlePropertyMixin):
signals = ['save', 'cancel']
def __init__(self, app):
self.app = app
save_button = mywid.FixedButton(u'Save')
cancel_button = mywid.FixedButton(u'Cancel')
urwid.connect_signal(save_button, 'click',
lambda button:self._emit('save'))
urwid.connect_signal(cancel_button, 'click',
lambda button:self._emit('cancel'))
rows = []
buttons = [('pack', save_button),
('pack', cancel_button)]
buttons = urwid.Columns(buttons, dividechars=2)
self.project_button = ProjectButton(self.app)
self.title_field = mywid.MyEdit(u'', edit_text=u'', ring=app.ring)
self.description_field = mywid.MyEdit(u'', edit_text='',
multiline=True, ring=app.ring)
for (label, w) in [
(u'Title:', self.title_field),
(u'Description:', self.description_field),
(u'Project:', ('pack', self.project_button)),
]:
row = urwid.Columns([(12, urwid.Text(label)), w])
rows.append(row)
rows.append(urwid.Divider())
rows.append(buttons)
pile = urwid.Pile(rows)
fill = urwid.Filler(pile, valign='top')
super(NewStoryDialog, self).__init__(urwid.LineBox(fill, 'New Story'))
class ProjectButton(mywid.SearchSelectButton):
def __init__(self, app, key=None, value=None):
self.app = app
super(ProjectButton, self).__init__(app, 'Select Project', key, value,
self.getValues)
def getValues(self):
with self.app.db.getSession() as session:
projects = session.getProjects()
for project in projects:
yield (project.key, project.name)
class StatusButton(mywid.SearchSelectButton):
def __init__(self, app):
self.app = app
super(StatusButton, self).__init__(app, 'Select Status', 'todo', 'todo',
self.getValues)
def getValues(self):
return [('todo', 'todo'),
('merged', 'merged'),
('invalid', 'invalid'),
('review', 'review'),
('inprogress', 'inprogress'),
]
class AssigneeButton(mywid.SearchSelectButton):
def __init__(self, app):
self.app = app
super(AssigneeButton, self).__init__(app, 'Select Assignee', None, None,
self.getValues)
def getValues(self):
with self.app.db.getSession() as session:
users = session.getUsers()
for user in users:
yield (user.key, user.name)
class NewTaskDialog(urwid.WidgetWrap, mywid.LineBoxTitlePropertyMixin):
signals = ['save', 'cancel']
def __init__(self, app):
self.app = app
save_button = mywid.FixedButton(u'Save')
cancel_button = mywid.FixedButton(u'Cancel')
urwid.connect_signal(save_button, 'click',
lambda button:self._emit('save'))
urwid.connect_signal(cancel_button, 'click',
lambda button:self._emit('cancel'))
rows = []
buttons = [('pack', save_button),
('pack', cancel_button)]
buttons = urwid.Columns(buttons, dividechars=2)
self.project_button = ProjectButton(self.app)
self.status_button = StatusButton(self.app)
self.assignee_button = AssigneeButton(self.app)
self.title_field = mywid.MyEdit(u'', edit_text=u'', ring=app.ring)
for (label, w) in [
(u'Project:', ('pack', self.project_button)),
(u'Title:', self.title_field),
(u'Status:', ('pack', self.status_button)),
(u'Assignee:', ('pack', self.assignee_button)),
]:
row = urwid.Columns([(12, urwid.Text(label)), w])
rows.append(row)
rows.append(urwid.Divider())
rows.append(buttons)
pile = urwid.Pile(rows)
fill = urwid.Filler(pile, valign='top')
super(NewTaskDialog, self).__init__(urwid.LineBox(fill, 'New Task'))
class TaskRow(urwid.WidgetWrap):
task_focus_map = {
'task-title': 'focused-task-title',
'task-project': 'focused-task-project',
'task-status': 'focused-task-status',
'task-assignee': 'focused-task-assignee',
'task-note': 'focused-task-note',
}
def keypress(self, size, key):
if not self.app.input_buffer:
key = super(TaskRow, self).keypress(size, key)
keys = self.app.input_buffer + [key]
commands = self.app.config.keymap.getCommands(keys)
if keymap.DELETE_TASK in commands:
self.delete()
return None
return key
def __init__(self, app, story_view, task):
super(TaskRow, self).__init__(urwid.Pile([]))
self.app = app
self.story_view = story_view
self.task_key = task.key
self._note = u''
self.taskid = mywid.TextButton(self._note)
urwid.connect_signal(self.taskid, 'click',
lambda b:self.editNote(b))
self.project = ProjectButton(self.app)
urwid.connect_signal(self.project, 'changed',
lambda b:self.updateProject(b))
self.status = StatusButton(self.app)
urwid.connect_signal(self.status, 'changed',
lambda b:self.updateStatus(b))
self._title = u''
self.title = mywid.TextButton(self._title)
urwid.connect_signal(self.title, 'click',
lambda b:self.editTitle(b))
self.assignee = AssigneeButton(self.app)
urwid.connect_signal(self.assignee, 'changed',
lambda b:self.updateAssignee(b))
self.description = urwid.Text(u'')
self.columns = urwid.Columns([], dividechars=1)
for (widget, attr, packing) in [
(self.taskid, 'task-id', ('given', 4, False)),
(self.project, 'task-project', ('weight', 1, False)),
(self.title, 'task-title', ('weight', 2, False)),
(self.status, 'task-status', ('weight', 1, False)),
(self.assignee, 'task-assignee', ('weight', 1, False)),
]:
w = urwid.AttrMap(urwid.Padding(widget, width='pack'), attr,
focus_map={'focused': 'focused-'+attr})
self.columns.contents.append((w, packing))
self.pile = urwid.Pile([self.columns])
self.note = urwid.Text(u'')
self.note_visible = False
self.note_columns = urwid.Columns([], dividechars=1)
self.note_columns.contents.append((urwid.Text(u''), ('given', 1, False)))
self.note_columns.contents.append((self.note, ('weight', 1, False)))
self._w = urwid.AttrMap(self.pile, None)#, focus_map=self.task_focus_map)
self.refresh(task)
def setNote(self, note):
if note:
self._note = note
self.note.set_text(('task-note', self._note))
if not self.note_visible:
self.pile.contents.append((self.note_columns, ('weight', 1)))
self.note_visible = True
elif self.note_visible:
for x in self.pile.contents[:]:
if x[0] is self.note_columns:
self.pile.contents.remove(x)
self.note_visible = False
def refresh(self, task):
self.taskid.text.set_text(str(task.id))
self.project.update(task.project.key, task.project.name)
self.status.update(task.status, task.status)
self._title = task.title
self.title.text.set_text(self._title)
self.setNote(task.link)
if task.assignee:
self.assignee.update(task.assignee.key, task.assignee.name)
else:
self.assignee.update(None, 'Unassigned')
def updateProject(self, project_button):
with self.app.db.getSession() as session:
task = session.getTask(self.task_key)
project = session.getProject(project_button.key)
task.project = project
self.app.sync.submitTask(
sync.UpdateTaskTask(task.key, sync.HIGH_PRIORITY))
def updateStatus(self, status_button):
with self.app.db.getSession() as session:
task = session.getTask(self.task_key)
task.status = status_button.key
self.app.sync.submitTask(
sync.UpdateTaskTask(task.key, sync.HIGH_PRIORITY))
def updateAssignee(self, assignee_button):
with self.app.db.getSession() as session:
task = session.getTask(self.task_key)
user = session.getUser(assignee_button.key)
task.assignee = user
self.app.sync.submitTask(
sync.UpdateTaskTask(task.key, sync.HIGH_PRIORITY))
def editTitle(self, title_button):
dialog = mywid.LineEditDialog(self.app, 'Edit Task Title', '',
'Title: ', self._title,
ring=self.app.ring)
urwid.connect_signal(dialog, 'save',
lambda button: self.updateTitle(dialog, True))
urwid.connect_signal(dialog, 'cancel',
lambda button: self.updateTitle(dialog, False))
self.app.popup(dialog)
def updateTitle(self, dialog, save):
if save:
with self.app.db.getSession() as session:
task = session.getTask(self.task_key)
task.title = dialog.entry.edit_text
self._title = task.title
self.title.text.set_text(self._title)
self.app.sync.submitTask(
sync.UpdateTaskTask(task.key, sync.HIGH_PRIORITY))
self.app.backScreen()
def editNote(self, note_button):
dialog = mywid.LineEditDialog(self.app, 'Edit Task Note', '',
'Note: ', self._note,
ring=self.app.ring)
urwid.connect_signal(dialog, 'save',
lambda button: self.updateNote(dialog, True))
urwid.connect_signal(dialog, 'cancel',
lambda button: self.updateNote(dialog, False))
self.app.popup(dialog)
def updateNote(self, dialog, save):
if save:
with self.app.db.getSession() as session:
task = session.getTask(self.task_key)
task.link = dialog.entry.edit_text or None
self.setNote(task.link)
self.app.sync.submitTask(
sync.UpdateTaskTask(task.key, sync.HIGH_PRIORITY))
self.app.backScreen()
def delete(self):
dialog = mywid.YesNoDialog(u'Delete Task',
u'Are you sure you want to delete this task?')
urwid.connect_signal(dialog, 'no', lambda d: self.app.backScreen())
urwid.connect_signal(dialog, 'yes', self.finishDelete)
self.app.popup(dialog)
def finishDelete(self, dialog):
with self.app.db.getSession() as session:
task = session.getTask(self.task_key)
task.pending_delete = True
self.app.sync.submitTask(
sync.UpdateTaskTask(task.key, sync.HIGH_PRIORITY))
self.app.backScreen()
self.story_view.refresh()
class StoryButton(urwid.Button):
button_left = urwid.Text(u' ')
button_right = urwid.Text(u' ')
def __init__(self, story_view, story_key, text):
super(StoryButton, self).__init__('')
self.set_label(text)
self.story_view = story_view
self.story_key = story_key
urwid.connect_signal(self, 'click',
lambda button: self.openStory())
def set_label(self, text):
super(StoryButton, self).set_label(text)
def openStory(self):
try:
self.story_view.app.changeScreen(StoryView(self.story_view.app, self.story_key))
except boartty.view.DisplayError as e:
self.story_view.app.error(e.message)
class StoryEventBox(mywid.HyperText):
def __init__(self, story_view, event):
super(StoryEventBox, self).__init__(u'')
self.story_view = story_view
self.app = story_view.app
self.refresh(event)
def formatReply(self):
text = self.comment_text
pgraphs = []
pgraph_accumulator = []
wrap = True
for line in text.split('\n'):
if line.startswith('> '):
wrap = False
line = '> ' + line
if not line:
if pgraph_accumulator:
pgraphs.append((wrap, '\n'.join(pgraph_accumulator)))
pgraph_accumulator = []
wrap = True
continue
pgraph_accumulator.append(line)
if pgraph_accumulator:
pgraphs.append((wrap, '\n'.join(pgraph_accumulator)))
pgraph_accumulator = []
wrap = True
wrapper = textwrap.TextWrapper(initial_indent='> ',
subsequent_indent='> ')
wrapped_pgraphs = []
for wrap, pgraph in pgraphs:
if wrap:
wrapped_pgraphs.append('\n'.join(wrapper.wrap(pgraph)))
else:
wrapped_pgraphs.append(pgraph)
return '\n>\n'.join(wrapped_pgraphs)
def reply(self):
reply_text = self.formatReply()
if reply_text:
reply_text = self.event_creator + ' wrote:\n\n' + reply_text + '\n'
self.story_view.leaveComment(reply_text=reply_text)
def refresh(self, event):
self.event_id = event.id
self.event_creator = event.creator_name
description = event.description
if event.comment:
comment = event.comment.content
else:
comment = ''
self.comment_text = comment
created = self.app.time(event.created)
lines = comment.split('\n')
if event.creator.id == self.app.user_id:
name_style = 'story-event-own-name'
header_style = 'story-event-own-header'
creator_string = event.creator.name
else:
name_style = 'story-event-name'
header_style = 'story-event-header'
if event.creator.email:
creator_string = "%s <%s>" % (
event.creator.name,
event.creator.email)
else:
creator_string = event.creator.name
text = [(name_style, creator_string),
(header_style, ': '+description),
(header_style,
created.strftime(' (%Y-%m-%d %H:%M:%S%z)'))]
if event.comment and event.comment.draft and not event.comment.pending:
text.append(('story-event-draft', ' (draft)'))
elif event.comment:
link = mywid.Link('< Reply >',
'story-event-button',
'focused-story-event-button')
urwid.connect_signal(link, 'selected',
lambda link:self.reply())
text.append(' ')
text.append(link)
text.append('\n')
if lines and lines[-1]:
lines.append('')
comment_text = ['\n'.join(lines)]
for commentlink in self.app.config.commentlinks:
comment_text = commentlink.run(self.app, comment_text)
info = event.info or ''
if info:
info = [info + '\n']
else:
info = []
self.set_text(text+comment_text+info)
class DescriptionBox(mywid.HyperText):
def __init__(self, app, description):
self.app = app
super(DescriptionBox, self).__init__(description)
def set_text(self, text):
text = [text]
for commentlink in self.app.config.commentlinks:
text = commentlink.run(self.app, text)
super(DescriptionBox, self).set_text(text)
@mouse_scroll_decorator.ScrollByWheel
class StoryView(urwid.WidgetWrap):
def getCommands(self):
return [
(keymap.TOGGLE_HIDDEN,
"Toggle the hidden flag for the current story"),
(keymap.NEXT_STORY,
"Go to the next story in the list"),
(keymap.PREV_STORY,
"Go to the previous story in the list"),
(keymap.LEAVE_COMMENT,
"Leave a comment on the story"),
(keymap.NEW_TASK,
"Add a new task to the current story"),
(keymap.TOGGLE_HELD,
"Toggle the held flag for the current story"),
(keymap.TOGGLE_HIDDEN_COMMENTS,
"Toggle display of hidden comments"),
(keymap.SEARCH_RESULTS,
"Back to the list of stories"),
(keymap.TOGGLE_STARRED,
"Toggle the starred flag for the current story"),
(keymap.EDIT_DESCRIPTION,
"Edit the commit message of this story"),
(keymap.REFRESH,
"Refresh this story"),
(keymap.EDIT_TITLE,
"Edit the title of this story"),
]
def help(self):
key = self.app.config.keymap.formatKeys
commands = self.getCommands()
ret = [(c[0], key(c[0]), c[1]) for c in commands]
for k in self.app.config.reviewkeys.values():
action = ', '.join(['{category}:{value}'.format(**a) for a in k['approvals']])
ret.append(('', keymap.formatKey(k['key']), action))
return ret
def __init__(self, app, story_key):
super(StoryView, self).__init__(urwid.Pile([]))
self.log = logging.getLogger('boartty.view.story')
self.app = app
self.story_key = story_key
self.task_rows = {}
self.event_rows = {}
self.hide_events = True
self.marked_seen = False
self.title_label = urwid.Text(u'', wrap='clip')
self.creator_label = mywid.TextButton(u'', on_press=self.searchCreator)
self.tags_label = urwid.Text(u'', wrap='clip')
self.created_label = urwid.Text(u'', wrap='clip')
self.updated_label = urwid.Text(u'', wrap='clip')
self.status_label = urwid.Text(u'', wrap='clip')
self.permalink_label = mywid.TextButton(u'', on_press=self.openPermalink)
story_info = []
story_info_map={'story-data': 'focused-story-data'}
for l, v in [("Title", self.title_label),
("Creator", urwid.Padding(urwid.AttrMap(self.creator_label, None,
focus_map=story_info_map),
width='pack')),
("Tags", urwid.Padding(urwid.AttrMap(self.tags_label, None,
focus_map=story_info_map),
width='pack')),
("Created", self.created_label),
("Updated", self.updated_label),
("Status", self.status_label),
("Permalink", urwid.Padding(urwid.AttrMap(self.permalink_label, None,
focus_map=story_info_map),
width='pack')),
]:
row = urwid.Columns([(12, urwid.Text(('story-header', l), wrap='clip')), v])
story_info.append(row)
story_info = urwid.Pile(story_info)
self.description = DescriptionBox(app, u'')
self.listbox = urwid.ListBox(urwid.SimpleFocusListWalker([]))
self._w.contents.append((self.app.header, ('pack', 1)))
self._w.contents.append((urwid.Divider(), ('pack', 1)))
self._w.contents.append((self.listbox, ('weight', 1)))
self._w.set_focus(2)
self.listbox.body.append(story_info)
self.listbox.body.append(urwid.Divider())
self.listbox_tasks_start = len(self.listbox.body)
self.listbox.body.append(urwid.Divider())
self.listbox.body.append(self.description)
self.listbox.body.append(urwid.Divider())
self.refresh()
self.listbox.set_focus(3)
def interested(self, event):
if not ((isinstance(event, sync.StoryAddedEvent) and
self.story_key == event.story_key)
or
(isinstance(event, sync.StoryUpdatedEvent) and
self.story_key == event.story_key)):
self.log.debug("Ignoring refresh story due to event %s" % (event,))
return False
self.log.debug("Refreshing story due to event %s" % (event,))
return True
def refresh(self):
with self.app.db.getSession() as session:
story = session.getStory(self.story_key)
# When we first open the story, update its last_seen
# time.
if not self.marked_seen:
story.last_seen = datetime.datetime.utcnow()
self.marked_seen = True
hidden = starred = held = ''
# storyboard
#if story.hidden:
# hidden = ' (hidden)'
#if story.starred:
# starred = '* '
#if story.held:
# held = ' (held)'
self.title = '%sStory %s%s%s' % (starred, story.id,
hidden, held)
self.app.status.update(title=self.title)
self.story_rest_id = story.id
self.story_title = story.title
if story.creator:
self.creator_email = story.creator.email
else:
self.creator_email = None
if self.creator_email:
creator_string = '%s <%s>' % (story.creator_name,
story.creator.email)
else:
creator_string = story.creator_name
self.creator_label.text.set_text(('story-data', creator_string))
tags_string = ' '.join([t.name for t in story.tags])
self.tags_label.set_text(('story-data', tags_string))
self.title_label.set_text(('story-data', story.title))
self.created_label.set_text(('story-data', str(self.app.time(story.created))))
self.updated_label.set_text(('story-data', str(self.app.time(story.updated))))
self.status_label.set_text(('story-data', story.status))
self.permalink_url = '' # storyboard urlparse.urljoin(self.app.config.url, str(story.number))
self.permalink_label.text.set_text(('story-data', self.permalink_url))
self.description.set_text(story.description)
# The listbox has both tasks and events in it, so
# keep track of the index separate from the loop.
listbox_index = self.listbox_tasks_start
# The set of task keys currently displayed
unseen_keys = set(self.task_rows.keys())
for task in story.tasks:
if task.pending_delete:
continue
row = self.task_rows.get(task.key)
if not row:
row = TaskRow(self.app, self, task)
self.listbox.body.insert(listbox_index, row)
self.task_rows[task.key] = row
else:
unseen_keys.remove(task.key)
row.refresh(task)
listbox_index += 1
# Remove any events that should not be displayed
for key in unseen_keys:
row = self.task_rows.get(key)
self.listbox.body.remove(row)
del self.task_rows[key]
listbox_index -= 1
listbox_index = len(self.listbox.body)
# Get the set of events that should be displayed
display_events = []
for event in story.events:
if event.comment or (not self.hide_events):
display_events.append(event)
# The set of event keys currently displayed
unseen_keys = set(self.event_rows.keys())
# Make sure all of the events that should be displayed are
for event in display_events:
row = self.event_rows.get(event.key)
if not row:
box = StoryEventBox(self, event)
row = urwid.Padding(box, width=80)
self.listbox.body.insert(listbox_index, row)
self.event_rows[event.key] = row
else:
unseen_keys.remove(event.key)
row.original_widget.refresh(event)
listbox_index += 1
# Remove any events that should not be displayed
for key in unseen_keys:
row = self.event_rows.get(key)
self.listbox.body.remove(row)
del self.event_rows[key]
listbox_index -= 1
def toggleHidden(self):
with self.app.db.getSession() as session:
story = session.getStory(self.story_key)
story.hidden = not story.hidden
self.app.project_cache.clear(story.project)
def toggleStarred(self):
with self.app.db.getSession() as session:
story = session.getStory(self.story_key)
story.starred = not story.starred
story.pending_starred = True
self.app.sync.submitTask(
sync.StoryStarredTask(self.story_key, sync.HIGH_PRIORITY))
def toggleHeld(self):
return self.app.toggleHeldStory(self.story_key)
def keypress(self, size, key):
if not self.app.input_buffer:
key = super(StoryView, self).keypress(size, key)
keys = self.app.input_buffer + [key]
commands = self.app.config.keymap.getCommands(keys)
if keymap.TOGGLE_HIDDEN in commands:
self.toggleHidden()
self.refresh()
return None
if keymap.TOGGLE_STARRED in commands:
self.toggleStarred()
self.refresh()
return None
if keymap.TOGGLE_HELD in commands:
self.toggleHeld()
self.refresh()
return None
if keymap.LEAVE_COMMENT in commands:
self.leaveComment()
return None
if keymap.NEW_TASK in commands:
self.newTask()
return None
if keymap.SEARCH_RESULTS in commands:
widget = self.app.findStoryList()
if widget:
self.app.backScreen(widget)
return None
if ((keymap.NEXT_STORY in commands) or
(keymap.PREV_STORY in commands)):
widget = self.app.findStoryList()
if widget:
if keymap.NEXT_STORY in commands:
new_story_key = widget.getNextStoryKey(self.story_key)
else:
new_story_key = widget.getPrevStoryKey(self.story_key)
if new_story_key:
try:
view = StoryView(self.app, new_story_key)
self.app.changeScreen(view, push=False)
except boartty.view.DisplayError as e:
self.app.error(e.message)
return None
if keymap.TOGGLE_HIDDEN_COMMENTS in commands:
self.hide_events = not self.hide_events
self.refresh()
return None
if keymap.EDIT_DESCRIPTION in commands:
self.editDescription()
return None
if keymap.REFRESH in commands:
self.app.sync.submitTask(
sync.SyncStoryTask(self.story_rest_id, priority=sync.HIGH_PRIORITY))
self.app.status.update()
return None
if keymap.EDIT_TITLE in commands:
self.editTitle()
return None
return key
def editDescription(self):
with self.app.db.getSession() as session:
story = session.getStory(self.story_key)
dialog = mywid.TextEditDialog(self.app, u'Edit Description',
u'Description:',
u'Save', story.description)
urwid.connect_signal(dialog, 'cancel', self.app.backScreen)
urwid.connect_signal(dialog, 'save', lambda button:
self.doEditDescription(dialog))
self.app.popup(dialog,
relative_width=50, relative_height=75,
min_width=60, min_height=20)
def doEditDescription(self, dialog):
with self.app.db.getSession() as session:
story = session.getStory(self.story_key)
story.description = dialog.entry.edit_text
story.pending = True
self.app.sync.submitTask(
sync.UpdateStoryTask(self.story_key, sync.HIGH_PRIORITY))
self.app.backScreen()
self.refresh()
def leaveComment(self, parent=None, reply_text=None):
with self.app.db.getSession() as session:
story = session.getStory(self.story_key)
event = story.getDraftCommentEvent(parent)
if event:
text = event.comment.content
else:
text = u''
if reply_text:
text += reply_text
dialog = mywid.TextEditDialog(self.app, u'Leave Comment', u'Comment:',
u'Save', text)
urwid.connect_signal(dialog, 'cancel', lambda button:
self.cancelLeaveComment(dialog, parent))
urwid.connect_signal(dialog, 'save', lambda button:
self.saveLeaveComment(dialog, parent))
self.app.popup(dialog,
relative_width=50, relative_height=75,
min_width=60, min_height=20)
def cancelLeaveComment(self, dialog, parent):
with self.app.db.getSession() as session:
story = session.getStory(self.story_key)
user = session.getUser(self.app.user_id)
story.setDraftComment(user, parent, dialog.entry.edit_text)
self.app.backScreen()
self.refresh()
def saveLeaveComment(self, dialog, parent):
with self.app.db.getSession() as session:
story = session.getStory(self.story_key)
user = session.getUser(self.app.user_id)
event = story.setDraftComment(user, parent, dialog.entry.edit_text)
event.comment.pending = True
self.app.sync.submitTask(
sync.AddCommentTask(event.key, sync.HIGH_PRIORITY))
self.app.backScreen()
self.refresh()
def newTask(self):
dialog = NewTaskDialog(self.app)
urwid.connect_signal(dialog, 'save',
lambda button: self.saveNewTask(dialog))
urwid.connect_signal(dialog, 'cancel',
lambda button: self.cancelNewTask(dialog))
self.app.popup(dialog,
relative_width=50, relative_height=25,
min_width=60, min_height=8)
def cancelNewTask(self, dialog):
self.app.backScreen()
def saveNewTask(self, dialog):
with self.app.db.getSession() as session:
story = session.getStory(self.story_key)
task = story.addTask()
task.project = session.getProjectByID(dialog.project_button.key)
task.title = dialog.title_field.edit_text
task.status = dialog.status_button.key
if dialog.assignee_button.key:
task.assignee = session.getUserByID(dialog.assignee_button.key)
task.pending = True
self.app.sync.submitTask(
sync.UpdateTaskTask(task.key, sync.HIGH_PRIORITY))
self.app.backScreen()
self.refresh()
def editTitle(self):
dialog = mywid.LineEditDialog(self.app, 'Edit Story Title', '',
'Title: ', self.story_title,
ring=self.app.ring)
urwid.connect_signal(dialog, 'save',
lambda button: self.updateTitle(dialog, True))
urwid.connect_signal(dialog, 'cancel',
lambda button: self.updateTitle(dialog, False))
self.app.popup(dialog)
def updateTitle(self, dialog, save):
if save:
with self.app.db.getSession() as session:
story = session.getStory(self.story_key)
story.title = dialog.entry.edit_text
self.app.sync.submitTask(
sync.UpdateStoryTask(story.key, sync.HIGH_PRIORITY))
self.app.backScreen()
self.refresh()
def openPermalink(self, widget):
self.app.openURL(self.permalink_url)
def searchCreator(self, widget):
if self.creator_email:
self.app.doSearch("status:open creator:%s" % (self.creator_email,))
def searchTags(self, widget):
#storyboard
if self.topic:
self.app.doSearch("status:open topic:%s" % (self.topic,))

513
boartty/view/story_list.py Normal file
View File

@ -0,0 +1,513 @@
# Copyright 2014 OpenStack Foundation
# Copyright 2014 Hewlett-Packard Development Company, L.P.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import datetime
import logging
import six
import urwid
from boartty import keymap
from boartty import mywid
from boartty import sync
from boartty.view import story as view_story
from boartty.view import mouse_scroll_decorator
import boartty.view
class ColumnInfo(object):
def __init__(self, name, packing, value):
self.name = name
self.packing = packing
self.value = value
self.options = (packing, value)
if packing == 'given':
self.spacing = value + 1
else:
self.spacing = (value * 8) + 1
COLUMNS = [
ColumnInfo('ID', 'given', 8),
ColumnInfo('Title', 'weight', 4),
ColumnInfo('Status', 'weight', 1),
ColumnInfo('Creator', 'weight', 1),
ColumnInfo('Updated', 'given', 10),
]
class StoryListColumns(object):
def updateColumns(self):
del self.columns.contents[:]
cols = self.columns.contents
options = self.columns.options
for colinfo in COLUMNS:
if colinfo.name in self.enabled_columns:
attr = colinfo.name.lower().replace(' ', '_')
cols.append((getattr(self, attr),
options(*colinfo.options)))
class StoryRow(urwid.Button, StoryListColumns):
story_focus_map = {None: 'focused',
'active-story': 'focused-active-story',
'inactive-story': 'focused-inactive-story',
'starred-story': 'focused-starred-story',
'held-story': 'focused-held-story',
'marked-story': 'focused-marked-story',
}
def selectable(self):
return True
def __init__(self, app, story,
enabled_columns, callback=None):
super(StoryRow, self).__init__('', on_press=callback, user_data=story.key)
self.app = app
self.story_key = story.key
self.enabled_columns = enabled_columns
self.title = mywid.SearchableText(u'', wrap='clip')
self.id = mywid.SearchableText(u'')
self.updated = mywid.SearchableText(u'')
self.status = mywid.SearchableText(u'')
self.creator = mywid.SearchableText(u'', wrap='clip')
self.mark = False
self.columns = urwid.Columns([], dividechars=1)
self.row_style = urwid.AttrMap(self.columns, '')
self._w = urwid.AttrMap(self.row_style, None, focus_map=self.story_focus_map)
self.update(story)
def search(self, search, attribute):
if self.title.search(search, attribute):
return True
if self.id.search(search, attribute):
return True
if self.creator.search(search, attribute):
return True
if self.status.search(search, attribute):
return True
if self.updated.search(search, attribute):
return True
return False
def update(self, story):
if story.status != 'active' or story.hidden:
style = 'inactive-story'
else:
style = 'active-story'
title = story.title
flag = ' '
#if story.starred:
# flag = '*'
# style = 'starred-story'
#if story.held:
# flag = '!'
# style = 'held-story'
if self.mark:
flag = '%'
style = 'marked-story'
title = flag + title
self.row_style.set_attr_map({None: style})
self.title.set_text(title)
self.id.set_text(str(story.id))
self.creator.set_text(story.creator_name)
self.status.set_text(story.status)
today = self.app.time(datetime.datetime.utcnow()).date()
updated_time = self.app.time(story.updated)
if updated_time:
if today == updated_time.date():
self.updated.set_text(updated_time.strftime("%I:%M %p").upper())
else:
self.updated.set_text(updated_time.strftime("%Y-%m-%d"))
else:
self.updated.set_text('Unknown')
self.updateColumns()
class StoryListHeader(urwid.WidgetWrap, StoryListColumns):
def __init__(self, enabled_columns):
self.enabled_columns = enabled_columns
self.title = urwid.Text(u'Title', wrap='clip')
self.id = urwid.Text(u'ID')
self.updated = urwid.Text(u'Updated')
self.status = urwid.Text(u'Status')
self.creator = urwid.Text(u'Creator', wrap='clip')
self.columns = urwid.Columns([], dividechars=1)
super(StoryListHeader, self).__init__(self.columns)
def update(self):
self.updateColumns()
@mouse_scroll_decorator.ScrollByWheel
class StoryListView(urwid.WidgetWrap, mywid.Searchable):
required_columns = set(['ID', 'Title', 'Updated'])
optional_columns = set([])
def getCommands(self):
if self.project_key:
refresh_help = "Sync current project"
else:
refresh_help = "Sync subscribed projects"
return [
(keymap.TOGGLE_HELD,
"Toggle the held flag for the currently selected story"),
(keymap.TOGGLE_HIDDEN,
"Toggle the hidden flag for the currently selected story"),
(keymap.TOGGLE_LIST_ACTIVE,
"Toggle whether only active or all changes are displayed"),
(keymap.TOGGLE_STARRED,
"Toggle the starred flag for the currently selected story"),
(keymap.TOGGLE_MARK,
"Toggle the process mark for the currently selected story"),
(keymap.REFINE_STORY_SEARCH,
"Refine the current search query"),
(keymap.REFRESH,
refresh_help),
(keymap.SORT_BY_NUMBER,
"Sort stories by number"),
(keymap.SORT_BY_UPDATED,
"Sort stories by how recently the story was updated"),
(keymap.SORT_BY_REVERSE,
"Reverse the sort"),
(keymap.INTERACTIVE_SEARCH,
"Interactive search"),
]
def help(self):
key = self.app.config.keymap.formatKeys
commands = self.getCommands()
return [(c[0], key(c[0]), c[1]) for c in commands]
def __init__(self, app, query, query_desc=None, project_key=None,
active=False, sort_by=None, reverse=None):
super(StoryListView, self).__init__(urwid.Pile([]))
self.log = logging.getLogger('boartty.view.story_list')
self.searchInit()
self.app = app
self.query = query
self.query_desc = query_desc or query
self.active = active
self.story_rows = {}
self.enabled_columns = set()
for colinfo in COLUMNS:
if (colinfo.name in self.required_columns or
colinfo.name not in self.optional_columns):
self.enabled_columns.add(colinfo.name)
self.disabled_columns = set()
self.listbox = urwid.ListBox(urwid.SimpleFocusListWalker([]))
self.project_key = project_key
if 'Project' not in self.required_columns and project_key is not None:
self.enabled_columns.discard('Project')
self.disabled_columns.add('Project')
#storyboard: creator
if 'Owner' not in self.required_columns and 'owner:' in query:
# This could be or'd with something else, but probably
# not.
self.enabled_columns.discard('Owner')
self.disabled_columns.add('Owner')
self.sort_by = sort_by or app.config.story_list_options['sort-by']
if reverse is not None:
self.reverse = reverse
else:
self.reverse = app.config.story_list_options['reverse']
self.header = StoryListHeader(self.enabled_columns)
self.refresh()
self._w.contents.append((app.header, ('pack', 1)))
self._w.contents.append((urwid.Divider(), ('pack', 1)))
self._w.contents.append((urwid.AttrWrap(self.header, 'table-header'), ('pack', 1)))
self._w.contents.append((self.listbox, ('weight', 1)))
self._w.set_focus(3)
def interested(self, event):
if not ((self.project_key is not None and
isinstance(event, sync.StoryAddedEvent) and
self.project_key in event.related_project_keys)
or
(self.project_key is None and
isinstance(event, sync.StoryAddedEvent))
or
(isinstance(event, sync.StoryUpdatedEvent) and
event.story_key in self.story_rows.keys())):
self.log.debug("Ignoring refresh story list due to event %s" % (event,))
return False
self.log.debug("Refreshing story list due to event %s" % (event,))
return True
def refresh(self):
unseen_keys = set(self.story_rows.keys())
with self.app.db.getSession() as session:
story_list = session.getStories(self.query, self.active,
sort_by=self.sort_by)
if self.active:
self.title = (u'Active %d stories in %s' %
(len(story_list), self.query_desc))
else:
self.title = (u'All %d stories in %s' %
(len(story_list), self.query_desc))
self.short_title = self.query_desc
if '/' in self.short_title and ' ' not in self.short_title:
i = self.short_title.rfind('/')
self.short_title = self.short_title[i+1:]
self.app.status.update(title=self.title)
if 'Status' not in self.required_columns and self.active:
self.enabled_columns.discard('Status')
self.disabled_columns.add('Status')
else:
self.enabled_columns.add('Status')
self.disabled_columns.discard('Status')
self.chooseColumns()
self.header.update()
i = 0
if self.reverse:
story_list.reverse()
new_rows = []
if len(self.listbox.body):
focus_pos = self.listbox.focus_position
focus_row = self.listbox.body[focus_pos]
else:
focus_pos = 0
focus_row = None
for story in story_list:
row = self.story_rows.get(story.key)
if not row:
row = StoryRow(self.app, story,
self.enabled_columns,
callback=self.onSelect)
self.listbox.body.insert(i, row)
self.story_rows[story.key] = row
else:
row.update(story)
unseen_keys.remove(story.key)
new_rows.append(row)
i += 1
self.listbox.body[:] = new_rows
if focus_row in self.listbox.body:
pos = self.listbox.body.index(focus_row)
else:
pos = min(focus_pos, len(self.listbox.body)-1)
self.listbox.body.set_focus(pos)
for key in unseen_keys:
row = self.story_rows[key]
del self.story_rows[key]
def chooseColumns(self):
currently_enabled_columns = self.enabled_columns.copy()
size = self.app.loop.screen.get_cols_rows()
cols = size[0]
for colinfo in COLUMNS:
if (colinfo.name not in self.disabled_columns):
cols -= colinfo.spacing
for colinfo in COLUMNS:
if colinfo.name in self.optional_columns:
if cols >= colinfo.spacing:
self.enabled_columns.add(colinfo.name)
cols -= colinfo.spacing
else:
self.enabled_columns.discard(colinfo.name)
if currently_enabled_columns != self.enabled_columns:
self.header.updateColumns()
for key, value in six.iteritems(self.story_rows):
value.updateColumns()
def getQueryString(self):
if self.project_key is not None:
return "project:%s %s" % (self.query_desc, self.app.config.project_story_list_query)
return self.app.config.project_story_list_query
return self.query
def clearStoryList(self):
for key, value in six.iteritems(self.story_rows):
self.listbox.body.remove(value)
self.story_rows = {}
def getNextStoryKey(self, story_key):
row = self.story_rows.get(story_key)
try:
i = self.listbox.body.index(row)
except ValueError:
return None
if i+1 >= len(self.listbox.body):
return None
row = self.listbox.body[i+1]
return row.story_key
def getPrevStoryKey(self, story_key):
row = self.story_rows.get(story_key)
try:
i = self.listbox.body.index(row)
except ValueError:
return None
if i <= 0:
return None
row = self.listbox.body[i-1]
return row.story_key
def toggleStarred(self, story_key):
with self.app.db.getSession() as session:
story = session.getStory(story_key)
story.starred = not story.starred
ret = story.starred
story.pending_starred = True
self.app.sync.submitTask(
sync.StoryStarredTask(story_key, sync.HIGH_PRIORITY))
return ret
def toggleHeld(self, story_key):
return self.app.toggleHeldStory(story_key)
def toggleHidden(self, story_key):
with self.app.db.getSession() as session:
story = session.getStory(story_key)
story.hidden = not story.hidden
ret = story.hidden
hidden_str = 'hidden' if story.hidden else 'visible'
self.log.debug("Set story %s to %s", story_key, hidden_str)
return ret
def advance(self):
pos = self.listbox.focus_position
if pos < len(self.listbox.body)-1:
pos += 1
self.listbox.focus_position = pos
def keypress(self, size, key):
if self.searchKeypress(size, key):
return None
if not self.app.input_buffer:
key = super(StoryListView, self).keypress(size, key)
keys = self.app.input_buffer + [key]
commands = self.app.config.keymap.getCommands(keys)
ret = self.handleCommands(commands)
if ret is True:
if keymap.FURTHER_INPUT not in commands:
self.app.clearInputBuffer()
return None
return key
def onResize(self):
self.chooseColumns()
def handleCommands(self, commands):
if keymap.TOGGLE_LIST_ACTIVE in commands:
self.active = not self.active
self.refresh()
return True
if keymap.TOGGLE_HIDDEN in commands:
if not len(self.listbox.body):
return True
pos = self.listbox.focus_position
story_key = self.listbox.body[pos].story_key
hidden = self.toggleHidden(story_key)
if hidden:
# Here we can avoid a full refresh by just removing the particular
# row from the story list
row = self.story_rows[story_key]
self.listbox.body.remove(row)
del self.story_rows[story_key]
else:
# Just fall back on doing a full refresh if we're in a situation
# where we're not just popping a row from the list of stories.
self.refresh()
self.advance()
return True
if keymap.TOGGLE_HELD in commands:
if not len(self.listbox.body):
return True
pos = self.listbox.focus_position
story_key = self.listbox.body[pos].story_key
self.toggleHeld(story_key)
row = self.story_rows[story_key]
with self.app.db.getSession() as session:
story = session.getStory(story_key)
row.update(story)
self.advance()
return True
if keymap.TOGGLE_STARRED in commands:
if not len(self.listbox.body):
return True
pos = self.listbox.focus_position
story_key = self.listbox.body[pos].story_key
self.toggleStarred(story_key)
row = self.story_rows[story_key]
with self.app.db.getSession() as session:
story = session.getStory(story_key)
row.update(story)
self.advance()
return True
if keymap.TOGGLE_MARK in commands:
if not len(self.listbox.body):
return True
pos = self.listbox.focus_position
story_key = self.listbox.body[pos].story_key
row = self.story_rows[story_key]
row.mark = not row.mark
with self.app.db.getSession() as session:
story = session.getStory(story_key)
row.update(story)
self.advance()
return True
if keymap.REFRESH in commands:
if self.project_key:
self.app.sync.submitTask(
sync.SyncProjectTask(self.project_key, sync.HIGH_PRIORITY))
else:
self.app.sync.submitTask(
sync.SyncSubscribedProjectsTask(sync.HIGH_PRIORITY))
self.app.status.update()
return True
if keymap.SORT_BY_NUMBER in commands:
if not len(self.listbox.body):
return True
self.sort_by = 'number'
self.clearStoryList()
self.refresh()
return True
if keymap.SORT_BY_UPDATED in commands:
if not len(self.listbox.body):
return True
self.sort_by = 'updated'
self.clearStoryList()
self.refresh()
return True
if keymap.SORT_BY_REVERSE in commands:
if not len(self.listbox.body):
return True
if self.reverse:
self.reverse = False
else:
self.reverse = True
self.clearStoryList()
self.refresh()
return True
if keymap.REFINE_STORY_SEARCH in commands:
default = self.getQueryString()
self.app.searchDialog(default)
return True
if keymap.INTERACTIVE_SEARCH in commands:
self.searchStart()
return True
return False
def onSelect(self, button, story_key):
try:
view = view_story.StoryView(self.app, story_key)
self.app.changeScreen(view)
except boartty.view.DisplayError as e:
self.app.error(str(e))

View File

@ -85,17 +85,17 @@ qthelp:
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Gertty.qhcp"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Boartty.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Gertty.qhc"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Boartty.qhc"
devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@echo "To view the help file:"
@echo "# mkdir -p $$HOME/.local/share/devhelp/Gertty"
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Gertty"
@echo "# mkdir -p $$HOME/.local/share/devhelp/Boartty"
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Boartty"
@echo "# devhelp"
epub:

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
#
# Gertty documentation build configuration file, created by
# Boartty documentation build configuration file, created by
# sphinx-quickstart on Fri Jan 15 13:41:54 2016.
#
# This file is execfile()d with the current directory set to its
@ -45,17 +45,17 @@ source_suffix = '.rst'
master_doc = 'index'
# General information about the project.
project = u'Gertty'
copyright = u'%s, Gertty Contributors' % datetime.date.today().year
project = u'Boartty'
copyright = u'%s, Boartty Contributors' % datetime.date.today().year
# 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 full version, including alpha/beta/rc tags.
from gertty.version import version_info as gertty_version
release = gertty_version.version_string_with_vcs()
from boartty.version import version_info as boartty_version
release = boartty_version.version_string_with_vcs()
# The short X.Y version.
version = gertty_version.canonical_version_string()
version = boartty_version.canonical_version_string()
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
@ -178,7 +178,7 @@ html_static_path = ['_static']
#html_file_suffix = None
# Output file base name for HTML help builder.
htmlhelp_basename = 'Gerttydoc'
htmlhelp_basename = 'Boarttydoc'
# -- Options for LaTeX output ---------------------------------------------
@ -198,7 +198,7 @@ latex_elements = {
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
('index', 'Gertty.tex', u'Gertty Documentation',
('index', 'Boartty.tex', u'Boartty Documentation',
u'James E. Blair', 'manual'),
]
@ -228,7 +228,7 @@ latex_documents = [
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
('index', 'gertty', u'Gertty Documentation',
('index', 'boartty', u'Boartty Documentation',
[u'James E. Blair'], 1)
]
@ -242,8 +242,8 @@ man_pages = [
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
('index', 'Gertty', u'Gertty Documentation',
u'James E. Blair', 'Gertty', 'One line description of project.',
('index', 'Boartty', u'Boartty Documentation',
u'James E. Blair', 'Boartty', 'One line description of project.',
'Miscellaneous'),
]

View File

@ -1,44 +1,36 @@
Configuration
-------------
Gertty uses a YAML based configuration file that it looks for at
``~/.gertty.yaml``. Several sample configuration files are included.
Boartty uses a YAML based configuration file that it looks for at
``~/.boartty.yaml``. Several sample configuration files are included.
You can find them in the examples/ directory of the
`source distribution <https://git.openstack.org/cgit/openstack/gertty/tree/examples>`_
or the share/gertty/examples directory after installation.
`source distribution <https://git.openstack.org/cgit/openstack/boartty/tree/examples>`_
or the share/boartty/examples directory after installation.
Select one of the sample config files, copy it to ~/.gertty.yaml and
Select one of the sample config files, copy it to ~/.boartty.yaml and
edit as necessary. Search for ``CHANGEME`` to find parameters that
need to be supplied. The sample config files are as follows:
**minimal-gertty.yaml**
Only contains the parameters required for Gertty to actually run.
**minimal-boartty.yaml**
Only contains the parameters required for Boartty to actually run.
**reference-gertty.yaml**
**reference-boartty.yaml**
An exhaustive list of all supported options with examples.
**openstack-gertty.yaml**
**openstack-boartty.yaml**
A configuration designed for use with OpenStack's installation of
Gerrit.
**googlesource-gertty.yaml**
A configuration designed for use with installations of Gerrit
running on googlesource.com.
You will need a Storyboard authentication token which you can generate
or retrieve by navigating to ``Profile``, then ``Tokens`` (the "key"
icon), or visiting the `/#!/profile/tokens` URI in your Storyboard
installation. Issue a new token if you have not done so before, and
give it a sufficiently long lifetime (for example, one decade). Copy
and paste the resulting token in your ``~/.boartty.yaml`` file.
You will need your Gerrit password which you can generate or retrieve
by navigating to ``Settings``, then ``HTTP Password``.
Gertty uses local git repositories to perform much of its work. These
can be the same git repositories that you use when developing a
project. Gertty will not alter the working directory or index unless
you request it to (and even then, the usual git safeguards against
accidentally losing work remain in place). You will need to supply
the name of a directory where Gertty will find or clone git
repositories for your projects as the ``git-root`` parameter.
The config file is designed to support multiple Gerrit instances. The
first one is used by default, but others can be specified by supplying
the name on the command line.
The config file is designed to support multiple Storyboard instances.
The first one is used by default, but others can be specified by
supplying the name on the command line.
Configuration Reference
~~~~~~~~~~~~~~~~~~~~~~~
@ -49,8 +41,8 @@ configuration file.
Servers
+++++++
This section lists the servers that Gertty can talk to. Multiple
servers may be listed; by default, Gertty will use the first one
This section lists the servers that Boartty can talk to. Multiple
servers may be listed; by default, Boartty will use the first one
listed. To select another, simply specify its name on the command
line.
@ -65,30 +57,17 @@ line.
**url (required)**
The URL of the Gerrit server. HTTPS should be preferred.
**username (required)**
Your username in Gerrit. [required]
**password (required)**
Your password in Gerrit. Obtain it from Settings -> HTTP Password
in the Gerrit web interface.
**auth-type**
Authentication type required by the Gerrit server. Can be 'basic',
'digest', or 'form'. Defaults to 'digest'.
**git-root (required)**
A location where Gertty should store its git repositories. These
can be the same git repositories where you do your own work --
Gertty will not modify them unless you tell it to, and even then
the normal git protections against losing work remain in place.
**token (required)**
Your authentication token from Storyboard. Obtain it as described
above in "Configuration".
**dburi**
The location of Gertty's sqlite database. If you have more than
The location of Boartty's sqlite database. If you have more than
one server, you should specify a dburi for any additional servers.
By default a SQLite database called ~/.gertty.db is used.
By default a SQLite database called ~/.boartty.db is used.
**ssl-ca-path**
If your Gerrit server uses a non-standard certificate chain
If your Storyboard server uses a non-standard certificate chain
(e.g. on a test server), you can pass a full path to a bundle of
CA certificates here:
@ -98,18 +77,18 @@ line.
turn off certificate validation.
**log-file**
By default Gertty logs errors to a file and truncates that file
By default Boartty logs errors to a file and truncates that file
each time it starts (so that it does not grow without bound). If
you would like to log to a different location, you may specify it
with this option.
**socket**
Gertty listens on a unix domain socket for remote commands at
~/.gertty.sock. This option may be used to change the path.
Boartty listens on a unix domain socket for remote commands at
~/.boartty.sock. This option may be used to change the path.
**lock-file**
Gertty uses a lock file per server to prevent multiple processes
from running at the same time. The default is ~/.gertty.servername.lock
Boartty uses a lock file per server to prevent multiple processes
from running at the same time. The default is ~/.boartty.servername.lock
Example:
@ -117,14 +96,12 @@ Example:
servers:
- name: CHANGEME
url: https://CHANGEME.example.org/
username: CHANGEME
password: CHANGEME
git-root: ~/git/
token: CHANGEME
Palettes
++++++++
Gertty comes with two palettes defined internally. The default
Boartty comes with two palettes defined internally. The default
palette is suitable for use on a terminal with a dark background. The
`light` palette is for a terminal with a white or light background.
You may customize the colors in either of those palettes, or define
@ -140,7 +117,7 @@ high-color terminals.
For a reference of possible color names, see the `Urwid Manual
<http://urwid.org/manual/displayattributes.html#foreground-and-background-settings>`_
To see the list of possible palette entries, run `gertty --print-palette`.
To see the list of possible palette entries, run `boartty --print-palette`.
The following example alters two colors in the default palette, one
color in the light palette, and one color in a custom palette.
@ -148,12 +125,12 @@ color in the light palette, and one color in a custom palette.
.. code-block: yaml
palettes:
- name: default
added-line: ['dark green', '']
added-word: ['light green', '']
task-title: ['light green', '']
task-id: ['dark cyan', '']
- name: light
filename: ['dark cyan', '']
task-project: ['dark blue', '']
- name: custom
filename: ['light yellow', '']
task-project: ['dark red', '']
Palettes may be selected at runtime with the `-p PALETTE` command
line option, or you may set the default palette in the config file.
@ -170,21 +147,21 @@ may be overridden and custom keymaps defined and selected in the
config file or the command line.
Each keymap contains a mapping of command -> key(s). If a command is
not specified, Gertty will use the keybinding specified in the default
not specified, Boartty will use the keybinding specified in the default
map. More than one key can be bound to a command.
Run `gertty --print-keymap` for a list of commands that can be bound.
Run `boartty --print-keymap` for a list of commands that can be bound.
The following example modifies the `default` keymap:
.. code-block: yaml
keymaps:
- name: default
diff: 'd'
leave-comment: 'r'
- name: custom
review: ['r', 'R']
- name: osx #OS X blocks ctrl+o
change-search: 'ctrl s'
leave-comment: ['r', 'R']
- name: osx # OS X blocks ctrl+o
story-search: 'ctrl s'
To specify a sequence of keys, they must be a list of keystrokes
@ -204,9 +181,9 @@ option, or in the config file.
Commentlinks
++++++++++++
Commentlinks are regular expressions that are applied to commit and
review messages. They can be replaced with internal or external
links, or have colors applied.
Commentlinks are regular expressions that are applied to story
descriptions and comments. They can be replaced with internal or
external links, or have colors applied.
**commentlinks**
This is a list of commentlink patterns. Each commentlink pattern is
@ -247,7 +224,7 @@ links, or have colors applied.
palette entry.
**search**
A hyperlink that will perform a Gertty search when activated.
A hyperlink that will perform a Boartty search when activated.
**text**
The replacement text.
@ -255,31 +232,31 @@ links, or have colors applied.
**query**
The search query to use.
This example matches Gerrit change ids, and replaces them with a link
to an internal Gertty search for that change id.
This example matches story numbers, and replaces them with a link to
an internal Boartty search for that story.
.. code-block: yaml
commentlinks:
- match: "(?P<id>I[0-9a-fA-F]{40})"
- match: "(?P<id>[0-9]+)"
replacements:
- search:
text: "{id}"
query: "change:{id}"
query: "story:{id}"
Change List Options
+++++++++++++++++++
Story List Options
++++++++++++++++++
**change-list-query**
This is the query used for the list of changes when a project is
selected. The default is `status:open`.
**story-list-query**
This is the query used for the list of storyies when a project is
selected. The default is empty.
**change-list-options**
This section defines default sorting options for the change list.
**story-list-options**
This section defines default sorting options for the story list.
**sort-by**
This key specifies the sort order, which can be `number` (the
Change number), `updated` (when the change was last updated), or
`last-seen` (when the change was last opened in Gertty).
Story number), `updated` (when the story was last updated), or
`last-seen` (when the story was last opened in Boartty).
**reverse**
This is a boolean value which indicates whether the list should be
@ -288,45 +265,16 @@ Change List Options
Example:
.. code-block: yaml
change-list-options:
story-list-options:
sort-by: 'number'
reverse: false
**thread-changes**
Dependent changes are displayed as "threads" in the change list by
default. To disable this behavior, set this value to false.
Change View Options
+++++++++++++++++++
**hide-comments**
This is a list of descriptors which cause matching comments to be
hidden by default. Press the `t` key to toggle the display of
matching comments.
The only supported criterion is `author`.
**author**
A regular expression to match against the comment author's name.
For example, to hide comments from a CI system:
.. code-block: yaml
hide-comments:
- author: "^(.*CI|Jenkins)$"
**diff-view**
Specifies how patch diffs should be displayed. The values `unified`
or `side-by-side` (the default) are supported.
Dashboards
++++++++++
This section defines customized dashboards. You may supply any
Gertty search string and bind them to any key. They will appear in
the global help text, and pressing the key anywhere in Gertty will
Boartty search string and bind them to any key. They will appear in
the global help text, and pressing the key anywhere in Boartty will
run the query and display the results.
**dashboards**
@ -337,7 +285,7 @@ run the query and display the results.
bar at the top of the screen.
**query**
The search query to perform to gather changes to be listed in the
The search query to perform to gather stories to be listed in the
dashboard.
**key**
@ -348,60 +296,15 @@ Example:
.. code-block: yaml
dashboards:
- name: "My changes"
query: "owner:self status:open"
- name: "My stories"
query: "creator:self status:active"
key: "f2"
Reviewkeys
++++++++++
Reviewkeys are hotkeys that perform immediate reviews within the
change screen. Any pending comments or review messages will be
attached to the review; otherwise an empty review message will be
left. The approvals list is exhaustive, so if you specify an empty
list, Gertty will submit a review that clears any previous approvals.
Reviewkeys appear in the help text for the change screen.
**reviewkeys**
A list of reviewkey definitions, the format of which is described
below.
**key**
This key to which this review action should be bound.
**approvals**
A list of approvals to include when this reviewkey is activated.
Each element of the list should include both a category and a
value.
**category**
The name of the review label for this approval.
**value**
The value for this approval.
**submit**
Set this to `true` to instruct Gerrit to submit the change when
this reviewkey is activated.
The following example includes a reviewkey that clears all labels, as
well as one that leaves a +1 "Code-Review" approval.
.. code-block: yaml
reviewkeys:
- key: 'meta 0'
approvals: []
- key: 'meta 1'
approvals:
- category: 'Code-Review'
value: 1
General Options
+++++++++++++++
**breadcrumbs**
Gertty displays a footer at the bottom of the screen by default
Boartty displays a footer at the bottom of the screen by default
which contains navigation information in the form of "breadcrumbs"
-- short descriptions of previous screens, with the right-most entry
indicating the screen that will be displayed if you press the `ESC`
@ -412,15 +315,6 @@ General Options
them in UTC instead, set this value to `true`.
**handle-mouse**
Gertty handles mouse input by default. If you don't want it
Boartty handles mouse input by default. If you don't want it
interfering with your terminal's mouse handling, set this value to
`false`.
**expire-age**
By default, closed changes that are older than two months are
removed from the local database (and their refs are removed from the
local git repos so that git may garbage collect them). If you would
like to change the expiration delay or disable it, uncomment the
following line. The time interval is specified in the same way as
the "age:" term in Gerrit's search syntax. To disable it
altogether, set the value to the empty string.

View File

@ -1,10 +1,10 @@
Contributing
------------
For information on how to contribute to Gertty, please see the
For information on how to contribute to Boartty, please see the
contents of the CONTRIBUTING.rst file.
Bugs
~~~~
Bugs are handled at: https://storyboard.openstack.org/#!/project/698
Bugs are handled at: https://storyboard.openstack.org/

View File

@ -1,28 +1,23 @@
Gertty
======
Boartty
=======
Gertty is a console-based interface to the Gerrit Code Review system.
Boartty is a console-based interface to the Storyboard task-tracking
system.
As compared to the web interface, the main advantages are:
* Workflow -- the interface is designed to support a workflow similar
to reading network news or mail. In particular, it is designed to
deal with a large number of review requests across a large number
of projects.
deal with a large number of stories across a large number of
projects.
* Offline Use -- Gertty syncs information about changes in subscribed
projects to a local database and local git repos. All review
operations are performed against that database and then synced back
to Gerrit.
* Offline Use -- Boartty syncs information about changes in
subscribed projects to a local database. All review operations are
performed against that database and then synced back to Storyboard.
* Speed -- user actions modify locally cached content and need not
wait for server interaction.
* Convenience -- because Gertty downloads all changes to local git
repos, a single command instructs it to checkout a change into that
repo for detailed examination or testing of larger changes.
Contents:
.. toctree::

View File

@ -1,52 +1,18 @@
Installation
------------
Debian
~~~~~~
Gertty is packaged in Debian and is currently available in:
* unstable
* testing
* stable
You can install it with::
apt-get install gertty
Fedora
~~~~~~
Gertty is packaged starting in Fedora 21. You can install it with::
yum install python-gertty
openSUSE
~~~~~~~~
Gertty is packaged for openSUSE 13.1 onwards. You can install it via
`1-click install from the Open Build Service <http://software.opensuse.org/package/python-gertty>`_.
Arch Linux
~~~~~~~~~~
Gertty packages are available in the Arch User Repository packages. You
can get the package from::
https://aur.archlinux.org/packages/python2-gertty/
Source
~~~~~~
When installing from source, it is recommended (but not required) to
install Gertty in a virtualenv. To set one up::
install Boartty in a virtualenv. To set one up::
virtualenv gertty-env
source gertty-env/bin/activate
virtualenv boartty-env
source boartty-env/bin/activate
To install the latest version from the cheeseshop::
pip install gertty
pip install boartty
To install from a git checkout::

View File

@ -1,13 +1,13 @@
Usage
-----
After installing Gertty, you should be able to run it by invoking
``gertty``. If you installed it in a virtualenv, you can invoke it
without activating the virtualenv with ``/path/to/venv/bin/gertty``
which you may wish to add to your shell aliases. Use ``gertty
After installing Boartty, you should be able to run it by invoking
``boartty``. If you installed it in a virtualenv, you can invoke it
without activating the virtualenv with ``/path/to/venv/bin/boartty``
which you may wish to add to your shell aliases. Use ``boartty
--help`` to see a list of command line options available.
Once Gertty is running, you will need to start by subscribing to some
Once Boartty is running, you will need to start by subscribing to some
projects. Use 'L' to list all of the projects and then 's' to
subscribe to the ones you are interested in. Hit 'L' again to shrink
the list to your subscribed projects.
@ -15,37 +15,27 @@ the list to your subscribed projects.
In general, pressing the F1 key will show help text on any screen, and
ESC will take you to the previous screen.
Gertty works seamlessly offline or online. All of the actions that it
performs are first recorded in a local database (in ``~/.gertty.db``
by default), and are then transmitted to Gerrit. If Gertty is unable
to contact Gerrit for any reason, it will continue to operate against
the local database, and once it re-establishes contact, it will
process any pending changes.
Boartty works seamlessly offline or online. All of the actions that
it performs are first recorded in a local database (in
``~/.boartty.db`` by default), and are then transmitted to Storyboard.
If Boartty is unable to contact Storyboard for any reason, it will
continue to operate against the local database, and once it
re-establishes contact, it will process any pending changes.
The status bar at the top of the screen displays the current number of
outstanding tasks that Gertty must perform in order to be fully up to
outstanding tasks that Boartty must perform in order to be fully up to
date. Some of these tasks are more complicated than others, and some
of them will end up creating new tasks (for instance, one task may be
to search for new changes in a project which will then produce 5 new
tasks if there are 5 new changes).
to search for new stories in a project which will then produce 5 new
tasks if there are 5 new stories).
If Gertty is offline, it will so indicate in the status bar. It will
If Boartty is offline, it will so indicate in the status bar. It will
retry requests if needed, and will switch between offline and online
mode automatically.
If you review a change while offline with a positive vote, and someone
else leaves a negative vote on that change in the same category before
Gertty is able to upload your review, Gertty will detect the situation
and mark the change as "held" so that you may re-inspect the change
and any new comments before uploading the review. The status bar will
alert you to any held changes and direct you to a list of them (the
`F12` key by default). When viewing a change, the "held" flag may be
toggled with the exclamation key (`!`). Once held, a change must be
explicitly un-held in this manner for your review to be uploaded.
If Gertty encounters an error, this will also be indicated in the
status bar. You may wish to examine ~/.gertty.log to see what the
error was. In many cases, Gertty can continue after encountering an
If Boartty encounters an error, this will also be indicated in the
status bar. You may wish to examine ~/.boartty.log to see what the
error was. In many cases, Boartty can continue after encountering an
error. The error flag will be cleared when you leave the current
screen.
@ -56,17 +46,17 @@ Terminal Integration
~~~~~~~~~~~~~~~~~~~~
If you use rxvt-unicode, you can add something like the following to
``.Xresources`` to make Gerrit URLs that are displayed in your
``.Xresources`` to make Storyboard URLs that are displayed in your
terminal (perhaps in an email or irc client) clickable links that open
in Gertty::
in Boartty::
URxvt.perl-ext: default,matcher
URxvt.url-launcher: sensible-browser
URxvt.keysym.C-Delete: perl:matcher:last
URxvt.keysym.M-Delete: perl:matcher:list
URxvt.matcher.button: 1
URxvt.matcher.pattern.1: https:\/\/review.example.org/(\\#\/c\/)?(\\d+)[\w]*
URxvt.matcher.launcher.1: gertty --open $0
URxvt.matcher.pattern.1: https:\/\/storyboard.example.org/#!/story/(\\d+)[\w]*
URxvt.matcher.launcher.1: boartty --open $0
You will want to adjust the pattern to match the review site you are
You will want to adjust the pattern to match the Storyboard site you are
interested in; multiple patterns may be added as needed.

View File

@ -1,88 +0,0 @@
# This is an example ~/.gertty.yaml file for use with installations of
# Gerrit running on googlesource.com. Most of these options are not
# required, rather, they customize Gertty to better deal with the
# particulars of Google's Gerrit configuration.
# This file does not list all of the available options. For a full
# list with explanations, see the 'reference-gertty.yaml' file.
servers:
- name: CHANGEME-review
url: https://CHANGEME-review.googlesource.com/
# Your gerrit username.
username: CHANGEME
# Set password at https://{name}-review.googlesource.com/#/settings/http-password
# Note this is not your Google password.
password: CHANGEME
auth-type: basic
git-root: ~/git/
# Uncomment the next line if your terminal has a white background
# palette: light
# Commentlinks are regexes that are applied to commit and review
# messages. They can be replaced with internal or external links, or
# have colors applied.
commentlinks:
# Match Gerrit change ids, and replace them with a link to an
# internal Gertty search for that change id.
- match: "(?P<id>I[0-9a-fA-F]{40})"
replacements:
- search:
text: "{id}"
query: "change:{id}"
# Uncomment the following line to use a unified diff view instead
# of the default side-by-side:
# diff-view: unified
# This section defines customized dashboards. You can supply any
# Gertty search string and bind them to any key. They will appear in
# the global help text, and pressing the key anywhere in Gertty will
# discard the current display stack and replace it with the results of
# the query.
#
# NB: "recentlyseen:24 hours" does not just return changes seen in the
# last 24 hours -- it returns changes seen within 24 hours of the most
# recently seen change. So you can take the weekend off and pick up
# where you were.
dashboards:
- name: "My changes"
query: "owner:self status:open"
key: "f2"
- name: "Incoming reviews"
query: "is:open is:reviewer"
key: "f3"
- name: "Starred changes"
query: "is:starred"
key: "f4"
- name: "Recently seen changes"
query: "recentlyseen:24 hours"
sort-by: "last-seen"
reverse: True
key: "f5"
# Reviewkeys are hotkeys that perform immediate reviews within the
# change screen. Any pending comments or review messages will be
# attached to the review; otherwise an empty review will be left. The
# approvals list is exhaustive, so if you specify an empty list,
# Gertty will submit a review that clears any previous approvals. To
# submit the change with the review, include 'submit: True' with the
# reviewkey. Reviewkeys appear in the help text for the change
# screen.
reviewkeys:
- key: 'meta 0'
approvals: []
- key: 'meta 1'
approvals:
- category: 'Code-Review'
value: 1
- key: 'meta 2'
approvals:
- category: 'Code-Review'
value: 2
- key: 'meta 3'
approvals:
- category: 'Code-Review'
value: 2
submit: True

View File

@ -0,0 +1,15 @@
# This is an example ~/.boartty.yaml file with only the required
# settings.
# This file does not list all of the available options. For a full
# list with explanations, see the 'reference-boartty.yaml' file.
servers:
- name: CHANGEME
url: https://CHANGEME.example.org/
# Your authentication token for Storyboard. Go to the "Profile"
# and then "Tokens" (the "key" icon) page in the Storyboard web
# interface and create a token. Give it a sufficiently long
# validity period (e.g., one decade), and copy and paste the value
# here.
token: CHANGEME

View File

@ -1,13 +0,0 @@
# This is an example ~/.gertty.yaml file with only the required
# settings.
# This file does not list all of the available options. For a full
# list with explanations, see the 'reference-gertty.yaml' file.
servers:
- name: CHANGEME
url: https://CHANGEME.example.org/
username: CHANGEME
# Set corresponding HTTP password in gerrit settings
password: CHANGEME
git-root: ~/git/

View File

@ -0,0 +1,42 @@
# This is an example ~/.boartty.yaml file for use with OpenStack's
# Storyboard. Most of these options are not required, rather, they
# customize Boartty to better deal with the particulars of OpenStack's
# Storyboard configuration.
# This file does not list all of the available options. For a full
# list with explanations, see the 'reference-boartty.yaml' file.
servers:
- name: openstack
url: https://review.openstack.org/
# Your authentication token for Storyboard. Go to the "Profile"
# and then "Tokens" (the "key" icon) page in the Storyboard web
# interface and create a token. Give it a sufficiently long
# validity period (e.g., one decade), and copy and paste the value
# here.
token: CHANGEME
# Uncomment the next line if your terminal has a white background
# palette: light
# This section defines customized dashboards. You can supply any
# Boartty search string and bind them to any key. They will appear in
# the global help text, and pressing the key anywhere in Boartty will
# run the query and display the results.
#
# NB: "recentlyseen:24 hours" does not just return stories seen in the
# last 24 hours -- it returns stories seen within 24 hours of the most
# recently seen change. So you can take the weekend off and pick up
# where you were.
dashboards:
- name: "My stories"
query: "creator:self status:active"
key: "f2"
- name: "Starred stories"
query: "is:starred"
key: "f3"
- name: "Recently seen stories"
query: "recentlyseen:24 hours"
sort-by: "last-seen"
reverse: True
key: "f4"

View File

@ -1,142 +0,0 @@
# This is an example ~/.gertty.yaml file for use with OpenStack's
# Gerrit. Most of these options are not required, rather, they
# customize Gertty to better deal with the particulars of OpenStack's
# Gerrit configuration.
# This file does not list all of the available options. For a full
# list with explanations, see the 'reference-gertty.yaml' file.
servers:
- name: openstack
url: https://review.openstack.org/
# Your gerrit username.
username: CHANGEME
# Set password at https://review.openstack.org/#/settings/http-password
# Note this is not your launchpad password.
password: CHANGEME
git-root: ~/git/
# This section adds the colors that we will reference later in the
# commentlinks section for test results. You can also change other
# colors here.
palettes:
- name: default
test-SUCCESS: ['light green', '']
test-FAILURE: ['light red', '']
test-UNSTABLE: ['yellow', '']
# Uncomment the next line if your terminal has a white background
# palette: light
# Commentlinks are regexes that are applied to commit and review
# messages. They can be replaced with internal or external links, or
# have colors applied.
commentlinks:
# This matches the job results left by Zuul.
- match: "^- (?P<job>.*?) (?P<url>.*?) : (?P<result>[^ ]+) ?(?P<comment>.*)$"
# This indicates that this is a test result, and should be indexed
# using the "job" match group from the commentlink regex. Gertty
# displays test results in their own area of the screen.
test-result: "{job}"
replacements:
# Replace the matching text with a hyperlink to the "url" match
# group whose text is the "job" match group.
- link:
text: "{job:<42}"
url: "{url}"
# Follow that with the plain text of the "result" match group
# with the color "test-{result}" applied. See the palette
# section above.
- text:
color: "test-{result}"
text: "{result} "
# And then follow that with the plain text of the "comment"
# match group.
- text: "{comment}"
# Match Gerrit change ids, and replace them with a link to an
# internal Gertty search for that change id.
- match: "(?P<id>I[0-9a-fA-F]{40})"
replacements:
- search:
text: "{id}"
query: "change:{id}"
# Match external references to bugs on Launchpad
- match: "(?P<bug_str>(?:[Cc]loses|[Pp]artial|[Rr]elated)-[Bb]ug *: *#?(?P<bug_id>\\d+))"
replacements:
- link:
text: "{bug_str}"
url: "https://launchpad.net/bugs/{bug_id}"
# Match external references to blueprints on Launchpad
- match: "blueprint +(?P<blueprint>[\\w\\-.]+)"
replacements:
- link:
text: "blueprint {blueprint}"
url: "https://blueprints.launchpad.net/openstack/?searchtext={blueprint}"
# This is the query used for the list of changes when a project is
# selected. The default is "status:open". If you don't want to see
# changes which are WIP or have verification failures, use a query like this:
# change-list-query: "status:open not label:Workflow=-1"
# If you also want to exclude reviews with failed tests, the query is slightly
# more complex:
# "status:open not (label:Workflow=-1 or label:Verified=-1)"
# Uncomment the following line to use a unified diff view instead of the
# default side-by-side:
# diff-view: unified
# Hide comments by default that match the following criteria.
# You can toggle their display with 't'.
hide-comments:
- author: "^(.*CI|Jenkins)$"
# This section defines customized dashboards. You can supply any
# Gertty search string and bind them to any key. They will appear in
# the global help text, and pressing the key anywhere in Gertty will
# discard the current display stack and replace it with the results of
# the query.
#
# NB: "recentlyseen:24 hours" does not just return changes seen in the
# last 24 hours -- it returns changes seen within 24 hours of the most
# recently seen change. So you can take the weekend off and pick up
# where you were.
dashboards:
- name: "My changes"
query: "owner:self status:open"
key: "f2"
- name: "Incoming reviews"
query: "is:open is:reviewer"
key: "f3"
- name: "Starred changes"
query: "is:starred"
key: "f4"
- name: "Recently seen changes"
query: "recentlyseen:24 hours"
sort-by: "last-seen"
reverse: True
key: "f5"
# Reviewkeys are hotkeys that perform immediate reviews within the
# change screen. Any pending comments or review messages will be
# attached to the review; otherwise an empty review will be left. The
# approvals list is exhaustive, so if you specify an empty list,
# Gertty will submit a review that clears any previous approvals.
# They will appear in the help text for the change screen.
reviewkeys:
- key: 'meta 0'
approvals: []
- key: 'meta 1'
approvals:
- category: 'Code-Review'
value: 1
- key: 'meta 2'
approvals:
- category: 'Code-Review'
value: 2
- key: 'meta 3'
approvals:
- category: 'Code-Review'
value: 2
- category: 'Workflow'
value: 1

View File

@ -0,0 +1,187 @@
# This is an example ~/.boartty.yaml with an exhaustive listing of
# options with commentary.
# This section lists the servers that Boartty can talk to. Multiple
# servers may be listed; by default, Boartty will use the first one
# listed. To select another, simply specify its name on the command
# line.
servers:
- name: CHANGEME
url: https://CHANGEME.example.org/
# Your authentication token for Storyboard. Go to the "Profile"
# and then "Tokens" (the "key" icon) page in the Storyboard web
# interface and create a token. Give it a sufficiently long
# validity period (e.g., one decade), and copy and paste the value
# here.
token: CHANGEME
lock-file: ~/.boartty.CHANGEME.lock
# Each server section can have the following fields:
# A name that describes the server, to reference on the command line. [required]
# - name: sample
# The URL of the Storyboard server. HTTPS should be preferred. [required]
# url: https://example.org/
# Your authentication token for Storyboard. Go to the "Profile" and
# then "Tokens" (the "key" icon) page in the Storyboard web interface
# and create a token. Give it a sufficiently long validity period
# (e.g., one decade), and copy and paste the value here.
# token: CHANGEME
# The location of Boartty's sqlite database. If you have more than one
# server, you should specify a dburi for any additional servers.
# By default a SQLite database called ~/.boartty.db is used.
# dburi: sqlite:////home/user/.boartty.db
# If your Storyboard server uses a non-standard certificate chain (e.g. on a test
# server), you can pass a full path to a bundle of CA certificates here:
# ssl-ca-path: ~/.pki/ca-chain.pem
# In case you do not care about security and want to use a sledgehammer
# approach to SSL, you can set this value to false to turn off certificate
# validation.
# verify-ssl: true
# By default Boartty logs errors to a file and truncates that file each
# time it starts (so that it does not grow without bound). If you
# would like to log to a different location, you may specify it here.
# log-file: ~/.boartty.log
# Boartty listens on a unix domain socket for remote commands at
# ~/.boartty.sock. You may change the path here:
# socket: ~/.boartty.sock
# Boartty uses a lock file per server to prevent multiple processes
# from running at the same time. Example:
# lock-file: /run/lockme.lock
# Boartty comes with two palettes defined internally. The default
# palette is suitable for use on a terminal with a dark background.
# The "light" palette is for a terminal with a white or light
# background. You may customize the colors in either of those
# palettes, or define your own palette.
# The following alters two colors in the default palette, one color in
# the light palette, and one color in a custom palette. If any color
# is not defined in a palette, the value from the default palette is
# used. The values are a list of at least two elements describing the
# colors to be use for the foreground and background colors.
# Additional elements can specify (in order) the color to use for
# monochrome terminals, the foreground, and background colors to use
# in high-color terminals.
# For a reference of possible color names, see:
# http://urwid.org/manual/displayattributes.html#foreground-and-background-settings
# To see the list of possible palette entries, run "boartty --print-palette".
palettes:
- name: default
task-title: ['light green', '']
task-id: ['dark cyan', '']
- name: light
task-project: ['dark blue', '']
- name: custom
task-project: ['dark red', '']
# Palettes may be selected at runtime with the "-p PALETTE" command
# line option, or you may set the default palette here:
# palette: light
# Keymaps work the same way as palettes. Two keymaps are defined
# internally, the 'default' keymap and the 'vi' keymap. Individual
# keys may be overridden and custom keymaps defined and selected in
# the config file or the command line.
# Each keymap contains a mapping of command -> key(s). If a command
# is not specified, Boartty will use the keybinding specified in the
# default map. More than one key can be bound to a command.
# Run "boartty --print-keymap" for a list of commands that can be
# bound.
keymaps:
- name: default
leave-comment: 'r'
- name: custom
leave-comment: ['r', 'R']
- name: osx # OS X blocks ctrl+o
story-search: 'ctrl s'
# To specify a sequence of keys, they must be a list of keystrokes
# within a list of key series. For example:
- name: vi
quit: [[':', 'q']]
# The default keymap may be selected with the '-k KEYMAP' command line
# option, or with the following line:
# keymap: custom
# Commentlinks are regular expressions that are applied to commit and
# review messages. They can be replaced with internal or external
# links, or have colors applied.
commentlinks:
# This example matches story numbers, and replaces them with a link to
# an internal Boartty search for that story.
- match: "(?P<id>[0-9]+)"
replacements:
- search:
text: "{id}"
query: "story:{id}"
# Any number of commentlink entries may be specified. Start each with
# a "match" key and regex. Named match groups within the regex may be
# used in the replacements section. Any number of replacements may be
# specified. The types of replacement available are:
#
# Text: Plain text whose color may be specified. The color references
# a palette entry.
# - text:
# color: ""
# text: ""
# Link: A hyperlink with the indicated text that when activated will
# open the user's browser with the supplied URL
# - link:
# text: ""
# url: ""
# Search: A hyperlink that will perform a Boartty search when
# activated.
# - search:
# text: "{id}"
# query: "change:{id}"
# This is the query used for the list of stories when a project is
# selected. The default is empty.
# story-list-query: ""
# This section defines default sorting options for the story
# list. The "sort-by" key specifies the sort order, which can be
# 'number', 'updated', or 'last-seen'. The 'reverse' key specifies
# ascending (true) or descending (false) order.
# story-list-options:
# sort-by: 'number'
# reverse: false
# Uncomment the following line to disable the navigation breadcrumbs
# at the bottom of the screen:
# breadcrumbs: false
# Times are displayed in the local timezone by default. To display
# them in UTC instead, uncomment the following line:
# display-times-in-utc: true
# Boartty handles mouse input by default. If you don't want it messing
# with your terminal's mouse handling, uncomment the following line:
# handle-mouse: false
# This section defines customized dashboards. You can supply any
# Boartty search string and bind them to any key. They will appear in
# the global help text, and pressing the key anywhere in Boartty will
# run the query and display the results.
#
# NB: "recentlyseen:24 hours" does not just return stories seen in the
# last 24 hours -- it returns stories seen within 24 hours of the most
# recently seen change. So you can take the weekend off and pick up
# where you were.
dashboards:
- name: "My stories"
query: "creator:self status:active"
key: "f2"
- name: "Starred stories"
query: "is:starred"
key: "f3"
- name: "Recently seen stories"
query: "recentlyseen:24 hours"
sort-by: "last-seen"
reverse: True
key: "f4"

View File

@ -1,248 +0,0 @@
# This is an example ~/.gertty.yaml with an exhaustive listing of
# options with commentary.
# This section lists the servers that Gertty can talk to. Multiple
# servers may be listed; by default, Gertty will use the first one
# listed. To select another, simply specify its name on the command
# line.
servers:
- name: CHANGEME
url: https://CHANGEME.example.org/
username: CHANGEME
# Your HTTP Password for gerrit. Go to the "HTTP Password" section in your
# account settings to generate/retrieve this password.
password: CHANGEME
git-root: ~/git/
lock-file: ~/.gertty.CHANGEME.lock
# Each server section can have the following fields:
# A name that describes the server, to reference on the command line. [required]
# - name: sample
# The URL of the Gerrit server. HTTPS should be preferred. [required]
# url: https://example.org/
# Your username in Gerrit. [required]
# username: CHANGEME
# Your password in Gerrit (Settings -> HTTP Password). [required]
# password: CHANGEME
# Authentication type required by the Gerrit server. Can be 'basic',
# 'digest', or 'form'. Defaults to 'digest'.
# auth-type: digest
# A location where Gertty should store its git repositories. These
# can be the same git repositories where you do your own work --
# Gertty will not modify them unless you tell it to, and even then the
# normal git protections against losing work remain in place. [required]
# git-root: ~/git/
# The URL to clone git repos. By default, <url>/p/<project> is used. For a list
# of valid URLs, see:
# https://www.kernel.org/pub/software/scm/git/docs/git-clone.html#URLS
# git-url: ssh://user@example.org:29418
# The location of Gertty's sqlite database. If you have more than one
# server, you should specify a dburi for any additional servers.
# By default a SQLite database called ~/.gertty.db is used.
# dburi: sqlite:////home/user/.gertty.db
# If your Gerrit server uses a non-standard certificate chain (e.g. on a test
# server), you can pass a full path to a bundle of CA certificates here:
# ssl-ca-path: ~/.pki/ca-chain.pem
# In case you do not care about security and want to use a sledgehammer
# approach to SSL, you can set this value to false to turn off certificate
# validation.
# verify-ssl: true
# By default Gertty logs errors to a file and truncates that file each
# time it starts (so that it does not grow without bound). If you
# would like to log to a different location, you may specify it here.
# log-file: ~/.gertty.log
# Gertty listens on a unix domain socket for remote commands at
# ~/.gertty.sock. You may change the path here:
# socket: ~/.gertty.sock
# Gertty uses a lock file per server to prevent multiple processes
# from running at the same time. Example:
# lock-file: /run/lockme.lock
# Gertty comes with two palettes defined internally. The default
# palette is suitable for use on a terminal with a dark background.
# The "light" palette is for a terminal with a white or light
# background. You may customize the colors in either of those
# palettes, or define your own palette.
# The following alters two colors in the default palette, one color in
# the light palette, and one color in a custom palette. If any color
# is not defined in a palette, the value from the default palette is
# used. The values are a list of at least two elements describing the
# colors to be use for the foreground and background colors.
# Additional elements can specify (in order) the color to use for
# monochrome terminals, the foreground, and background colors to use
# in high-color terminals.
# For a reference of possible color names, see:
# http://urwid.org/manual/displayattributes.html#foreground-and-background-settings
# To see the list of possible palette entries, run "gertty --print-palette".
palettes:
- name: default
added-line: ['dark green', '']
added-word: ['light green', '']
- name: light
filename: ['dark cyan', '']
- name: custom
filename: ['light yellow', '']
# Palettes may be selected at runtime with the "-p PALETTE" command
# line option, or you may set the default palette here:
# palette: light
# Keymaps work the same way as palettes. Two keymaps are defined
# internally, the 'default' keymap and the 'vi' keymap. Individual
# keys may be overridden and custom keymaps defined and selected in
# the config file or the command line.
# Each keymap contains a mapping of command -> key(s). If a command
# is not specified, Gertty will use the keybinding specified in the
# default map. More than one key can be bound to a command.
# Run "gertty --print-keymap" for a list of commands that can be
# bound.
keymaps:
- name: default
diff: 'd'
- name: custom
review: ['r', 'R']
- name: osx # OS X blocks ctrl+o
change-search: 'ctrl s'
# To specify a sequence of keys, they must be a list of keystrokes
# within a list of key series. For example:
- name: vi
quit: [[':', 'q']]
# The default keymap may be selected with the '-k KEYMAP' command line
# option, or with the following line:
# keymap: custom
# Commentlinks are regular expressions that are applied to commit and
# review messages. They can be replaced with internal or external
# links, or have colors applied.
commentlinks:
# Match Gerrit change ids, and replace them with a link to an internal
# Gertty search for that change id.
- match: "(?P<id>I[0-9a-fA-F]{40})"
replacements:
- search:
text: "{id}"
query: "change:{id}"
# Any number of commentlink entries may be specified. Start each with
# a "match" key and regex. Named match groups within the regex may be
# used in the replacements section. Any number of replacements may be
# specified. The types of replacement available are:
#
# Text: Plain text whose color may be specified. The color references
# a palette entry.
# - text:
# color: ""
# text: ""
# Link: A hyperlink with the indicated text that when activated will
# open the user's browser with the supplied URL
# - link:
# text: ""
# url: ""
# Search: A hyperlink that will perform a Gertty search when
# activated.
# - search:
# text: "{id}"
# query: "change:{id}"
# This is the query used for the list of changes when a project is
# selected. The default is "status:open".
# change-list-query: "status:open"
# This section defines default sorting options for the change
# list. The "sort-by" key specifies the sort order, which can be
# 'number', 'updated', or 'last-seen'. The 'reverse' key specifies
# ascending (true) or descending (false) order.
# change-list-options:
# sort-by: 'number'
# reverse: false
# Uncomment the following line to disable the navigation breadcrumbs
# at the bottom of the screen:
# breadcrumbs: false
# Uncomment the following line to use a unified diff view instead
# of the default side-by-side:
# diff-view: unified
# Dependent changes are displayed as "threads" in the change list by
# default. To disable this behavior, uncomment the following line:
# thread-changes: false
# Times are displayed in the local timezone by default. To display
# them in UTC instead, uncomment the following line:
# display-times-in-utc: true
# Gertty handles mouse input by default. If you don't want it messing
# with your terminal's mouse handling, uncomment the following line:
# handle-mouse: false
# Closed changes that are older than two months are removed from the
# local database (and their refs are removed from the local git repos
# so that git may garbage collect them). If you would like to change
# the expiration delay or disable it, uncomment the following line.
# The time interval is specified in the same way as the "age:" term in
# Gerrit's search syntax. To disable it altogether, set the value to
# the empty string.
# expire-age: '2 months'
# Uncomment the following lines to Hide comments by default that match
# certain criteria. You can toggle their display with 't'. Currently
# the only supported criterion is "author".
# hide-comments:
# - author: "^(.*CI|Jenkins)$"
# This section defines customized dashboards. You can supply any
# Gertty search string and bind them to any key. They will appear in
# the global help text, and pressing the key anywhere in Gertty will
# run the query and display the results.
#
# NB: "recentlyseen:24 hours" does not just return changes seen in the
# last 24 hours -- it returns changes seen within 24 hours of the most
# recently seen change. So you can take the weekend off and pick up
# where you were.
dashboards:
- name: "My changes"
query: "owner:self status:open"
key: "f2"
- name: "Incoming reviews"
query: "is:open is:reviewer"
key: "f3"
- name: "Starred changes"
query: "is:starred"
key: "f4"
- name: "Recently seen changes"
query: "recentlyseen:24 hours"
sort-by: "last-seen"
reverse: True
key: "f5"
# Reviewkeys are hotkeys that perform immediate reviews within the
# change screen. Any pending comments or review messages will be
# attached to the review; otherwise an empty review will be left. The
# approvals list is exhaustive, so if you specify an empty list,
# Gertty will submit a review that clears any previous approvals. To
# submit the change with the review, include 'submit: True' with the
# reviewkey. Reviewkeys appear in the help text for the change
# screen.
reviewkeys:
- key: 'meta 0'
approvals: []
- key: 'meta 1'
approvals:
- category: 'Code-Review'
value: 1
- key: 'meta 2'
approvals:
- category: 'Code-Review'
value: 2
- key: 'meta 3'
approvals:
- category: 'Code-Review'
value: 2
submit: True

View File

@ -1,26 +0,0 @@
"""add query sync table
Revision ID: 1bb187bcd401
Revises: 3cc7e3753dc3
Create Date: 2015-03-26 07:32:33.584657
"""
# revision identifiers, used by Alembic.
revision = '1bb187bcd401'
down_revision = '3cc7e3753dc3'
from alembic import op
import sqlalchemy as sa
def upgrade():
op.create_table('sync_query',
sa.Column('key', sa.Integer(), nullable=False),
sa.Column('name', sa.String(255), index=True, unique=True, nullable=False),
sa.Column('updated', sa.DateTime, index=True),
sa.PrimaryKeyConstraint('key')
)
def downgrade():
pass

View File

@ -1,22 +0,0 @@
"""add revision indexes
Revision ID: 1cdd4e2e74c
Revises: 4a802b741d2f
Create Date: 2015-03-10 16:17:41.330825
"""
# revision identifiers, used by Alembic.
revision = '1cdd4e2e74c'
down_revision = '4a802b741d2f'
from alembic import op
def upgrade():
op.create_index(op.f('ix_revision_commit'), 'revision', ['commit'])
op.create_index(op.f('ix_revision_parent'), 'revision', ['parent'])
def downgrade():
pass

View File

@ -1,64 +0,0 @@
"""attach comments to files
Revision ID: 254ac5fc3941
Revises: 50344aecd1c2
Create Date: 2015-04-13 15:52:07.104397
"""
# revision identifiers, used by Alembic.
revision = '254ac5fc3941'
down_revision = '50344aecd1c2'
import sys
import warnings
from alembic import op
import sqlalchemy as sa
from gertty.dbsupport import sqlite_alter_columns, sqlite_drop_columns
def upgrade():
with warnings.catch_warnings():
warnings.simplefilter("ignore")
op.add_column('comment', sa.Column('file_key', sa.Integer()))
sqlite_alter_columns('comment', [
sa.Column('file_key', sa.Integer(), sa.ForeignKey('file.key'))
])
update_query = sa.text('update comment set file_key=:file_key where key=:key')
file_query = sa.text('select f.key from file f where f.revision_key=:revision_key and f.path=:path')
file_insert_query = sa.text('insert into file (key, revision_key, path, old_path, status, inserted, deleted) '
' values (NULL, :revision_key, :path, NULL, NULL, NULL, NULL)')
conn = op.get_bind()
countres = conn.execute('select count(*) from comment')
comments = countres.fetchone()[0]
comment_res = conn.execute('select p.name, c.number, c.status, r.key, r.number, m.file, m.key '
'from project p, change c, revision r, comment m '
'where m.revision_key=r.key and r.change_key=c.key and '
'c.project_key=p.key order by p.name')
count = 0
for (pname, cnumber, cstatus, rkey, rnumber, mfile, mkey) in comment_res.fetchall():
count += 1
sys.stdout.write('Comment %s / %s\r' % (count, comments))
sys.stdout.flush()
file_res = conn.execute(file_query, revision_key=rkey, path=mfile)
file_key = file_res.fetchone()
if not file_key:
conn.execute(file_insert_query, revision_key=rkey, path=mfile)
file_res = conn.execute(file_query, revision_key=rkey, path=mfile)
file_key = file_res.fetchone()
fkey = file_key[0]
file_res = conn.execute(update_query, file_key=fkey, key=mkey)
sqlite_drop_columns('comment', ['revision_key', 'file'])
print
def downgrade():
pass

View File

@ -1,25 +0,0 @@
"""fix account table
Revision ID: 2a11dd14665
Revises: 4cc9c46f9d8b
Create Date: 2014-08-20 13:07:25.079603
"""
# revision identifiers, used by Alembic.
revision = '2a11dd14665'
down_revision = '4cc9c46f9d8b'
from alembic import op
def upgrade():
op.drop_index('ix_account_name', 'account')
op.drop_index('ix_account_username', 'account')
op.drop_index('ix_account_email', 'account')
op.create_index(op.f('ix_account_name'), 'account', ['name'])
op.create_index(op.f('ix_account_username'), 'account', ['username'])
op.create_index(op.f('ix_account_email'), 'account', ['email'])
def downgrade():
pass

View File

@ -1,36 +0,0 @@
"""add can_submit column
Revision ID: 312cd5a9f878
Revises: 46b175bfa277
Create Date: 2014-09-18 16:37:13.149729
"""
# revision identifiers, used by Alembic.
revision = '312cd5a9f878'
down_revision = '46b175bfa277'
import warnings
from alembic import op
import sqlalchemy as sa
from gertty.dbsupport import sqlite_alter_columns
def upgrade():
with warnings.catch_warnings():
warnings.simplefilter("ignore")
op.add_column('revision', sa.Column('can_submit', sa.Boolean()))
conn = op.get_bind()
q = sa.text('update revision set can_submit=:submit')
conn.execute(q, submit=False)
sqlite_alter_columns('revision', [
sa.Column('can_submit', sa.Boolean(), nullable=False),
])
def downgrade():
pass

View File

@ -1,27 +0,0 @@
"""add conflicts table
Revision ID: 3610c2543e07
Revises: 4388de50824a
Create Date: 2016-02-05 16:43:20.047238
"""
# revision identifiers, used by Alembic.
revision = '3610c2543e07'
down_revision = '4388de50824a'
from alembic import op
import sqlalchemy as sa
def upgrade():
op.create_table('change_conflict',
sa.Column('key', sa.Integer(), nullable=False),
sa.Column('change1_key', sa.Integer(), sa.ForeignKey('change.key'), index=True),
sa.Column('change2_key', sa.Integer(), sa.ForeignKey('change.key'), index=True),
sa.PrimaryKeyConstraint('key')
)
def downgrade():
pass

View File

@ -1,26 +0,0 @@
"""add last_seen column to change
Revision ID: 37a702b7f58e
Revises: 3610c2543e07
Create Date: 2016-02-06 09:09:38.728225
"""
# revision identifiers, used by Alembic.
revision = '37a702b7f58e'
down_revision = '3610c2543e07'
import warnings
from alembic import op
import sqlalchemy as sa
def upgrade():
with warnings.catch_warnings():
warnings.simplefilter("ignore")
op.add_column('change', sa.Column('last_seen', sa.DateTime, index=True))
def downgrade():
pass

View File

@ -1,33 +0,0 @@
"""Added project updated column
Revision ID: 38104b4c1b84
Revises: 56e48a4a064a
Create Date: 2014-05-31 06:52:12.452205
"""
# revision identifiers, used by Alembic.
revision = '38104b4c1b84'
down_revision = '56e48a4a064a'
from alembic import op
import sqlalchemy as sa
def upgrade():
op.add_column('project', sa.Column('updated', sa.DateTime))
conn = op.get_bind()
res = conn.execute('select "key", name from project')
for (key, name) in res.fetchall():
q = sa.text("select max(updated) from change where project_key=:key")
res = conn.execute(q, key=key)
for (updated,) in res.fetchall():
q = sa.text('update project set updated=:updated where "key"=:key')
conn.execute(q, key=key, updated=updated)
op.create_index(op.f('ix_project_updated'), 'project', ['updated'], unique=False)
def downgrade():
op.drop_index(op.f('ix_project_updated'), table_name='project')
op.drop_column('project', 'updated')

View File

@ -1,37 +0,0 @@
"""add held
Revision ID: 3cc7e3753dc3
Revises: 1cdd4e2e74c
Create Date: 2015-03-22 08:48:15.516289
"""
# revision identifiers, used by Alembic.
revision = '3cc7e3753dc3'
down_revision = '1cdd4e2e74c'
import warnings
from alembic import op
import sqlalchemy as sa
from gertty.dbsupport import sqlite_alter_columns
def upgrade():
with warnings.catch_warnings():
warnings.simplefilter("ignore")
op.add_column('change', sa.Column('held', sa.Boolean()))
connection = op.get_bind()
change = sa.sql.table('change',
sa.sql.column('held', sa.Boolean()))
connection.execute(change.update().values({'held':False}))
sqlite_alter_columns('change', [
sa.Column('held', sa.Boolean(), index=True, nullable=False),
])
def downgrade():
pass

View File

@ -1,49 +0,0 @@
"""add draft fields
Revision ID: 3d429503a29a
Revises: 2a11dd14665
Create Date: 2014-08-30 13:26:03.698902
"""
# revision identifiers, used by Alembic.
revision = '3d429503a29a'
down_revision = '2a11dd14665'
import warnings
from alembic import op
import sqlalchemy as sa
from gertty.dbsupport import sqlite_alter_columns, sqlite_drop_columns
def upgrade():
with warnings.catch_warnings():
warnings.simplefilter("ignore")
op.add_column('message', sa.Column('draft', sa.Boolean()))
op.add_column('comment', sa.Column('draft', sa.Boolean()))
op.add_column('approval', sa.Column('draft', sa.Boolean()))
conn = op.get_bind()
conn.execute("update message set draft=pending")
conn.execute("update comment set draft=pending")
conn.execute("update approval set draft=pending")
sqlite_alter_columns('message', [
sa.Column('draft', sa.Boolean(), index=True, nullable=False),
])
sqlite_alter_columns('comment', [
sa.Column('draft', sa.Boolean(), index=True, nullable=False),
])
sqlite_alter_columns('approval', [
sa.Column('draft', sa.Boolean(), index=True, nullable=False),
])
sqlite_drop_columns('comment', ['pending'])
sqlite_drop_columns('approval', ['pending'])
def downgrade():
pass

View File

@ -1,35 +0,0 @@
"""add topic table
Revision ID: 4388de50824a
Revises: 254ac5fc3941
Create Date: 2015-10-31 19:06:38.538948
"""
# revision identifiers, used by Alembic.
revision = '4388de50824a'
down_revision = '254ac5fc3941'
from alembic import op
import sqlalchemy as sa
def upgrade():
op.create_table('topic',
sa.Column('key', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=255), index=True, nullable=False),
sa.Column('sequence', sa.Integer(), index=True, unique=True, nullable=False),
sa.PrimaryKeyConstraint('key')
)
op.create_table('project_topic',
sa.Column('key', sa.Integer(), nullable=False),
sa.Column('project_key', sa.Integer(), sa.ForeignKey('project.key'), index=True),
sa.Column('topic_key', sa.Integer(), sa.ForeignKey('topic.key'), index=True),
sa.Column('sequence', sa.Integer(), nullable=False),
sa.PrimaryKeyConstraint('key'),
sa.UniqueConstraint('topic_key', 'sequence', name='topic_key_sequence_const'),
)
def downgrade():
pass

View File

@ -1,176 +0,0 @@
"""Initial schema
Revision ID: 44402069e137
Revises: None
Create Date: 2014-05-04 17:10:23.127702
"""
# revision identifiers, used by Alembic.
revision = '44402069e137'
down_revision = None
from alembic import op
import sqlalchemy as sa
def upgrade():
### commands auto generated by Alembic - please adjust! ###
op.create_table('project',
sa.Column('key', sa.Integer(), nullable=False, quote=True),
sa.Column('name', sa.String(length=255), nullable=False),
sa.Column('subscribed', sa.Boolean(), nullable=True),
sa.Column('description', sa.Text(), nullable=False),
sa.PrimaryKeyConstraint('key')
)
op.create_index(op.f('ix_project_name'), 'project', ['name'], unique=True)
op.create_index(op.f('ix_project_subscribed'), 'project', ['subscribed'], unique=False)
op.create_table('change',
sa.Column('key', sa.Integer(), nullable=False, quote=True),
sa.Column('project_key', sa.Integer(), nullable=True),
sa.Column('id', sa.String(length=255), nullable=False),
sa.Column('number', sa.Integer(), nullable=False),
sa.Column('branch', sa.String(length=255), nullable=False),
sa.Column('change_id', sa.String(length=255), nullable=False),
sa.Column('topic', sa.String(length=255), nullable=True),
sa.Column('owner', sa.String(length=255), nullable=True),
sa.Column('subject', sa.Text(), nullable=False),
sa.Column('created', sa.DateTime(), nullable=False),
sa.Column('updated', sa.DateTime(), nullable=False),
sa.Column('status', sa.String(length=8), nullable=False),
sa.Column('hidden', sa.Boolean(), nullable=False),
sa.Column('reviewed', sa.Boolean(), nullable=False),
sa.ForeignKeyConstraint(['project_key'], ['project.key'], ),
sa.PrimaryKeyConstraint('key')
)
op.create_index(op.f('ix_change_branch'), 'change', ['branch'], unique=False)
op.create_index(op.f('ix_change_change_id'), 'change', ['change_id'], unique=False)
op.create_index(op.f('ix_change_created'), 'change', ['created'], unique=False)
op.create_index(op.f('ix_change_hidden'), 'change', ['hidden'], unique=False)
op.create_index(op.f('ix_change_id'), 'change', ['id'], unique=True)
op.create_index(op.f('ix_change_number'), 'change', ['number'], unique=True)
op.create_index(op.f('ix_change_owner'), 'change', ['owner'], unique=False)
op.create_index(op.f('ix_change_project_key'), 'change', ['project_key'], unique=False)
op.create_index(op.f('ix_change_reviewed'), 'change', ['reviewed'], unique=False)
op.create_index(op.f('ix_change_status'), 'change', ['status'], unique=False)
op.create_index(op.f('ix_change_topic'), 'change', ['topic'], unique=False)
op.create_index(op.f('ix_change_updated'), 'change', ['updated'], unique=False)
op.create_table('approval',
sa.Column('key', sa.Integer(), nullable=False, quote=True),
sa.Column('change_key', sa.Integer(), nullable=True),
sa.Column('name', sa.String(length=255), nullable=True),
sa.Column('category', sa.String(length=255), nullable=False),
sa.Column('value', sa.Integer(), nullable=False),
sa.Column('pending', sa.Boolean(), nullable=False),
sa.ForeignKeyConstraint(['change_key'], ['change.key'], ),
sa.PrimaryKeyConstraint('key')
)
op.create_index(op.f('ix_approval_change_key'), 'approval', ['change_key'], unique=False)
op.create_index(op.f('ix_approval_pending'), 'approval', ['pending'], unique=False)
op.create_table('revision',
sa.Column('key', sa.Integer(), nullable=False, quote=True),
sa.Column('change_key', sa.Integer(), nullable=True),
sa.Column('number', sa.Integer(), nullable=False),
sa.Column('message', sa.Text(), nullable=False),
sa.Column('commit', sa.String(length=255), nullable=False),
sa.Column('parent', sa.String(length=255), nullable=False),
sa.ForeignKeyConstraint(['change_key'], ['change.key'], ),
sa.PrimaryKeyConstraint('key')
)
op.create_index(op.f('ix_revision_change_key'), 'revision', ['change_key'], unique=False)
op.create_index(op.f('ix_revision_number'), 'revision', ['number'], unique=False)
op.create_table('label',
sa.Column('key', sa.Integer(), nullable=False, quote=True),
sa.Column('change_key', sa.Integer(), nullable=True),
sa.Column('category', sa.String(length=255), nullable=False),
sa.Column('value', sa.Integer(), nullable=False),
sa.Column('description', sa.String(length=255), nullable=False),
sa.ForeignKeyConstraint(['change_key'], ['change.key'], ),
sa.PrimaryKeyConstraint('key')
)
op.create_index(op.f('ix_label_change_key'), 'label', ['change_key'], unique=False)
op.create_table('permitted_label',
sa.Column('key', sa.Integer(), nullable=False, quote=True),
sa.Column('change_key', sa.Integer(), nullable=True),
sa.Column('category', sa.String(length=255), nullable=False),
sa.Column('value', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['change_key'], ['change.key'], ),
sa.PrimaryKeyConstraint('key')
)
op.create_index(op.f('ix_permitted_label_change_key'), 'permitted_label', ['change_key'], unique=False)
op.create_table('comment',
sa.Column('key', sa.Integer(), nullable=False, quote=True),
sa.Column('revision_key', sa.Integer(), nullable=True),
sa.Column('id', sa.String(length=255), nullable=True),
sa.Column('in_reply_to', sa.String(length=255), nullable=True),
sa.Column('created', sa.DateTime(), nullable=False),
sa.Column('name', sa.String(length=255), nullable=True),
sa.Column('file', sa.Text(), nullable=False),
sa.Column('parent', sa.Boolean(), nullable=False),
sa.Column('line', sa.Integer(), nullable=True),
sa.Column('message', sa.Text(), nullable=False),
sa.Column('pending', sa.Boolean(), nullable=False),
sa.ForeignKeyConstraint(['revision_key'], ['revision.key'], ),
sa.PrimaryKeyConstraint('key')
)
op.create_index(op.f('ix_comment_created'), 'comment', ['created'], unique=False)
op.create_index(op.f('ix_comment_id'), 'comment', ['id'], unique=False)
op.create_index(op.f('ix_comment_pending'), 'comment', ['pending'], unique=False)
op.create_index(op.f('ix_comment_revision_key'), 'comment', ['revision_key'], unique=False)
op.create_table('message',
sa.Column('key', sa.Integer(), nullable=False, quote=True),
sa.Column('revision_key', sa.Integer(), nullable=True),
sa.Column('id', sa.String(length=255), nullable=True),
sa.Column('created', sa.DateTime(), nullable=False),
sa.Column('name', sa.String(length=255), nullable=True),
sa.Column('message', sa.Text(), nullable=False),
sa.Column('pending', sa.Boolean(), nullable=False),
sa.ForeignKeyConstraint(['revision_key'], ['revision.key'], ),
sa.PrimaryKeyConstraint('key')
)
op.create_index(op.f('ix_message_created'), 'message', ['created'], unique=False)
op.create_index(op.f('ix_message_id'), 'message', ['id'], unique=False)
op.create_index(op.f('ix_message_pending'), 'message', ['pending'], unique=False)
op.create_index(op.f('ix_message_revision_key'), 'message', ['revision_key'], unique=False)
### end Alembic commands ###
def downgrade():
### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f('ix_message_revision_key'), table_name='message')
op.drop_index(op.f('ix_message_pending'), table_name='message')
op.drop_index(op.f('ix_message_id'), table_name='message')
op.drop_index(op.f('ix_message_created'), table_name='message')
op.drop_table('message')
op.drop_index(op.f('ix_comment_revision_key'), table_name='comment')
op.drop_index(op.f('ix_comment_pending'), table_name='comment')
op.drop_index(op.f('ix_comment_id'), table_name='comment')
op.drop_index(op.f('ix_comment_created'), table_name='comment')
op.drop_table('comment')
op.drop_index(op.f('ix_permitted_label_change_key'), table_name='permitted_label')
op.drop_table('permitted_label')
op.drop_index(op.f('ix_label_change_key'), table_name='label')
op.drop_table('label')
op.drop_index(op.f('ix_revision_number'), table_name='revision')
op.drop_index(op.f('ix_revision_change_key'), table_name='revision')
op.drop_table('revision')
op.drop_index(op.f('ix_approval_pending'), table_name='approval')
op.drop_index(op.f('ix_approval_change_key'), table_name='approval')
op.drop_table('approval')
op.drop_index(op.f('ix_change_updated'), table_name='change')
op.drop_index(op.f('ix_change_topic'), table_name='change')
op.drop_index(op.f('ix_change_status'), table_name='change')
op.drop_index(op.f('ix_change_reviewed'), table_name='change')
op.drop_index(op.f('ix_change_project_key'), table_name='change')
op.drop_index(op.f('ix_change_owner'), table_name='change')
op.drop_index(op.f('ix_change_number'), table_name='change')
op.drop_index(op.f('ix_change_id'), table_name='change')
op.drop_index(op.f('ix_change_hidden'), table_name='change')
op.drop_index(op.f('ix_change_created'), table_name='change')
op.drop_index(op.f('ix_change_change_id'), table_name='change')
op.drop_index(op.f('ix_change_branch'), table_name='change')
op.drop_table('change')
op.drop_index(op.f('ix_project_subscribed'), table_name='project')
op.drop_index(op.f('ix_project_name'), table_name='project')
op.drop_table('project')
### end Alembic commands ###

View File

@ -1,66 +0,0 @@
"""add pending actions
Revision ID: 46b175bfa277
Revises: 3d429503a29a
Create Date: 2014-08-31 09:20:11.789330
"""
# revision identifiers, used by Alembic.
revision = '46b175bfa277'
down_revision = '3d429503a29a'
import warnings
from alembic import op
import sqlalchemy as sa
from gertty.dbsupport import sqlite_alter_columns
def upgrade():
op.create_table('branch',
sa.Column('key', sa.Integer(), nullable=False),
sa.Column('project_key', sa.Integer(), sa.ForeignKey('project.key'), index=True, nullable=False),
sa.Column('name', sa.String(length=255), nullable=False),
sa.PrimaryKeyConstraint('key')
)
op.create_table('pending_cherry_pick',
sa.Column('key', sa.Integer(), nullable=False),
sa.Column('revision_key', sa.Integer(), sa.ForeignKey('revision.key'), index=True, nullable=False),
sa.Column('branch', sa.String(length=255), nullable=False),
sa.Column('message', sa.Text(), nullable=False),
sa.PrimaryKeyConstraint('key')
)
with warnings.catch_warnings():
warnings.simplefilter("ignore")
op.add_column('change', sa.Column('pending_rebase', sa.Boolean()))
op.add_column('change', sa.Column('pending_topic', sa.Boolean()))
op.add_column('change', sa.Column('pending_status', sa.Boolean()))
op.add_column('change', sa.Column('pending_status_message', sa.Text()))
op.add_column('revision', sa.Column('pending_message', sa.Boolean()))
connection = op.get_bind()
change = sa.sql.table('change',
sa.sql.column('pending_rebase', sa.Boolean()),
sa.sql.column('pending_topic', sa.Boolean()),
sa.sql.column('pending_status', sa.Boolean()))
connection.execute(change.update().values({'pending_rebase':False,
'pending_topic':False,
'pending_status':False}))
revision = sa.sql.table('revision',
sa.sql.column('pending_message', sa.Boolean()))
connection.execute(revision.update().values({'pending_message':False}))
sqlite_alter_columns('change', [
sa.Column('pending_rebase', sa.Boolean(), index=True, nullable=False),
sa.Column('pending_topic', sa.Boolean(), index=True, nullable=False),
sa.Column('pending_status', sa.Boolean(), index=True, nullable=False),
])
sqlite_alter_columns('revision', [
sa.Column('pending_message', sa.Boolean(), index=True, nullable=False),
])
def downgrade():
pass

View File

@ -1,41 +0,0 @@
"""add starred
Revision ID: 4a802b741d2f
Revises: 312cd5a9f878
Create Date: 2015-02-12 18:10:19.187733
"""
# revision identifiers, used by Alembic.
revision = '4a802b741d2f'
down_revision = '312cd5a9f878'
import warnings
from alembic import op
import sqlalchemy as sa
from gertty.dbsupport import sqlite_alter_columns
def upgrade():
with warnings.catch_warnings():
warnings.simplefilter("ignore")
op.add_column('change', sa.Column('starred', sa.Boolean()))
op.add_column('change', sa.Column('pending_starred', sa.Boolean()))
connection = op.get_bind()
change = sa.sql.table('change',
sa.sql.column('starred', sa.Boolean()),
sa.sql.column('pending_starred', sa.Boolean()))
connection.execute(change.update().values({'starred':False,
'pending_starred':False}))
sqlite_alter_columns('change', [
sa.Column('starred', sa.Boolean(), index=True, nullable=False),
sa.Column('pending_starred', sa.Boolean(), index=True, nullable=False),
])
def downgrade():
pass

View File

@ -1,73 +0,0 @@
"""add account table
Revision ID: 4cc9c46f9d8b
Revises: 725816dc500
Create Date: 2014-07-23 16:01:47.462597
"""
# revision identifiers, used by Alembic.
revision = '4cc9c46f9d8b'
down_revision = '725816dc500'
import warnings
from alembic import op
import sqlalchemy as sa
from gertty.dbsupport import sqlite_alter_columns, sqlite_drop_columns
def upgrade():
sqlite_drop_columns('message', ['name'])
sqlite_drop_columns('comment', ['name'])
sqlite_drop_columns('approval', ['name'])
sqlite_drop_columns('change', ['owner'])
op.create_table('account',
sa.Column('key', sa.Integer(), nullable=False),
sa.Column('id', sa.Integer(), index=True, unique=True, nullable=False),
sa.Column('name', sa.String(length=255)),
sa.Column('username', sa.String(length=255)),
sa.Column('email', sa.String(length=255)),
sa.PrimaryKeyConstraint('key')
)
op.create_index(op.f('ix_account_name'), 'account', ['name'], unique=True)
op.create_index(op.f('ix_account_username'), 'account', ['name'], unique=True)
op.create_index(op.f('ix_account_email'), 'account', ['name'], unique=True)
with warnings.catch_warnings():
warnings.simplefilter("ignore")
op.add_column('message', sa.Column('account_key', sa.Integer()))
op.add_column('comment', sa.Column('account_key', sa.Integer()))
op.add_column('approval', sa.Column('account_key', sa.Integer()))
op.add_column('change', sa.Column('account_key', sa.Integer()))
sqlite_alter_columns('message', [
sa.Column('account_key', sa.Integer(), sa.ForeignKey('account.key'))
])
sqlite_alter_columns('comment', [
sa.Column('account_key', sa.Integer(), sa.ForeignKey('account.key'))
])
sqlite_alter_columns('approval', [
sa.Column('account_key', sa.Integer(), sa.ForeignKey('account.key'))
])
sqlite_alter_columns('change', [
sa.Column('account_key', sa.Integer(), sa.ForeignKey('account.key'))
])
op.create_index(op.f('ix_message_account_key'), 'message', ['account_key'], unique=False)
op.create_index(op.f('ix_comment_account_key'), 'comment', ['account_key'], unique=False)
op.create_index(op.f('ix_approval_account_key'), 'approval', ['account_key'], unique=False)
op.create_index(op.f('ix_change_account_key'), 'change', ['account_key'], unique=False)
connection = op.get_bind()
project = sa.sql.table('project', sa.sql.column('updated', sa.DateTime))
connection.execute(project.update().values({'updated':None}))
approval = sa.sql.table('approval', sa.sql.column('pending'))
connection.execute(approval.delete().where(approval.c.pending==False))
def downgrade():
pass

View File

@ -1,94 +0,0 @@
"""add files table
Revision ID: 50344aecd1c2
Revises: 1bb187bcd401
Create Date: 2015-04-13 08:08:08.682803
"""
# revision identifiers, used by Alembic.
revision = '50344aecd1c2'
down_revision = '1bb187bcd401'
import re
import sys
from alembic import op, context
import sqlalchemy as sa
import git.exc
import gertty.db
import gertty.gitrepo
def upgrade():
op.create_table('file',
sa.Column('key', sa.Integer(), nullable=False),
sa.Column('revision_key', sa.Integer(), nullable=False, index=True),
sa.Column('path', sa.Text(), nullable=False, index=True),
sa.Column('old_path', sa.Text(), index=True),
sa.Column('status', sa.String(length=1)),
sa.Column('inserted', sa.Integer()),
sa.Column('deleted', sa.Integer()),
sa.PrimaryKeyConstraint('key')
)
pathre = re.compile('((.*?)\{|^)(.*?) => (.*?)(\}(.*)|$)')
insert = sa.text('insert into file (key, revision_key, path, old_path, status, inserted, deleted) '
' values (NULL, :revision_key, :path, :old_path, :status, :inserted, :deleted)')
conn = op.get_bind()
countres = conn.execute('select count(*) from revision')
revisions = countres.fetchone()[0]
if revisions > 50:
print('')
print('Adding support for searching for changes by file modified. '
'This may take a while.')
qres = conn.execute('select p.name, c.number, c.status, r.key, r.number, r."commit", r.parent from project p, change c, revision r '
'where r.change_key=c.key and c.project_key=p.key order by p.name')
count = 0
for (pname, cnumber, cstatus, rkey, rnumber, commit, parent) in qres.fetchall():
count += 1
sys.stdout.write('Diffstat revision %s / %s\r' % (count, revisions))
sys.stdout.flush()
ires = conn.execute(insert, revision_key=rkey, path='/COMMIT_MSG', old_path=None,
status=None, inserted=None, deleted=None)
repo = gertty.gitrepo.get_repo(pname, context.config.gertty_app.config)
try:
stats = repo.diffstat(parent, commit)
except git.exc.GitCommandError:
# Probably a missing commit
if cstatus not in ['MERGED', 'ABANDONED']:
print("Unable to examine diff for %s %s change %s,%s" % (cstatus, pname, cnumber, rnumber))
continue
for stat in stats:
try:
(added, removed, path) = stat
except ValueError:
if cstatus not in ['MERGED', 'ABANDONED']:
print("Empty diffstat for %s %s change %s,%s" % (cstatus, pname, cnumber, rnumber))
m = pathre.match(path)
status = gertty.db.File.STATUS_MODIFIED
old_path = None
if m:
status = gertty.db.File.STATUS_RENAMED
pre = m.group(2) or ''
post = m.group(6) or ''
old_path = pre+m.group(3)+post
path = pre+m.group(4)+post
try:
added = int(added)
except ValueError:
added = None
try:
removed = int(removed)
except ValueError:
removed = None
conn.execute(insert, revision_key=rkey, path=path, old_path=old_path,
status=status, inserted=added, deleted=removed)
print('')
def downgrade():
pass

View File

@ -1,26 +0,0 @@
"""Increase status field width
Revision ID: 56e48a4a064a
Revises: 44402069e137
Create Date: 2014-05-05 11:49:42.133569
"""
# revision identifiers, used by Alembic.
revision = '56e48a4a064a'
down_revision = '44402069e137'
import sqlalchemy as sa
from gertty.dbsupport import sqlite_alter_columns
def upgrade():
sqlite_alter_columns('change', [
sa.Column('status', sa.String(16), index=True, nullable=False)
])
def downgrade():
sqlite_alter_columns('change', [
sa.Column('status', sa.String(8), index=True, nullable=False)
])

View File

@ -1,41 +0,0 @@
"""Add fetch ref column
Revision ID: 725816dc500
Revises: 38104b4c1b84
Create Date: 2014-05-31 14:51:08.078616
"""
# revision identifiers, used by Alembic.
revision = '725816dc500'
down_revision = '38104b4c1b84'
import warnings
from alembic import op
import sqlalchemy as sa
from gertty.dbsupport import sqlite_alter_columns
def upgrade():
with warnings.catch_warnings():
warnings.simplefilter("ignore")
op.add_column('revision', sa.Column('fetch_auth', sa.Boolean()))
op.add_column('revision', sa.Column('fetch_ref', sa.String(length=255)))
conn = op.get_bind()
res = conn.execute('select r.key, r.number, c.number from revision r, "change" c where r.change_key=c.key')
for (rkey, rnumber, cnumber) in res.fetchall():
q = sa.text('update revision set fetch_auth=:auth, fetch_ref=:ref where "key"=:key')
ref = 'refs/changes/%s/%s/%s' % (str(cnumber)[-2:], cnumber, rnumber)
res = conn.execute(q, key=rkey, ref=ref, auth=False)
sqlite_alter_columns('revision', [
sa.Column('fetch_auth', sa.Boolean(), nullable=False),
sa.Column('fetch_ref', sa.String(length=255), nullable=False)
])
def downgrade():
op.drop_column('revision', 'fetch_auth')
op.drop_column('revision', 'fetch_ref')

View File

@ -1,37 +0,0 @@
"""add change.outdated
Revision ID: 7ef7dfa2ca3a
Revises: 37a702b7f58e
Create Date: 2016-08-09 08:59:04.441926
"""
# revision identifiers, used by Alembic.
revision = '7ef7dfa2ca3a'
down_revision = '37a702b7f58e'
import warnings
from alembic import op
import sqlalchemy as sa
from gertty.dbsupport import sqlite_alter_columns
def upgrade():
with warnings.catch_warnings():
warnings.simplefilter("ignore")
op.add_column('change', sa.Column('outdated', sa.Boolean()))
connection = op.get_bind()
change = sa.sql.table('change',
sa.sql.column('outdated', sa.Boolean()))
connection.execute(change.update().values({'outdated':False}))
sqlite_alter_columns('change', [
sa.Column('outdated', sa.Boolean(), index=True, nullable=False),
])
def downgrade():
pass

View File

@ -1,69 +0,0 @@
# Copyright 2015 Christoph Gysin <christoph.gysin@gmail.com>
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import logging
import requests
from six.moves.urllib import parse as urlparse
class FormAuth(requests.auth.AuthBase):
def __init__(self, username, password):
self.username = username
self.password = password
self.log = logging.getLogger('gertty.auth')
def _retry_using_form_auth(self, response, args):
adapter = requests.adapters.HTTPAdapter()