diff --git a/forwardport/data/security.xml b/forwardport/data/security.xml
index 3f7b8248..d25ae794 100644
--- a/forwardport/data/security.xml
+++ b/forwardport/data/security.xml
@@ -17,7 +17,33 @@
1
1
+
+ Admin access to tagging
+
+
+ 1
+ 1
+ 1
+ 1
+
+
+ Admin access to branch remover
+
+
+ 1
+ 1
+ 1
+ 1
+
+
+ No normal access to tagging
+
+ 1
+ 0
+ 0
+ 0
+
No normal access to batches
diff --git a/forwardport/models/project.py b/forwardport/models/project.py
index 46c7c0fc..43f9f667 100644
--- a/forwardport/models/project.py
+++ b/forwardport/models/project.py
@@ -886,6 +886,7 @@ class Feedback(models.Model):
class Tagging(models.Model):
_name = 'forwardport.tagging'
+ _description = "ad-hoc forwardport tagging commands"
token_field = fields.Selection([
('github_token', 'Mergebot'),
diff --git a/runbot_merge/models/pull_requests.py b/runbot_merge/models/pull_requests.py
index 4b942add..24adcd65 100644
--- a/runbot_merge/models/pull_requests.py
+++ b/runbot_merge/models/pull_requests.py
@@ -26,7 +26,7 @@ WAIT_FOR_VISIBILITY = [10, 10, 10, 10]
_logger = logging.getLogger(__name__)
class Project(models.Model):
- _name = 'runbot_merge.project'
+ _name = _description = 'runbot_merge.project'
name = fields.Char(required=True, index=True)
repo_ids = fields.One2many(
@@ -213,7 +213,7 @@ class Project(models.Model):
return bool(self.env.cr.rowcount)
class Repository(models.Model):
- _name = 'runbot_merge.repository'
+ _name = _description = 'runbot_merge.repository'
name = fields.Char(required=True)
project_id = fields.Many2one('runbot_merge.project', required=True)
@@ -285,7 +285,7 @@ class Repository(models.Model):
})
class Branch(models.Model):
- _name = 'runbot_merge.branch'
+ _name = _description = 'runbot_merge.branch'
_order = 'sequence, name'
name = fields.Char(required=True)
@@ -507,7 +507,7 @@ class Branch(models.Model):
ACL = collections.namedtuple('ACL', 'is_admin is_reviewer is_author')
class PullRequests(models.Model):
- _name = 'runbot_merge.pull_requests'
+ _name = _description = 'runbot_merge.pull_requests'
_order = 'number desc'
target = fields.Many2one('runbot_merge.branch', required=True, index=True)
@@ -544,18 +544,14 @@ class PullRequests(models.Model):
reviewed_by = fields.Many2one('res.partner')
delegates = fields.Many2many('res.partner', help="Delegate reviewers, not intrinsically reviewers but can review this PR")
- priority = fields.Selection([
- (0, 'Urgent'),
- (1, 'Pressing'),
- (2, 'Normal'),
- ], default=2, index=True)
+ priority = fields.Integer(default=2, index=True)
statuses = fields.Text(compute='_compute_statuses')
status = fields.Char(compute='_compute_statuses')
previous_failure = fields.Char(default='{}')
- batch_id = fields.Many2one('runbot_merge.batch',compute='_compute_active_batch', store=True)
- batch_ids = fields.Many2many('runbot_merge.batch')
+ batch_id = fields.Many2one('runbot_merge.batch', string="Active Batch", compute='_compute_active_batch', store=True)
+ batch_ids = fields.Many2many('runbot_merge.batch', string="Batches")
staging_id = fields.Many2one(related='batch_id.staging_id', store=True)
commits_map = fields.Char(help="JSON-encoded mapping of PR commits to actually integrated commits. The integration head (either a merge commit or the PR's topmost) is mapped from the 'empty' pr commit (the key is an empty string, because you can't put a null key in json maps).", default='{}')
@@ -574,22 +570,10 @@ class PullRequests(models.Model):
return super(PullRequests, self)._compute_display_name()
def name_get(self):
- return {
- p.id: '%s#%s' % (p.repository.name, p.number)
+ return [
+ (p.id, '%s#%d' % (p.repository.name, p.number))
for p in self
- }
-
- def __str__(self):
- if len(self) == 0:
- separator = ''
- elif len(self) == 1:
- separator = ' '
- else:
- separator = 's '
- return '' % (separator, ' '.join(
- '{0.id} ({0.display_name})'.format(p)
- for p in self
- ))
+ ]
# missing link to other PRs
@api.depends('priority', 'state', 'squash', 'merge_method', 'batch_id.active', 'label')
@@ -608,6 +592,7 @@ class PullRequests(models.Model):
for s in self:
c = Commits.search([('sha', '=', s.head)])
if not (c and c.statuses):
+ s.status = s.statuses = False
continue
statuses = json.loads(c.statuses)
@@ -965,7 +950,6 @@ class PullRequests(models.Model):
'message': message,
})
- @api.multi
def write(self, vals):
oldstate = { pr: pr._tagstate for pr in self }
@@ -988,7 +972,6 @@ class PullRequests(models.Model):
})
return w
- @api.multi
def unlink(self):
for pr in self:
self.env['runbot_merge.pull_requests.tagging'].create({
@@ -1212,7 +1195,7 @@ class PullRequests(models.Model):
WHERE id = %s AND state != 'merged'
''', [self.id])
self.env.cr.commit()
- self.invalidate_cache(fnames=['state'], ids=[self.id])
+ self.modified(['state'])
if self.env.cr.rowcount:
self.env['runbot_merge.pull_requests.tagging'].create({
'pull_request': self.number,
@@ -1259,7 +1242,7 @@ class Tagging(models.Model):
way of that. Instead, queue tagging changes into this table whose
execution can be cron-driven.
"""
- _name = 'runbot_merge.pull_requests.tagging'
+ _name = _description = 'runbot_merge.pull_requests.tagging'
repository = fields.Many2one('runbot_merge.repository', required=True)
# store the PR number (not id) as we need a Tagging for PR objects
@@ -1290,7 +1273,7 @@ class Tagging(models.Model):
class Feedback(models.Model):
""" Queue of feedback comments to send to PR users
"""
- _name = 'runbot_merge.pull_requests.feedback'
+ _name = _description = 'runbot_merge.pull_requests.feedback'
repository = fields.Many2one('runbot_merge.repository', required=True)
# store the PR number (not id) as we may want to send feedback to PR
@@ -1310,7 +1293,7 @@ class Commit(models.Model):
independent of everything else as commits can be created by
statuses only, by PR pushes, by branch updates, ...
"""
- _name = 'runbot_merge.commit'
+ _name = _description = 'runbot_merge.commit'
sha = fields.Char(required=True)
statuses = fields.Char(help="json-encoded mapping of status contexts to states", default="{}")
@@ -1366,7 +1349,7 @@ class Commit(models.Model):
return res
class Stagings(models.Model):
- _name = 'runbot_merge.stagings'
+ _name = _description = 'runbot_merge.stagings'
target = fields.Many2one('runbot_merge.branch', required=True)
@@ -1466,7 +1449,6 @@ class Stagings(models.Model):
vals['timeout_limit'] = fields.Datetime.to_string(datetime.datetime.now() + datetime.timedelta(minutes=s.target.project_id.ci_timeout))
s.write(vals)
- @api.multi
def action_cancel(self):
self.cancel("explicitly cancelled by %s", self.env.user.display_name)
return { 'type': 'ir.actions.act_window_close' }
@@ -1691,7 +1673,7 @@ class Stagings(models.Model):
return repo_name
class Split(models.Model):
- _name = 'runbot_merge.split'
+ _name = _description = 'runbot_merge.split'
target = fields.Many2one('runbot_merge.branch', required=True)
batch_ids = fields.One2many('runbot_merge.batch', 'split_id', context={'active_test': False})
@@ -1703,7 +1685,7 @@ class Batch(models.Model):
repositories e.g. change an API in repo1, this breaks use of that API
in repo2 which now needs to be updated.
"""
- _name = 'runbot_merge.batch'
+ _name = _description = 'runbot_merge.batch'
target = fields.Many2one('runbot_merge.branch', required=True)
staging_id = fields.Many2one('runbot_merge.stagings')
@@ -1796,7 +1778,7 @@ class Batch(models.Model):
})
class FetchJob(models.Model):
- _name = 'runbot_merge.fetch_job'
+ _name = _description = 'runbot_merge.fetch_job'
active = fields.Boolean(default=True)
repository = fields.Many2one('runbot_merge.repository', required=True)
diff --git a/runbot_merge/models/res_partner.py b/runbot_merge/models/res_partner.py
index e170e80c..6fddbb06 100644
--- a/runbot_merge/models/res_partner.py
+++ b/runbot_merge/models/res_partner.py
@@ -8,7 +8,7 @@ class Partner(models.Model):
reviewer = fields.Boolean(default=False, help="Can review PRs (maybe m2m to repos/branches?)")
self_reviewer = fields.Boolean(default=False, help="Can review own PRs (independent from reviewer)")
delegate_reviewer = fields.Many2many('runbot_merge.pull_requests')
- formatted_email = fields.Char(compute='_rfc5322_formatted')
+ formatted_email = fields.Char(string="commit email", compute='_rfc5322_formatted')
def _auto_init(self):
res = super(Partner, self)._auto_init()
diff --git a/runbot_merge/static/scss/runbot_merge.scss b/runbot_merge/static/scss/runbot_merge.scss
new file mode 100644
index 00000000..33d60d86
--- /dev/null
+++ b/runbot_merge/static/scss/runbot_merge.scss
@@ -0,0 +1,61 @@
+// FIX: bs4 shit-heap colors and styles
+body {
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+ color: #666666;
+}
+h1, h2, h3, h4, h5, h6{
+ color: inherit;
+ margin-top: 0.66em;
+ margin-bottom: 0.33em;
+}
+h5 { font-size: 1em; }
+.bg-success, .bg-info, .bg-warning, .bg-danger, .bg-gray-lighter {
+ color: inherit;
+}
+.dropdown-item, .dropdown-menu, .dropdown-menu a {
+ color: inherit;
+}
+.bg-success {
+ background-color: #dff0d8 !important;
+}
+.bg-info {
+ background-color: #d9edf7 !important;
+}
+.bg-warning {
+ background-color: #fcf8e3 !important;
+}
+.bg-danger {
+ background-color: #f2dede !important;
+}
+.list-inline {
+ margin-bottom: 10px;
+}
+.list-inline > li {
+ padding: 0 5px;
+ margin-right: 0;
+}
+
+// mergebot layouting
+.stagings {
+ display: flex;
+ align-items: stretch;
+}
+.stagings > li {
+ flex: 1;
+
+ padding: 0.1em;
+ padding-left: 0.5em;
+}
+.stagings > li:not(:last-child) {
+ border-right: 1px solid lightgray;
+}
+.batch:not(:last-child) {
+ border-bottom: 1px solid lightgray;
+}
+.batch a:not(:last-of-type) a:after {
+ content: ",";
+}
+.pr-listing > * { display: inline-block; }
+.pr-awaiting { opacity: 0.8; }
+.pr-blocked { opacity: 0.6; }
+.pr-failed { opacity: 0.9; }
diff --git a/runbot_merge/views/templates.xml b/runbot_merge/views/templates.xml
index e6c13f22..a3976963 100644
--- a/runbot_merge/views/templates.xml
+++ b/runbot_merge/views/templates.xml
@@ -1,34 +1,17 @@
-
-
-
-
+
+
+
+
+
+
+
-