Add support for conflicts-with

This adds a database migration for a new table.

Change-Id: I9d5ea4eec89f706435430b90f563b5a0c0fef9e8
This commit is contained in:
James E. Blair 2016-02-05 21:11:21 -08:00
parent 7fc7d18db4
commit 4eef0452d5
4 changed files with 144 additions and 34 deletions

View File

@ -0,0 +1,27 @@
"""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

@ -82,6 +82,12 @@ change_table = Table(
Column('pending_status', Boolean, index=True, nullable=False),
Column('pending_status_message', Text),
)
change_conflict_table = Table(
'change_conflict', metadata,
Column('key', Integer, primary_key=True),
Column('change1_key', Integer, ForeignKey("change.key"), index=True),
Column('change2_key', Integer, ForeignKey("change.key"), index=True),
)
revision_table = Table(
'revision', metadata,
Column('key', Integer, primary_key=True),
@ -366,6 +372,30 @@ class Change(object):
owner_name = self.owner.email
return owner_name
@property
def conflicts(self):
return tuple(set(self.conflicts1 + self.conflicts2))
def addConflict(self, other):
session = Session.object_session(self)
if other in self.conflicts1 or other in self.conflicts2:
return
if self in other.conflicts1 or self in other.conflicts2:
return
self.conflicts1.append(other)
session.flush()
session.expire(other, attribute_names=['conflicts2'])
def delConflict(self, other):
session = Session.object_session(self)
if other in self.conflicts1:
self.conflicts1.remove(other)
session.flush()
session.expire(other, attribute_names=['conflicts2'])
if self in other.conflicts1:
other.conflicts1.remove(self)
session.flush()
session.expire(self, attribute_names=['conflicts2'])
class Revision(object):
def __init__(self, change, number, message, commit, parent,
@ -586,6 +616,16 @@ mapper(Topic, topic_table, properties=dict(
mapper(ProjectTopic, project_topic_table)
mapper(Change, change_table, properties=dict(
owner=relationship(Account),
conflicts1=relationship(Change,
secondary=change_conflict_table,
primaryjoin=change_table.c.key==change_conflict_table.c.change1_key,
secondaryjoin=change_table.c.key==change_conflict_table.c.change2_key,
),
conflicts2=relationship(Change,
secondary=change_conflict_table,
primaryjoin=change_table.c.key==change_conflict_table.c.change2_key,
secondaryjoin=change_table.c.key==change_conflict_table.c.change1_key,
),
revisions=relationship(Revision, backref='change',
order_by=revision_table.c.number,
cascade='all, delete-orphan'),

View File

@ -351,29 +351,7 @@ class SyncProjectTask(Task):
else:
query += ' status:open'
queries.append(query)
changes = []
sortkey = ''
done = False
offset = 0
while not done:
query = '&'.join(queries)
# We don't actually want to limit to 500, but that's the server-side default, and
# if we don't specify this, we won't get a _more_changes flag.
q = 'changes/?n=500%s&%s' % (sortkey, query)
self.log.debug('Query: %s ' % (q,))
responses = sync.get(q)
if len(queries) == 1:
responses = [responses]
done = True
for batch in responses:
changes += batch
if batch and '_more_changes' in batch[-1]:
done = False
if '_sortkey' in batch[-1]:
sortkey = '&N=%s' % (batch[-1]['_sortkey'],)
else:
offset += len(batch)
sortkey = '&start=%s' % (offset,)
changes = sync.query(queries)
change_ids = [c['id'] for c in changes]
with app.db.getSession() as session:
# Winnow the list of IDs to only the ones in the local DB.
@ -566,6 +544,8 @@ class SyncChangeTask(Task):
for remote_commit, remote_revision in remote_change.get('revisions', {}).items():
remote_comments_data = sync.get('changes/%s/revisions/%s/comments' % (self.change_id, remote_commit))
remote_revision['_gertty_remote_comments_data'] = remote_comments_data
remote_conflicts = sync.query(['q=status:open+is:mergeable+conflicts:%s' %
remote_change['_number']])
fetches = collections.defaultdict(list)
parent_commits = set()
with app.db.getSession() as session:
@ -600,6 +580,28 @@ class SyncChangeTask(Task):
change.subject = remote_change['subject']
change.updated = dateutil.parser.parse(remote_change['updated'])
change.topic = remote_change.get('topic')
unseen_conflicts = [x.id for x in change.conflicts]
for remote_conflict in remote_conflicts:
conflict_id = remote_conflict['id']
conflict = session.getChangeByID(conflict_id)
if not conflict:
self.log.info("Need to sync conflicting change %s for change %s.",
conflict_id, change.number)
sync.submitTask(SyncChangeTask(conflict_id, priority=self.priority))
else:
if conflict not in change.conflicts:
self.log.info("Added conflict %s for change %s in local DB.",
conflict.number, change.number)
change.addConflict(conflict)
self.results.append(ChangeUpdatedEvent(conflict))
if conflict_id in unseen_conflicts:
unseen_conflicts.remove(conflict_id)
for conflict_id in unseen_conflicts:
conflict = session.getChangeByID(conflict_id)
self.log.info("Deleted conflict %s for change %s in local DB.",
conflict.number, change.number)
change.delConflict(conflict)
self.results.append(ChangeUpdatedEvent(conflict))
repo = gitrepo.get_repo(change.project.name, app.config)
new_revision = False
for remote_commit, remote_revision in remote_change.get('revisions', {}).items():
@ -1290,6 +1292,8 @@ class VacuumDatabaseTask(Task):
session.vacuum()
class Sync(object):
_quiet_debug_mode = False
def __init__(self, app):
self.user_agent = 'Gertty/%s %s' % (gertty.version.version_info.release_string(),
requests.utils.default_user_agent())
@ -1308,16 +1312,17 @@ class Sync(object):
self.auth = authclass(
self.app.config.username, self.app.config.password)
self.submitTask(GetVersionTask(HIGH_PRIORITY))
self.submitTask(SyncOwnAccountTask(HIGH_PRIORITY))
self.submitTask(CheckReposTask(HIGH_PRIORITY))
self.submitTask(UploadReviewsTask(HIGH_PRIORITY))
self.submitTask(SyncProjectListTask(HIGH_PRIORITY))
self.submitTask(SyncSubscribedProjectsTask(NORMAL_PRIORITY))
self.submitTask(SyncSubscribedProjectBranchesTask(LOW_PRIORITY))
self.submitTask(PruneDatabaseTask(self.app.config.expire_age, LOW_PRIORITY))
self.periodic_thread = threading.Thread(target=self.periodicSync)
self.periodic_thread.daemon = True
self.periodic_thread.start()
if not self._quiet_debug_mode:
self.submitTask(SyncOwnAccountTask(HIGH_PRIORITY))
self.submitTask(CheckReposTask(HIGH_PRIORITY))
self.submitTask(UploadReviewsTask(HIGH_PRIORITY))
self.submitTask(SyncProjectListTask(HIGH_PRIORITY))
self.submitTask(SyncSubscribedProjectsTask(NORMAL_PRIORITY))
self.submitTask(SyncSubscribedProjectBranchesTask(LOW_PRIORITY))
self.submitTask(PruneDatabaseTask(self.app.config.expire_age, LOW_PRIORITY))
self.periodic_thread = threading.Thread(target=self.periodicSync)
self.periodic_thread.daemon = True
self.periodic_thread.start()
def periodicSync(self):
hourly = time.time()
@ -1475,3 +1480,29 @@ class Sync(object):
micro = int(parts[2])
self.version = (major, minor, micro)
self.log.info("Remote version is: %s (parsed as %s)" % (version, self.version))
def query(self, queries):
changes = []
sortkey = ''
done = False
offset = 0
while not done:
query = '&'.join(queries)
# We don't actually want to limit to 500, but that's the server-side default, and
# if we don't specify this, we won't get a _more_changes flag.
q = 'changes/?n=500%s&%s' % (sortkey, query)
self.log.debug('Query: %s' % (q,))
responses = self.get(q)
if len(queries) == 1:
responses = [responses]
done = True
for batch in responses:
changes += batch
if batch and '_more_changes' in batch[-1]:
done = False
if '_sortkey' in batch[-1]:
sortkey = '&N=%s' % (batch[-1]['_sortkey'],)
else:
offset += len(batch)
sortkey = '&start=%s' % (offset,)
return changes

View File

@ -485,7 +485,9 @@ class ChangeView(urwid.WidgetWrap):
self.depends_on_rows = {}
self.needed_by = urwid.Pile([])
self.needed_by_rows = {}
self.related_changes = urwid.Pile([self.depends_on, self.needed_by])
self.conflicts_with = urwid.Pile([])
self.conflicts_with_rows = {}
self.related_changes = urwid.Pile([self.depends_on, self.needed_by, self.conflicts_with])
self.results = mywid.HyperText(u'') # because it scrolls better than a table
self.grid = mywid.MyGridFlow([change_info, self.commit_message, votes, self.results],
cell_width=80, h_sep=2, v_sep=1, align='left')
@ -770,6 +772,16 @@ class ChangeView(urwid.WidgetWrap):
self.needed_by, self.needed_by_rows,
header='Needed by:')
# Handle conflicts_with
conflicts = {}
conflicts.update((c.key, c.subject)
for c in change.conflicts
if (c.status != 'MERGED' and
c.status != 'ABANDONED'))
self._updateDependenciesWidget(conflicts,
self.conflicts_with, self.conflicts_with_rows,
header='Conflicts with:')
def toggleReviewed(self):
with self.app.db.getSession() as session: