16 changed files with 2607 additions and 0 deletions
@ -0,0 +1,202 @@
|
||||
|
||||
Apache License |
||||
Version 2.0, January 2004 |
||||
http://www.apache.org/licenses/ |
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION |
||||
|
||||
1. Definitions. |
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction, |
||||
and distribution as defined by Sections 1 through 9 of this document. |
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by |
||||
the copyright owner that is granting the License. |
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all |
||||
other entities that control, are controlled by, or are under common |
||||
control with that entity. For the purposes of this definition, |
||||
"control" means (i) the power, direct or indirect, to cause the |
||||
direction or management of such entity, whether by contract or |
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the |
||||
outstanding shares, or (iii) beneficial ownership of such entity. |
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity |
||||
exercising permissions granted by this License. |
||||
|
||||
"Source" form shall mean the preferred form for making modifications, |
||||
including but not limited to software source code, documentation |
||||
source, and configuration files. |
||||
|
||||
"Object" form shall mean any form resulting from mechanical |
||||
transformation or translation of a Source form, including but |
||||
not limited to compiled object code, generated documentation, |
||||
and conversions to other media types. |
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or |
||||
Object form, made available under the License, as indicated by a |
||||
copyright notice that is included in or attached to the work |
||||
(an example is provided in the Appendix below). |
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object |
||||
form, that is based on (or derived from) the Work and for which the |
||||
editorial revisions, annotations, elaborations, or other modifications |
||||
represent, as a whole, an original work of authorship. For the purposes |
||||
of this License, Derivative Works shall not include works that remain |
||||
separable from, or merely link (or bind by name) to the interfaces of, |
||||
the Work and Derivative Works thereof. |
||||
|
||||
"Contribution" shall mean any work of authorship, including |
||||
the original version of the Work and any modifications or additions |
||||
to that Work or Derivative Works thereof, that is intentionally |
||||
submitted to Licensor for inclusion in the Work by the copyright owner |
||||
or by an individual or Legal Entity authorized to submit on behalf of |
||||
the copyright owner. For the purposes of this definition, "submitted" |
||||
means any form of electronic, verbal, or written communication sent |
||||
to the Licensor or its representatives, including but not limited to |
||||
communication on electronic mailing lists, source code control systems, |
||||
and issue tracking systems that are managed by, or on behalf of, the |
||||
Licensor for the purpose of discussing and improving the Work, but |
||||
excluding communication that is conspicuously marked or otherwise |
||||
designated in writing by the copyright owner as "Not a Contribution." |
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity |
||||
on behalf of whom a Contribution has been received by Licensor and |
||||
subsequently incorporated within the Work. |
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of |
||||
this License, each Contributor hereby grants to You a perpetual, |
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable |
||||
copyright license to reproduce, prepare Derivative Works of, |
||||
publicly display, publicly perform, sublicense, and distribute the |
||||
Work and such Derivative Works in Source or Object form. |
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of |
||||
this License, each Contributor hereby grants to You a perpetual, |
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable |
||||
(except as stated in this section) patent license to make, have made, |
||||
use, offer to sell, sell, import, and otherwise transfer the Work, |
||||
where such license applies only to those patent claims licensable |
||||
by such Contributor that are necessarily infringed by their |
||||
Contribution(s) alone or by combination of their Contribution(s) |
||||
with the Work to which such Contribution(s) was submitted. If You |
||||
institute patent litigation against any entity (including a |
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work |
||||
or a Contribution incorporated within the Work constitutes direct |
||||
or contributory patent infringement, then any patent licenses |
||||
granted to You under this License for that Work shall terminate |
||||
as of the date such litigation is filed. |
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the |
||||
Work or Derivative Works thereof in any medium, with or without |
||||
modifications, and in Source or Object form, provided that You |
||||
meet the following conditions: |
||||
|
||||
(a) You must give any other recipients of the Work or |
||||
Derivative Works a copy of this License; and |
||||
|
||||
(b) You must cause any modified files to carry prominent notices |
||||
stating that You changed the files; and |
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works |
||||
that You distribute, all copyright, patent, trademark, and |
||||
attribution notices from the Source form of the Work, |
||||
excluding those notices that do not pertain to any part of |
||||
the Derivative Works; and |
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its |
||||
distribution, then any Derivative Works that You distribute must |
||||
include a readable copy of the attribution notices contained |
||||
within such NOTICE file, excluding those notices that do not |
||||
pertain to any part of the Derivative Works, in at least one |
||||
of the following places: within a NOTICE text file distributed |
||||
as part of the Derivative Works; within the Source form or |
||||
documentation, if provided along with the Derivative Works; or, |
||||
within a display generated by the Derivative Works, if and |
||||
wherever such third-party notices normally appear. The contents |
||||
of the NOTICE file are for informational purposes only and |
||||
do not modify the License. You may add Your own attribution |
||||
notices within Derivative Works that You distribute, alongside |
||||
or as an addendum to the NOTICE text from the Work, provided |
||||
that such additional attribution notices cannot be construed |
||||
as modifying the License. |
||||
|
||||
You may add Your own copyright statement to Your modifications and |
||||
may provide additional or different license terms and conditions |
||||
for use, reproduction, or distribution of Your modifications, or |
||||
for any such Derivative Works as a whole, provided Your use, |
||||
reproduction, and distribution of the Work otherwise complies with |
||||
the conditions stated in this License. |
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise, |
||||
any Contribution intentionally submitted for inclusion in the Work |
||||
by You to the Licensor shall be under the terms and conditions of |
||||
this License, without any additional terms or conditions. |
||||
Notwithstanding the above, nothing herein shall supersede or modify |
||||
the terms of any separate license agreement you may have executed |
||||
with Licensor regarding such Contributions. |
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade |
||||
names, trademarks, service marks, or product names of the Licensor, |
||||
except as required for reasonable and customary use in describing the |
||||
origin of the Work and reproducing the content of the NOTICE file. |
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or |
||||
agreed to in writing, Licensor provides the Work (and each |
||||
Contributor provides its Contributions) on an "AS IS" BASIS, |
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or |
||||
implied, including, without limitation, any warranties or conditions |
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A |
||||
PARTICULAR PURPOSE. You are solely responsible for determining the |
||||
appropriateness of using or redistributing the Work and assume any |
||||
risks associated with Your exercise of permissions under this License. |
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory, |
||||
whether in tort (including negligence), contract, or otherwise, |
||||
unless required by applicable law (such as deliberate and grossly |
||||
negligent acts) or agreed to in writing, shall any Contributor be |
||||
liable to You for damages, including any direct, indirect, special, |
||||
incidental, or consequential damages of any character arising as a |
||||
result of this License or out of the use or inability to use the |
||||
Work (including but not limited to damages for loss of goodwill, |
||||
work stoppage, computer failure or malfunction, or any and all |
||||
other commercial damages or losses), even if such Contributor |
||||
has been advised of the possibility of such damages. |
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing |
||||
the Work or Derivative Works thereof, You may choose to offer, |
||||
and charge a fee for, acceptance of support, warranty, indemnity, |
||||
or other liability obligations and/or rights consistent with this |
||||
License. However, in accepting such obligations, You may act only |
||||
on Your own behalf and on Your sole responsibility, not on behalf |
||||
of any other Contributor, and only if You agree to indemnify, |
||||
defend, and hold each Contributor harmless for any liability |
||||
incurred by, or claims asserted against, such Contributor by reason |
||||
of your accepting any such warranty or additional liability. |
||||
|
||||
END OF TERMS AND CONDITIONS |
||||
|
||||
APPENDIX: How to apply the Apache License to your work. |
||||
|
||||
To apply the Apache License to your work, attach the following |
||||
boilerplate notice, with the fields enclosed by brackets "[]" |
||||
replaced with your own identifying information. (Don't include |
||||
the brackets!) The text should be enclosed in the appropriate |
||||
comment syntax for the file format. We also recommend that a |
||||
file or class name and description of purpose be included on the |
||||
same "printed page" as the copyright notice for easier |
||||
identification within third-party archives. |
||||
|
||||
Copyright [yyyy] [name of copyright owner] |
||||
|
||||
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. |
@ -0,0 +1,111 @@
|
||||
Gertty |
||||
====== |
||||
|
||||
Gertty is a console-based interface to the Gerrit Code Review 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. |
||||
|
||||
* 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. |
||||
|
||||
* 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. |
||||
|
||||
Usage |
||||
----- |
||||
|
||||
Create a file at ``~/.gerttyrc`` with the following contents:: |
||||
|
||||
[gerrit] |
||||
url=https://review.example.org/ |
||||
username=<gerrit username> |
||||
password=<gerrit password> |
||||
git_root=~/git/ |
||||
|
||||
You can generate or retrieve your Gerrit password by navigating to |
||||
Settings, then HTTP Password. Set ``git_root`` to a directory where |
||||
Gertty should find or clone git repositories for your projects. |
||||
|
||||
If your Gerrit uses a self-signed certificate, you can add:: |
||||
|
||||
verify_ssl=False |
||||
|
||||
To the section. |
||||
|
||||
The config file is designed to support multiple Gerrit instances, but |
||||
currently, only the first one is used. |
||||
|
||||
After installing the requirements (listed in requirements.txt), you |
||||
should be able to simply run Gertty. You will need to start by |
||||
subscribing to some projects. Use 'l' to list all of the projects and |
||||
then 's' to subscribe to them. |
||||
|
||||
In general, pressing the F1 key will show help text on any screen, and |
||||
ESC will take you to the previous screen. |
||||
|
||||
To select text (e.g., to copy to the clipboard), hold Shift while |
||||
selecting the text. |
||||
|
||||
Philosophy |
||||
---------- |
||||
|
||||
Gertty 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. |
||||
|
||||
* 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 |
||||
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 |
||||
(88 or 256) may be used. |
||||
|
||||
* Keyboard navigation (with easy-to-remember commands) should be |
||||
considered the primary mode of interaction. Mouse interaction |
||||
should also be supported. |
||||
|
||||
* The navigation philosophy is a stack of screens, where each |
||||
selection pushes a new screen onto the stack, and ESC pops the |
||||
screen off. This makes sense when drilling down to a change from |
||||
lists, but also supports linking from change to change (via commit |
||||
messages or comments) and navigating back intuitive (it matches |
||||
expectations set by the web browsers). |
||||
|
||||
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` |
||||
|
||||
Bugs are handled at: https://storyboard.openstack.org/ |
||||
|
||||
Code reviews are handled by gerrit at: https://review.openstack.org |
||||
|
||||
Use `git review` to submit patches (after creating a gerrit account |
||||
that links to your launchpad account). Example:: |
||||
|
||||
# Do your commits |
||||
$ git review |
||||
# Enter your username if prompted |
@ -0,0 +1,46 @@
|
||||
# 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. |
||||
|
||||
import os |
||||
import ConfigParser |
||||
|
||||
|
||||
DEFAULT_CONFIG_PATH='~/.gerttyrc' |
||||
|
||||
class Config(object): |
||||
def __init__(self, server=None, path=DEFAULT_CONFIG_PATH): |
||||
self.path = os.path.expanduser(path) |
||||
self.config = ConfigParser.RawConfigParser() |
||||
self.config.read(self.path) |
||||
if server is None: |
||||
server = self.config.sections()[0] |
||||
self.server = server |
||||
self.url = self.config.get(server, 'url') |
||||
self.username = self.config.get(server, 'username') |
||||
self.password = self.config.get(server, 'password') |
||||
if self.config.has_option(server, 'verify_ssl'): |
||||
self.verify_ssl = self.config.getboolean(server, 'verify_ssl') |
||||
else: |
||||
self.verify_ssl = True |
||||
if not self.verify_ssl: |
||||
os.environ['GIT_SSL_NO_VERIFY']='true' |
||||
self.git_root = os.path.expanduser(self.config.get(server, 'git_root')) |
||||
if self.config.has_option(server, 'dburi'): |
||||
self.dburi = self.config.get(server, 'dburi') |
||||
else: |
||||
self.dburi = 'sqlite:///' + os.path.expanduser('~/.gertty.db') |
||||
if self.config.has_option(server, 'log_file'): |
||||
self.log_file = os.path.expanduser(self.config.get(server, 'log_file')) |
||||
else: |
||||
self.log_file = os.path.expanduser('~/.gertty.log') |
@ -0,0 +1,446 @@
|
||||
# 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. |
||||
|
||||
import sqlalchemy |
||||
from sqlalchemy import create_engine, MetaData, Table, Column, Integer, String, Boolean, DateTime, Text, select, func |
||||
from sqlalchemy.schema import ForeignKey |
||||
from sqlalchemy.orm import mapper, sessionmaker, relationship, column_property, scoped_session |
||||
from sqlalchemy.orm.session import Session |
||||
from sqlalchemy.sql.expression import and_ |
||||
|
||||
metadata = MetaData() |
||||
project_table = Table( |
||||
'project', metadata, |
||||
Column('key', Integer, primary_key=True), |
||||
Column('name', String(255), index=True, unique=True, nullable=False), |
||||
Column('subscribed', Boolean, index=True, default=False), |
||||
Column('description', Text, nullable=False, default=''), |
||||
) |
||||
change_table = Table( |
||||
'change', metadata, |
||||
Column('key', Integer, primary_key=True), |
||||
Column('project_key', Integer, ForeignKey("project.key"), index=True), |
||||
Column('id', String(255), index=True, unique=True, nullable=False), |
||||
Column('number', Integer, index=True, unique=True, nullable=False), |
||||
Column('branch', String(255), index=True, nullable=False), |
||||
Column('change_id', String(255), index=True, nullable=False), |
||||
Column('topic', String(255), index=True), |
||||
Column('owner', String(255), index=True), |
||||
Column('subject', Text, nullable=False), |
||||
Column('created', DateTime, index=True, nullable=False), |
||||
Column('updated', DateTime, index=True, nullable=False), |
||||
Column('status', String(8), index=True, nullable=False), |
||||
Column('hidden', Boolean, index=True, nullable=False), |
||||
Column('reviewed', Boolean, index=True, nullable=False), |
||||
) |
||||
revision_table = Table( |
||||
'revision', metadata, |
||||
Column('key', Integer, primary_key=True), |
||||
Column('change_key', Integer, ForeignKey("change.key"), index=True), |
||||
Column('number', Integer, index=True, nullable=False), |
||||
Column('message', Text, nullable=False), |
||||
Column('commit', String(255), nullable=False), |
||||
Column('parent', String(255), nullable=False), |
||||
) |
||||
message_table = Table( |
||||
'message', metadata, |
||||
Column('key', Integer, primary_key=True), |
||||
Column('revision_key', Integer, ForeignKey("revision.key"), index=True), |
||||
Column('id', String(255), index=True), #, unique=True, nullable=False), |
||||
Column('created', DateTime, index=True, nullable=False), |
||||
Column('name', String(255)), |
||||
Column('message', Text, nullable=False), |
||||
Column('pending', Boolean, index=True, nullable=False), |
||||
) |
||||
comment_table = Table( |
||||
'comment', metadata, |
||||
Column('key', Integer, primary_key=True), |
||||
Column('revision_key', Integer, ForeignKey("revision.key"), index=True), |
||||
Column('id', String(255), index=True), #, unique=True, nullable=False), |
||||
Column('in_reply_to', String(255)), |
||||
Column('created', DateTime, index=True, nullable=False), |
||||
Column('name', String(255)), |
||||
Column('file', Text, nullable=False), |
||||
Column('parent', Boolean, nullable=False), |
||||
Column('line', Integer), |
||||
Column('message', Text, nullable=False), |
||||
Column('pending', Boolean, index=True, nullable=False), |
||||
) |
||||
label_table = Table( |
||||
'label', metadata, |
||||
Column('key', Integer, primary_key=True), |
||||
Column('change_key', Integer, ForeignKey("change.key"), index=True), |
||||
Column('category', String(255), nullable=False), |
||||
Column('value', Integer, nullable=False), |
||||
Column('description', String(255), nullable=False), |
||||
) |
||||
permitted_label_table = Table( |
||||
'permitted_label', metadata, |
||||
Column('key', Integer, primary_key=True), |
||||
Column('change_key', Integer, ForeignKey("change.key"), index=True), |
||||
Column('category', String(255), nullable=False), |
||||
Column('value', Integer, nullable=False), |
||||
) |
||||
approval_table = Table( |
||||
'approval', metadata, |
||||
Column('key', Integer, primary_key=True), |
||||
Column('change_key', Integer, ForeignKey("change.key"), index=True), |
||||
Column('name', String(255)), |
||||
Column('category', String(255), nullable=False), |
||||
Column('value', Integer, nullable=False), |
||||
Column('pending', Boolean, index=True, nullable=False), |
||||
) |
||||
|
||||
|
||||
class Project(object): |
||||
def __init__(self, name, subscribed=False, description=''): |
||||
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 |
||||
|
||||
class Change(object): |
||||
def __init__(self, project, id, number, branch, change_id, |
||||
owner, subject, created, updated, status, |
||||
topic=False, hidden=False, reviewed=False): |
||||
self.project_key = project.key |
||||
self.id = id |
||||
self.number = number |
||||
self.branch = branch |
||||
self.change_id = change_id |
||||
self.topic = topic |
||||
self.owner = owner |
||||
self.subject = subject |
||||
self.created = created |
||||
self.updated = updated |
||||
self.status = status |
||||
self.hidden = hidden |
||||
self.reviewed = reviewed |
||||
|
||||
def getCategories(self): |
||||
categories = [] |
||||
for label in self.labels: |
||||
if label.category in categories: |
||||
continue |
||||
categories.append(label.category) |
||||
return categories |
||||
|
||||
def getMaxForCategory(self, category): |
||||
if not hasattr(self, '_approval_cache'): |
||||
self._updateApprovalCache() |
||||
return self._approval_cache.get(category, 0) |
||||
|
||||
def _updateApprovalCache(self): |
||||
cat_min = {} |
||||
cat_max = {} |
||||
cat_value = {} |
||||
for approval in self.approvals: |
||||
cur_min = cat_min.get(approval.category, 0) |
||||
cur_max = cat_max.get(approval.category, 0) |
||||
cur_min = min(approval.value, cur_min) |
||||
cur_max = max(approval.value, cur_max) |
||||
cat_min[approval.category] = cur_min |
||||
cat_max[approval.category] = cur_max |
||||
cur_value = cat_value.get(approval.category, 0) |
||||
if abs(cur_min) > abs(cur_value): |
||||
cur_value = cur_min |
||||
if abs(cur_max) > abs(cur_value): |
||||
cur_value = cur_max |
||||
cat_value[approval.category] = cur_value |
||||
self._approval_cache = cat_value |
||||
|
||||
def createRevision(self, *args, **kw): |
||||
session = Session.object_session(self) |
||||
args = [self] + list(args) |
||||
r = Revision(*args, **kw) |
||||
self.revisions.append(r) |
||||
session.add(r) |
||||
session.flush() |
||||
return r |
||||
|
||||
def createLabel(self, *args, **kw): |
||||
session = Session.object_session(self) |
||||
args = [self] + list(args) |
||||
l = Label(*args, **kw) |
||||
self.labels.append(l) |
||||
session.add(l) |
||||
session.flush() |
||||
return l |
||||
|
||||
def createApproval(self, *args, **kw): |
||||
session = Session.object_session(self) |
||||
args = [self] + list(args) |
||||
l = Approval(*args, **kw) |
||||
self.approvals.append(l) |
||||
session.add(l) |
||||
session.flush() |
||||
return l |
||||
|
||||
def createPermittedLabel(self, *args, **kw): |
||||
session = Session.object_session(self) |
||||
args = [self] + list(args) |
||||
l = PermittedLabel(*args, **kw) |
||||
self.permitted_labels.append(l) |
||||
session.add(l) |
||||
session.flush() |
||||
return l |
||||
|
||||
class Revision(object): |
||||
def __init__(self, change, number, message, commit, parent): |
||||
self.change_key = change.key |
||||
self.number = number |
||||
self.message = message |
||||
self.commit = commit |
||||
self.parent = parent |
||||
|
||||
def createMessage(self, *args, **kw): |
||||
session = Session.object_session(self) |
||||
args = [self] + list(args) |
||||
m = Message(*args, **kw) |
||||
self.messages.append(m) |
||||
session.add(m) |
||||
session.flush() |
||||
return m |
||||
|
||||
def createComment(self, *args, **kw): |
||||
session = Session.object_session(self) |
||||
args = [self] + list(args) |
||||
c = Comment(*args, **kw) |
||||
self.comments.append(c) |
||||
session.add(c) |
||||
session.flush() |
||||
return c |
||||
|
||||
class Message(object): |
||||
def __init__(self, revision, id, created, name, message, pending=False): |
||||
self.revision_key = revision.key |
||||
self.id = id |
||||
self.created = created |
||||
self.name = name |
||||
self.message = message |
||||
self.pending = pending |
||||
|
||||
class Comment(object): |
||||
def __init__(self, revision, id, in_reply_to, created, name, file, parent, line, message, pending=False): |
||||
self.revision_key = revision.key |
||||
self.id = id |
||||
self.in_reply_to = in_reply_to |
||||
self.created = created |
||||
self.name = name |
||||
self.file = file |
||||
self.parent = parent |
||||
self.line = line |
||||
self.message = message |
||||
self.pending = pending |
||||
|
||||
class Label(object): |
||||
def __init__(self, change, category, value, description): |
||||
self.change_key = change.key |
||||
self.category = category |
||||
self.value = value |
||||
self.description = description |
||||
|
||||
class PermittedLabel(object): |
||||
def __init__(self, change, category, value): |
||||
self.change_key = change.key |
||||
self.category = category |
||||
self.value = value |
||||
|
||||
class Approval(object): |
||||
def __init__(self, change, name, category, value, pending=False): |
||||
self.change_key = change.key |
||||
self.name = name |
||||
self.category = category |
||||
self.value = value |
||||
self.pending = pending |
||||
|
||||
mapper(Project, project_table, properties=dict( |
||||
changes=relationship(Change, backref='project', |
||||
order_by=change_table.c.number), |
||||
unreviewed_changes=relationship(Change, |
||||
primaryjoin=and_(project_table.c.key==change_table.c.project_key, |
||||
change_table.c.hidden==False, |
||||
change_table.c.reviewed==False), |
||||
order_by=change_table.c.number, |
||||
), |
||||
reviewed_changes=relationship(Change, |
||||
primaryjoin=and_(project_table.c.key==change_table.c.project_key, |
||||
change_table.c.hidden==False, |
||||
change_table.c.reviewed==True), |
||||
order_by=change_table.c.number, |
||||
), |
||||
updated = column_property( |
||||
select([func.max(change_table.c.updated)]).where( |
||||
change_table.c.project_key==project_table.c.key) |
||||
), |
||||
)) |
||||
mapper(Change, change_table, properties=dict( |
||||
revisions=relationship(Revision, backref='change', |
||||
order_by=revision_table.c.number), |
||||
messages=relationship(Message, |
||||
secondary=revision_table, |
||||
order_by=message_table.c.created), |
||||
labels=relationship(Label, backref='change', order_by=(label_table.c.category, |
||||
label_table.c.value)), |
||||
permitted_labels=relationship(PermittedLabel, backref='change', |
||||
order_by=(permitted_label_table.c.category, |
||||
permitted_label_table.c.value)), |
||||
approvals=relationship(Approval, backref='change', order_by=(approval_table.c.category, |
||||
approval_table.c.value)), |
||||
pending_approvals=relationship(Approval, |
||||
primaryjoin=and_(change_table.c.key==approval_table.c.change_key, |
||||
approval_table.c.pending==True), |
||||
order_by=(approval_table.c.category, |
||||
approval_table.c.value)) |
||||
)) |
||||
mapper(Revision, revision_table, properties=dict( |
||||
messages=relationship(Message, backref='revision'), |
||||
comments=relationship(Comment, backref='revision', |
||||
order_by=(comment_table.c.line, |
||||
comment_table.c.created)), |
||||
pending_comments=relationship(Comment, |
||||
primaryjoin=and_(revision_table.c.key==comment_table.c.revision_key, |
||||
comment_table.c.pending==True), |
||||
order_by=(comment_table.c.line, |
||||
comment_table.c.created)), |
||||
)) |
||||
mapper(Message, message_table) |
||||
mapper(Comment, comment_table) |
||||
mapper(Label, label_table) |
||||
mapper(PermittedLabel, permitted_label_table) |
||||
mapper(Approval, approval_table) |
||||
|
||||
class Database(object): |
||||
def __init__(self, app): |
||||
self.app = app |
||||
self.engine = create_engine(self.app.config.dburi) |
||||
metadata.create_all(self.engine) |
||||
self.session_factory = sessionmaker(bind=self.engine) |
||||
self.session = scoped_session(self.session_factory) |
||||
|
||||
def getSession(self): |
||||
return DatabaseSession(self.session) |
||||
|
||||
class DatabaseSession(object): |
||||
def __init__(self, session): |
||||
self.session = session |
||||
|
||||
def __enter__(self): |
||||
return self |
||||
|
||||
def __exit__(self, etype, value, tb): |
||||
if etype: |
||||
self.session().rollback() |
||||
else: |
||||
self.session().commit() |
||||
self.session().close() |
||||
self.session = None |
||||
|
||||
def abort(self): |
||||
self.session().rollback() |
||||
|
||||
def commit(self): |
||||
self.session().commit() |
||||
|
||||
def delete(self, obj): |
||||
self.session().delete(obj) |
||||
|
||||
def getProjects(self, subscribed=False): |
||||
if subscribed: |
||||
return self.session().query(Project).filter_by(subscribed=subscribed).order_by(Project.name).all() |
||||
else: |
||||
return self.session().query(Project).order_by(Project.name).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 getChange(self, key): |
||||
try: |
||||
return self.session().query(Change).filter_by(key=key).one() |
||||
except sqlalchemy.orm.exc.NoResultFound: |
||||
return None |
||||
|
||||
def getChangeByID(self, id): |
||||
try: |
||||
return self.session().query(Change).filter_by(id=id).one() |
||||
except sqlalchemy.orm.exc.NoResultFound: |
||||
return None |
||||
|
||||
def getRevision(self, key): |
||||
try: |
||||
return self.session().query(Revision).filter_by(key=key).one() |
||||
except sqlalchemy.orm.exc.NoResultFound: |
||||
return None |
||||
|
||||
def getRevisionByCommit(self, commit): |
||||
try: |
||||
return self.session().query(Revision).filter_by(commit=commit).one() |
||||
except sqlalchemy.orm.exc.NoResultFound: |
||||
return None |
||||
|
||||
def getRevisionByNumber(self, change, number): |
||||
try: |
||||
return self.session().query(Revision).filter_by(change_key=change.key, number=number).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 getMessage(self, key): |
||||
try: |
||||
return self.session().query(Message).filter_by(key=key).one() |
||||
except sqlalchemy.orm.exc.NoResultFound: |
||||
return None |
||||
|
||||
def getMessageByID(self, id): |
||||
try: |
||||
return self.session().query(Message).filter_by(id=id).one() |
||||
except sqlalchemy.orm.exc.NoResultFound: |
||||
return None |
||||
|
||||
def getPendingMessages(self): |
||||
return self.session().query(Message).filter_by(pending=True).all() |
||||
|
||||
def createProject(self, *args, **kw): |
||||
o = Project(*args, **kw) |
||||
self.session().add(o) |
||||
self.session().flush() |
||||
return o |
@ -0,0 +1,186 @@
|
||||
# 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. |
||||
|
||||
import argparse |
||||
import logging |
||||
import os |
||||
import sys |
||||
import threading |
||||
|
||||
import urwid |
||||
|
||||
import db |
||||
import config |
||||
import gitrepo |
||||
import mywid |
||||
import sync |
||||
import view.project_list |
||||
|
||||
palette=[('reversed', 'default,standout', ''), |
||||
('header', 'white,bold', 'dark blue'), |
||||
('error', 'light red', 'dark blue'), |
||||
('table-header', 'white,bold', ''), |
||||
# Diff |
||||
('removed-line', 'dark red', ''), |
||||
('removed-word', 'light red', ''), |
||||
('added-line', 'dark green', ''), |
||||
('added-word', 'light green', ''), |
||||
('nonexistent', 'default', ''), |
||||
('reversed-removed-line', 'dark red,standout', ''), |
||||
('reversed-removed-word', 'light red,standout', ''), |
||||
('reversed-added-line', 'dark green,standout', ''), |
||||
('reversed-added-word', 'light green,standout', ''), |
||||
('reversed-nonexistent', 'default,standout', ''), |
||||
('draft-comment', 'default', 'dark gray'), |
||||
('comment', 'white', 'dark gray'), |
||||
# Change view |
||||
('change-data', 'light cyan', ''), |
||||
('change-header', 'light blue', ''), |
||||
('revision-name', 'light blue', ''), |
||||
('revision-commit', 'dark blue', ''), |
||||
('revision-drafts', 'dark red', ''), |
||||
('reversed-revision-name', 'light blue,standout', ''), |
||||
('reversed-revision-commit', 'dark blue,standout', ''), |
||||
('reversed-revision-drafts', 'dark red,standout', ''), |
||||
('change-message-name', 'light blue', ''), |
||||
('change-message-header', 'dark blue', ''), |
||||
# project list |
||||
('unreviewed-project', 'white', ''), |
||||
('subscribed-project', 'default', ''), |
||||
('unsubscribed-project', 'dark gray', ''), |
||||
('reversed-unreviewed-project', 'white,standout', ''), |
||||
('reversed-subscribed-project', 'default,standout', ''), |
||||
('reversed-unsubscribed-project', 'dark gray,standout', ''), |
||||
# change list |
||||
('unreviewed-change', 'default', ''), |
||||
('reviewed-change', 'dark gray', ''), |
||||
('reversed-unreviewed-change', 'default,standout', ''), |
||||
('reversed-reviewed-change', 'dark gray,standout', ''), |
||||
] |
||||
|
||||
class StatusHeader(urwid.WidgetWrap): |
||||
def __init__(self, app): |
||||
super(StatusHeader, self).__init__(urwid.Columns([])) |
||||
self.app = app |
||||
self.title = urwid.Text(u'Start') |
||||
self.error = urwid.Text('') |
||||
self.offline = urwid.Text('') |
||||
self.sync = urwid.Text(u'Sync: 0') |
||||
self._w.contents.append((self.title, ('pack', None, False))) |
||||
self._w.contents.append((urwid.Text(u''), ('weight', 1, False))) |
||||
self._w.contents.append((self.error, ('pack', None, False))) |
||||
self._w.contents.append((self.offline, ('pack', None, False))) |
||||
self._w.contents.append((self.sync, ('pack', None, False))) |
||||
|
||||
def update(self, title=None, error=False, offline=None): |
||||
if title: |
||||
self.title.set_text(title) |
||||
if error: |
||||
self.error.set_text(('error', u'Error')) |
||||
if offline is not None: |
||||
if offline: |
||||
self.error.set_text(u'Offline') |
||||
else: |
||||
self.error.set_text(u'') |
||||
self.sync.set_text(u' Sync: %i' % self.app.sync.queue.qsize()) |
||||
|
||||
class App(object): |
||||
def __init__(self, server=None, debug=False): |
||||
self.server = server |
||||
self.config = config.Config(server) |
||||
if debug: |
||||
level = logging.DEBUG |
||||
else: |
||||
level = logging.WARNING |
||||
logging.basicConfig(filename=self.config.log_file, filemode='w', |
||||
format='%(asctime)s %(message)s', |
||||
level=level) |
||||
self.log = logging.getLogger('gertty.App') |
||||
self.log.debug("Starting") |
||||
self.db = db.Database(self) |
||||
self.sync = sync.Sync(self) |
||||
|
||||
self.screens = [] |
||||
self.status = StatusHeader(self) |
||||
self.header = urwid.AttrMap(self.status, 'header') |
||||
screen = view.project_list.ProjectListView(self) |
||||
self.status.update(title=screen.title) |
||||
self.loop = urwid.MainLoop(screen, palette=palette, |
||||
unhandled_input=self.unhandledInput) |
||||
sync_pipe = self.loop.watch_pipe(self.refresh) |
||||
#self.loop.screen.set_terminal_properties(colors=88) |
||||
self.sync_thread = threading.Thread(target=self.sync.run, args=(sync_pipe,)) |
||||
self.sync_thread.start() |
||||
self.loop.run() |
||||
|
||||
def changeScreen(self, widget): |
||||
self.status.update(title=widget.title) |
||||
self.screens.append(self.loop.widget) |
||||
self.loop.widget = widget |
||||
|
||||
def backScreen(self): |
||||
if not self.screens: |
||||
return |
||||
widget = self.screens.pop() |
||||
self.status.update(title=widget.title) |
||||
self.loop.widget = widget |
||||
self.refresh() |
||||
|
||||
def refresh(self, data=None): |
||||
widget = self.loop.widget |
||||
while isinstance(widget, urwid.Overlay): |
||||
widget = widget.contents[0][0] |
||||
widget.refresh() |
||||
|
||||
def popup(self, widget, |
||||
relative_width=50, relative_height=25, |
||||
min_width=20, min_height=8): |
||||
overlay = urwid.Overlay(widget, self.loop.widget, |
||||
'center', ('relative', relative_width), |
||||
'middle', ('relative', relative_height), |
||||
min_width=min_width, min_height=min_height) |
||||
self.screens.append(self.loop.widget) |
||||
self.loop.widget = overlay |
||||
|
||||
def help(self): |
||||
if not hasattr(self.loop.widget, 'help'): |
||||
return |
||||
dialog = mywid.MessageDialog('Help', self.loop.widget.help) |
||||
lines = self.loop.widget.help.split('\n') |
||||
urwid.connect_signal(dialog, 'close', |
||||
lambda button: self.backScreen()) |
||||
self.popup(dialog, min_width=76, min_height=len(lines)+2) |
||||
|
||||
def unhandledInput(self, key): |
||||
if key == 'esc': |
||||
self.backScreen() |
||||
elif key == 'f1': |
||||
self.help() |
||||
|
||||
def getRepo(self, project_name): |
||||
local_path = os.path.join(self.config.git_root, project_name) |
||||
local_root = os.path.abspath(self.config.git_root) |
||||
assert os.path.commonprefix((local_root, local_path)) == local_root |
||||
return gitrepo.Repo(self.config.url+'p/'+project_name, |
||||
local_path) |
||||
|
||||
if __name__ == '__main__': |
||||
parser = argparse.ArgumentParser( |
||||
description='Console client for Gerrit Code Review.') |
||||
parser.add_argument('-d', dest='debug', action='store_true', |
||||
help='enable debug logging') |
||||
parser.add_argument('server', nargs='?', |
||||
help='the server to use (as specified in config file)') |
||||
args = parser.parse_args() |
||||
g = App(args.server, args.debug) |
@ -0,0 +1,196 @@
|
||||
# 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. |
||||
|
||||
import difflib |
||||
import os |
||||
import re |
||||
|
||||
import git |
||||
|
||||
class DiffFile(object): |
||||
def __init__(self): |
||||
self.newname = None |
||||
self.oldname = None |
||||
self.oldlines = [] |
||||
self.newlines = [] |
||||
|
||||
class GitCheckoutError(Exception): |
||||
def __init__(self, msg): |
||||
super(GitCheckoutError, self).__init__(msg) |
||||
self.msg = msg |
||||
|
||||
class Repo(object): |
||||
def __init__(self, url, path): |
||||
self.url = url |
||||
self.path = path |
||||
self.differ = difflib.Differ() |
||||
if not os.path.exists(path): |
||||
git.Repo.clone_from(self.url, self.path) |
||||
|
||||
def fetch(self, url, refspec): |
||||
repo = git.Repo(self.path) |
||||
try: |
||||
repo.git.fetch(url, refspec) |
||||
except AssertionError: |
||||
repo.git.fetch(url, refspec) |
||||
|
||||
def checkout(self, ref): |
||||
repo = git.Repo(self.path) |
||||
try: |
||||
repo.git.checkout(ref) |
||||
except git.exc.GitCommandError as e: |
||||
raise GitCheckoutError(e.stderr.replace('\t', ' ')) |
||||
|
||||
def diffstat(self, old, new): |
||||
repo = git.Repo(self.path) |
||||
diff = repo.git.diff('-M', '--numstat', old, new) |
||||
ret = [] |
||||
for x in diff.split('\n'): |
||||
# Added, removed, filename |
||||
ret.append(x.split('\t')) |
||||
return ret |
||||
|
||||
def intraline_diff(self, old, new): |
||||
prevline = None |
||||
prevstyle = None |
||||
output_old = [] |
||||
output_new = [] |
||||
#socket.send('startold' + repr(old)+'\n') |
||||
#socket.send('startnew' + repr(new)+'\n') |
||||
for line in self.differ.compare(old, new): |
||||
#socket.sendall('diff output: ' + line+'\n') |
||||
key = line[0] |
||||
rest = line[2:] |
||||
if key == '?': |
||||
result = [] |
||||
accumulator = '' |
||||
emphasis = False |
||||
rest = rest[:-1] # It has a newline. |
||||
for i, c in enumerate(prevline): |
||||
if i >= len(rest): |
||||
indicator = ' ' |
||||
else: |
||||
indicator = rest[i] |
||||
#socket.sendall('%s %s %s %s %s\n' % (i, c, indicator, emphasis, accumulator)) |
||||
if indicator != ' ' and not emphasis: |
||||
# changing from not emph to emph |
||||
if accumulator: |
||||
result.append((prevstyle+'-line', accumulator)) |
||||
accumulator = '' |
||||
emphasis = True |
||||
elif indicator == ' ' and emphasis: |
||||
# changing from emph to not emph |
||||
if accumulator: |
||||
result.append((prevstyle+'-word', accumulator)) |
||||
accumulator = '' |
||||
emphasis = False |
||||
accumulator += c |
||||
if accumulator: |
||||
if emphasis: |
||||
result.append((prevstyle+'-word', accumulator)) |
||||
else: |
||||
result.append((prevstyle+'-line', accumulator)) |
||||
if prevstyle == 'added': |
||||
output_new.append(result) |
||||
elif prevstyle == 'removed': |
||||
output_old.append(result) |
||||
prevline = None |
||||
continue |
||||
if prevline is not None: |
||||
if prevstyle == 'added': |
||||
output_new.append((prevstyle+'-line', prevline)) |
||||
elif prevstyle == 'removed': |
||||
output_old.append((prevstyle+'-line', prevline)) |
||||
if key == '+': |
||||
prevstyle = 'added' |
||||
elif key == '-': |
||||
prevstyle = 'removed' |
||||
prevline = rest |
||||
#socket.sendall('prev'+repr(prevline)+'\n') |
||||
if prevline is not None: |
||||
if prevstyle == 'added': |
||||
output_new.append((prevstyle+'-line', prevline)) |
||||
elif prevstyle == 'removed': |
||||
output_old.append((prevstyle+'-line', prevline)) |
||||
#socket.sendall(repr(output_old)+'\n') |
||||
#socket.sendall(repr(output_new)+'\n') |
||||
#socket.sendall('\n') |
||||
return output_old, output_new |
||||
|
||||
header_re = re.compile('@@ -(\d+)(,\d+)? \+(\d+)(,\d+)? @@') |
||||
def diff(self, old, new, context=20): |
||||
repo = git.Repo(self.path) |
||||
#'-y', '-x', 'diff -C10', old, new, path).split('\n'): |
||||
oldc = repo.commit(old) |
||||
newc = repo.commit(new) |
||||
files = [] |
||||
for context in oldc.diff(newc, create_patch=True, U=context): |
||||
f = DiffFile() |
||||
files.append(f) |
||||
old_lineno = 0 |
||||
new_lineno = 0 |
||||
offset = 0 |
||||
oldchunk = [] |
||||
newchunk = [] |
||||
for line in context.diff.split('\n'): |
||||
if line.startswith('---'): |
||||
f.oldname = line[6:] |
||||
continue |
||||
if line.startswith('+++'): |
||||
f.newname = line[6:] |
||||
continue |
||||
if line.startswith('@@'): |
||||
#socket.sendall(line) |
||||
m = self.header_re.match(line) |
||||
#socket.sendall(str(m.groups())) |
||||
old_lineno = int(m.group(1)) |
||||
new_lineno = int(m.group(3)) |
||||
continue |
||||
if not line: |
||||
line = ' ' |
||||
key = line[0] |
||||
rest = line[1:] |
||||
if key == '-': |
||||
oldchunk.append(rest) |
||||
continue |
||||
if key == '+': |
||||
newchunk.append(rest) |
||||
continue |
||||
# end of chunk |
||||
if oldchunk or newchunk: |
||||
oldchunk, newchunk = self.intraline_diff(oldchunk, newchunk) |
||||
for l in oldchunk: |
||||
f.oldlines.append((old_lineno, '-', l)) |
||||
old_lineno += 1 |
||||
offset -= 1 |
||||
for l in newchunk: |
||||
f.newlines.append((new_lineno, '+', l)) |
||||
new_lineno += 1 |
||||
offset += 1 |
||||
oldchunk = [] |
||||
newchunk = [] |
||||
while offset > 0: |
||||
f.oldlines.append((None, '', '')) |
||||
offset -= 1 |
||||
while offset < 0: |
||||
f.newlines.append((None, '', '')) |
||||
offset += 1 |
||||
if key == ' ': |
||||
f.oldlines.append((old_lineno, ' ', rest)) |
||||
f.newlines.append((new_lineno, ' ', rest)) |
||||
old_lineno += 1 |
||||
new_lineno += 1 |
||||
continue |
||||
raise Exception("Unhandled line: %s" % line) |
||||
return files |
@ -0,0 +1,61 @@
|
||||
# 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. |
||||
|
||||
import urwid |
||||
|
||||
class TextButton(urwid.Button): |
||||
def selectable(self): |
||||
return True |
||||
|
||||
def __init__(self, text, on_press=None, user_data=None): |
||||
super(TextButton, self).__init__('', on_press=on_press, user_data=user_data) |
||||
text = urwid.Text(text) |
||||
self._w = urwid.AttrMap(text, None, focus_map='reversed') |
||||
|
||||
class FixedButton(urwid.Button): |
||||
def sizing(self): |
||||
return frozenset([urwid.FIXED]) |
||||
|
||||
def pack(self, size, focus=False): |
||||
return (len(self.get_label())+4, 1) |
||||
|
||||
class TableColumn(urwid.Pile): |
||||
def pack(self, size, focus=False): |
||||
mx = max([len(i[0].text) for i in self.contents]) |
||||
return (mx+2, len(self.contents)) |
||||
|
||||
class Table(urwid.WidgetWrap): |
||||
def __init__(self, headers=[]): |
||||
super(Table, self).__init__( |
||||
urwid.Columns([('pack', TableColumn([('pack', w)])) for w in headers])) |
||||
|
||||
def addRow(self, cells=[]): |
||||
for i, widget in enumerate(cells): |
||||
self._w.contents[i][0].contents.append((widget, ('pack', None))) |
||||
|
||||
class MessageDialog(urwid.WidgetWrap): |
||||
signals = ['close'] |
||||
def __init__(self, title, message): |
||||
ok_button = FixedButton(u'OK') |
||||
urwid.connect_signal(ok_button, 'click', |
||||
lambda button:self._emit('close')) |
||||
buttons = urwid.Columns([('pack', ok_button)], |
||||
dividechars=2) |
||||
rows = [] |
||||
rows.append(urwid.Text(message)) |
||||
rows.append(urwid.Divider()) |
||||
rows.append(buttons) |
||||
pile = urwid.Pile(rows) |
||||
fill = urwid.Filler(pile, valign='top') |
||||
super(MessageDialog, self).__init__(urwid.LineBox(fill, title)) |
@ -0,0 +1,453 @@
|
||||
# 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. |
||||
|
||||
import collections |
||||
import logging |
||||
import math |
||||
import os |
||||
import threading |
||||
import urlparse |
||||
import json |
||||
import time |
||||
import Queue |
||||
import datetime |
||||
|
||||
import dateutil.parser |
||||
import requests |
||||
|
||||
HIGH_PRIORITY=0 |
||||
NORMAL_PRIORITY=1 |
||||
LOW_PRIORITY=2 |
||||
|
||||
class MultiQueue(object): |
||||
def __init__(self, priorities): |
||||
self.queues = collections.OrderedDict() |
||||
for key in priorities: |
||||
self.queues[key] = collections.deque() |
||||
self.condition = threading.Condition() |
||||
|
||||
def qsize(self): |
||||
count = 0 |
||||
for queue in self.queues.values(): |
||||
count += len(queue) |
||||
return count |
||||
|
||||
def put(self, item, priority): |
||||
self.condition.acquire() |
||||
try: |
||||
self.queues[priority].append(item) |
||||
self.condition.notify() |
||||
finally: |
||||
self.condition.release() |
||||
|
||||
def get(self): |
||||
self.condition.acquire() |
||||
try: |
||||
while True: |
||||
for queue in self.queues.values(): |
||||
try: |
||||
ret = queue.popleft() |
||||
return ret |
||||
except IndexError: |
||||
pass |
||||
self.condition.wait() |
||||
finally: |
||||
self.condition.release() |
||||
|
||||
class Task(object): |
||||
def __init__(self, priority=NORMAL_PRIORITY): |
||||
self.log = logging.getLogger('gertty.sync') |
||||
self.priority = priority |
||||
self.succeeded = None |
||||
self.event = threading.Event() |
||||
|
||||
def complete(self, success): |
||||
self.succeeded = success |
||||
self.event.set() |
||||
|
||||
def wait(self): |
||||
self.event.wait() |
||||
|
||||
class SyncProjectListTask(Task): |
||||
def __repr__(self): |
||||
return '<SyncProjectListTask>' |
||||
|
||||
def run(self, sync): |
||||
app = sync.app |
||||
with app.db.getSession() as session: |
||||
remote = sync.get('projects/?d') |
||||
remote_keys = set(remote.keys()) |
||||
|
||||
local = {} |
||||
for p in session.getProjects(): |
||||
local[p.name] = p |
||||
local_keys = set(local.keys()) |
||||
|
||||
for name in local_keys-remote_keys: |
||||
session.delete(local[name]) |
||||
|
||||
for name in remote_keys-local_keys: |
||||
p = remote[name] |
||||
session.createProject(name, description=p.get('description', '')) |
||||
|
||||
class SyncSubscribedProjectsTask(Task): |
||||
def __repr__(self): |
||||
return '<SyncSubscribedProjectsTask>' |
||||
|
||||
def run(self, sync): |
||||
app = sync.app |
||||
with app.db.getSession() as session: |
||||
for p in session.getProjects(subscribed=True): |
||||
sync.submitTask(SyncProjectTask(p.key, self.priority)) |
||||
|
||||
class SyncProjectTask(Task): |
||||
_closed_statuses = ['MERGED', 'ABANDONED'] |
||||
|
||||
def __init__(self, project_key, priority=NORMAL_PRIORITY): |
||||
|