17e0b0aef1
We have pipelines like e.g. "check-tripleo" that would like to be matched in the Gerrit UI. Change-Id: Id0613813945590e399c223aae7bf2e69b9cfd222
566 lines
19 KiB
JavaScript
566 lines
19 KiB
JavaScript
// Copyright (c) 2014 VMware, Inc.
|
|
// Copyright (c) 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.
|
|
|
|
// this regex matches the hash part of review pages
|
|
var hashRegex = /^\#\/c\/([\d]+)((\/\d+)([.][.](\d+))?)?\/?$/;
|
|
// this regex matches CI comments
|
|
var ciRegex = /^(.* CI|Jenkins|Zuul)$/;
|
|
// this regex matches "Patch set #"
|
|
var psRegex = /^(Uploaded patch set|Patch Set) (\d+)(:|\.)/;
|
|
// this regex matches merge failure messages
|
|
var mergeFailedRegex = /Merge Failed\./;
|
|
// this regex matches the name of CI systems we trust to report merge failures
|
|
var trustedCIRegex = /^(OpenStack CI|Jenkins|Zuul)$/;
|
|
// this regex matches the name+pipeline that we want at the top of the CI list
|
|
var firstPartyCI = /^(Jenkins|Zuul)/;
|
|
// this regex matches the pipeline markup
|
|
var pipelineNameRegex = /Build \w+ \(([-\w]+) pipeline\)/;
|
|
// The url to full status information on running jobs
|
|
var zuulStatusURL = 'http://status.openstack.org/zuul';
|
|
// The json URL to check for running jobs
|
|
var zuulStatusJSON = 'https://zuul.openstack.org/status/change/';
|
|
|
|
// This is a variable to determine if we're in debugging mode, which
|
|
// lets you globally set it to see what's going on in the flow.
|
|
var hideci_debug = false;
|
|
// This is a variable to enable zuul integration, we default it off so
|
|
// that it creates no additional load, and that it's easy to turn off
|
|
// the feature.
|
|
var zuul_inline = false;
|
|
|
|
/**
|
|
dbg(...) - prints a list of items out to the javascript
|
|
console.log. This allows us to leave tracing in this file which is a
|
|
no-op by default, but can be triggered if you enter a javascript
|
|
console and set hideci_debug = true.
|
|
*/
|
|
function dbg () {
|
|
if (hideci_debug == true) {
|
|
for (var i = 0; i < arguments.length; i++) {
|
|
console.log(arguments[i]);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function format_time(ms, words) {
|
|
if (ms == null) {
|
|
return "unknown";
|
|
}
|
|
var seconds = (+ms)/1000;
|
|
var minutes = Math.floor(seconds/60);
|
|
var hours = Math.floor(minutes/60);
|
|
seconds = Math.floor(seconds % 60);
|
|
minutes = Math.floor(minutes % 60);
|
|
r = '';
|
|
if (words) {
|
|
if (hours) {
|
|
r += hours;
|
|
r += ' hr ';
|
|
}
|
|
r += minutes + ' min';
|
|
} else {
|
|
if (hours < 10) r += '0';
|
|
r += hours + ':';
|
|
if (minutes < 10) r += '0';
|
|
r += minutes + ':';
|
|
if (seconds < 10) r += '0';
|
|
r += seconds;
|
|
}
|
|
return r;
|
|
}
|
|
|
|
var ci_parse_psnum = function($panel) {
|
|
var match = psRegex.exec($panel.html());
|
|
if (match !== null) {
|
|
return parseInt(match[2]);
|
|
}
|
|
return 0;
|
|
};
|
|
|
|
var ci_parse_is_merge_conflict = function($panel) {
|
|
return (mergeFailedRegex.exec($panel.html()) !== null);
|
|
};
|
|
|
|
var ci_find_pipeline = function($panel) {
|
|
var match = pipelineNameRegex.exec($panel.html());
|
|
if (match !== null) {
|
|
return match[1];
|
|
} else {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
var ci_parse_results = function($panel) {
|
|
var result_list = [];
|
|
var test_results = $panel.find("li.comment_test");
|
|
var pipeline = null;
|
|
if (test_results !== null) {
|
|
test_results.each(function(i, li) {
|
|
var result = {};
|
|
result["name"] = $(li).find("span.comment_test_name").find("a")[0].innerHTML;
|
|
result["link"] = $(li).find("span.comment_test_name").find("a")[0];
|
|
result["result"] = $(li).find("span.comment_test_result")[0];
|
|
result_list.push(result);
|
|
});
|
|
}
|
|
return result_list;
|
|
};
|
|
|
|
/***
|
|
* function ci_group_by_ci_pipeline - create a group by structure for
|
|
* iterating on CI's pipelines
|
|
*
|
|
* This function takes the full list of comments, the current patch
|
|
* number, and builds an array of (ci_name_pipelinename, comments array)
|
|
* tuples. That makes it very easy to process during the display
|
|
* phase to ensure we only display the latest result for every CI
|
|
* pipeline.
|
|
*
|
|
* Comments that do not have a parsable pipeline (3rd party ci
|
|
* systems) get collapsed by name, and we specify 'check' for their
|
|
* pipeline.
|
|
*
|
|
**/
|
|
|
|
var ci_group_by_ci_pipeline = function(current, comments) {
|
|
var ci_pipelines = [];
|
|
var ci_pipeline_comments = [];
|
|
for (var i = 0; i < comments.length; i++) {
|
|
var comment = comments[i];
|
|
if ((comment.psnum != current) || !comment.is_ci || (comment.results.length == 0)) {
|
|
continue;
|
|
}
|
|
var name_pipeline = comment.name;
|
|
if (comment.pipeline !== null) {
|
|
name_pipeline += ' ' + comment.pipeline;
|
|
}
|
|
|
|
var index = ci_pipelines.indexOf(name_pipeline);
|
|
if (index == -1) {
|
|
// not found, so create new entries
|
|
ci_pipelines.push(name_pipeline);
|
|
ci_pipeline_comments.push([comment]);
|
|
} else {
|
|
ci_pipeline_comments[index].push(comment);
|
|
}
|
|
}
|
|
|
|
function sort_by_name(a,b) {
|
|
if (a[0] < b[0])
|
|
return -1;
|
|
else if (a[0] > b[0])
|
|
return 1;
|
|
else
|
|
return 0;
|
|
}
|
|
|
|
var results = [];
|
|
var notfirstparty = [];
|
|
// we want to separate out first party CI results to always be the
|
|
// top of the list, and third party CI to come after, so that
|
|
// hunting for first party CI isn't tough.
|
|
for (i = 0; i < ci_pipelines.length; i++) {
|
|
if (firstPartyCI.test(ci_pipelines[i])) {
|
|
results.push([ci_pipelines[i], ci_pipeline_comments[i]]);
|
|
} else {
|
|
notfirstparty.push([ci_pipelines[i], ci_pipeline_comments[i]]);
|
|
}
|
|
}
|
|
|
|
notfirstparty.sort(sort_by_name);
|
|
|
|
for (i = 0; i < notfirstparty.length; i++) {
|
|
results.push(notfirstparty[i]);
|
|
}
|
|
return results;
|
|
};
|
|
|
|
var ci_parse_comments = function() {
|
|
var comments = [];
|
|
$("p").each(function() {
|
|
var match = psRegex.exec($(this).html());
|
|
if (match !== null) {
|
|
var psnum = parseInt(match[2]);
|
|
var top = $(this).parent().parent().parent();
|
|
// old change screen
|
|
var name = top.attr("name");
|
|
if (!name) {
|
|
// new change screen
|
|
name = $(this).parent().parent().parent().children().children()[0].innerHTML;
|
|
top = $(this).parent().parent().parent().parent();
|
|
}
|
|
var comment = {};
|
|
comment.name = name;
|
|
|
|
var date_cell = top.find(".commentPanelDateCell");
|
|
if (date_cell.attr("title")) {
|
|
// old change screen
|
|
comment.date = date_cell.attr("title");
|
|
} else {
|
|
// new change screen
|
|
comment.date = $(this).parent().parent().parent().children().children()[2].innerHTML
|
|
}
|
|
var comment_panel = $(this).parent();
|
|
comment.psnum = psnum;
|
|
comment.merge_conflict = ci_parse_is_merge_conflict(comment_panel);
|
|
comment.pipeline = ci_find_pipeline(comment_panel);
|
|
comment.results = ci_parse_results(comment_panel);
|
|
comment.is_ci = (ciRegex.exec(comment.name) !== null);
|
|
comment.is_trusted_ci = (trustedCIRegex.exec(comment.name) !== null);
|
|
comment.ref = top;
|
|
dbg("Found comment", comment);
|
|
comments.push(comment);
|
|
}
|
|
});
|
|
return comments;
|
|
};
|
|
|
|
var ci_latest_patchset = function(comments) {
|
|
var psnum = 0;
|
|
for (var i = 0; i < comments.length; i++) {
|
|
psnum = Math.max(psnum, comments[i].psnum);
|
|
}
|
|
return psnum;
|
|
};
|
|
|
|
var ci_is_merge_conflict = function(comments) {
|
|
var latest = ci_latest_patchset(comments);
|
|
var conflict = false;
|
|
for (var i = 0; i < comments.length; i++) {
|
|
var comment = comments[i];
|
|
// only if we are actually talking about the latest patch set
|
|
if (comment.psnum == latest) {
|
|
if (comment.is_trusted_ci) {
|
|
conflict = comment.merge_conflict;
|
|
}
|
|
}
|
|
}
|
|
return conflict;
|
|
};
|
|
|
|
var ci_prepare_results_table = function() {
|
|
// Create a table and insert it after the approval table
|
|
var table = $("table.test_result_table")[0];
|
|
if (!table) {
|
|
table = document.createElement("table");
|
|
$(table).addClass("test_result_table");
|
|
$(table).addClass("infoTable").css({"margin-top":"1em", "margin-bottom":"1em"});
|
|
|
|
var approval_table = $("div.approvalTable");
|
|
if (approval_table.length) {
|
|
var outer_table = document.createElement("table");
|
|
$(outer_table).insertBefore(approval_table);
|
|
var outer_table_row = document.createElement("tr");
|
|
$(outer_table).append(outer_table_row);
|
|
var td = document.createElement("td");
|
|
$(outer_table_row).append(td);
|
|
$(td).css({"vertical-align":"top"});
|
|
$(td).append(approval_table);
|
|
td = document.createElement("td");
|
|
$(outer_table_row).append(td);
|
|
$(td).css({"vertical-align":"top"});
|
|
$(td).append(table);
|
|
} else {
|
|
var big_table_row = $("div.screen>div>div>table>tbody>tr");
|
|
var td = $(big_table_row).children()[1];
|
|
$(td).append(table);
|
|
}
|
|
} else {
|
|
$(table).empty();
|
|
}
|
|
return table;
|
|
};
|
|
|
|
var ci_display_results = function(comments) {
|
|
var table = ci_prepare_results_table();
|
|
if (ci_is_merge_conflict(comments)) {
|
|
var mc_header = $("<tr>").append($('<td class="merge_conflict" colpsan="2">Patch in Merge Conflict</td>'));
|
|
mc_header.css('width', '400');
|
|
mc_header.css('font-weight', 'bold');
|
|
mc_header.css('color', 'red');
|
|
mc_header.css('padding-left', '2em');
|
|
$(table).append(mc_header);
|
|
|
|
return;
|
|
}
|
|
var current = ci_latest_patchset(comments);
|
|
var ci_pipelines = ci_group_by_ci_pipeline(current, comments);
|
|
for (var i = 0; i < ci_pipelines.length; i++) {
|
|
var ci_pipeline_name = ci_pipelines[i][0];
|
|
var ci_pipeline_comments = ci_pipelines[i][1];
|
|
// the most recent comment on a pipeline
|
|
var last = ci_pipeline_comments.length - 1;
|
|
var comment = ci_pipeline_comments[last];
|
|
var rechecks = "";
|
|
if (last > 0) {
|
|
rechecks = " (" + last + " rechecks)";
|
|
}
|
|
|
|
var header = $("<tr>").append($('<td class="header">' + ci_pipeline_name + rechecks + '</td>'));
|
|
header.append('<td class="header ci_date">' + comment.date + '</td>');
|
|
$(table).append(header);
|
|
for (var j = 0; j < comment.results.length; j++) {
|
|
var result = comment.results[j];
|
|
var tr = $("<tr>");
|
|
tr.append($("<td>").append($(result["link"]).clone()));
|
|
tr.append($("<td>").append($(result["result"]).clone()));
|
|
$(table).append(tr);
|
|
}
|
|
}
|
|
};
|
|
|
|
var set_cookie = function (name, value) {
|
|
document.cookie = name + "=" + value + "; path=/";
|
|
};
|
|
|
|
var read_cookie = function (name) {
|
|
var nameEQ = name + "=";
|
|
var ca = document.cookie.split(';');
|
|
for (var i = 0; i < ca.length; i++) {
|
|
var c = ca[i];
|
|
while (c.charAt(0) == ' ') {
|
|
c = c.substring(1, c.length);
|
|
}
|
|
if (c.indexOf(nameEQ) == 0) {
|
|
return c.substring(nameEQ.length, c.length);
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
|
|
var ci_toggle_visibility = function(comments, showOrHide) {
|
|
if (!comments) {
|
|
comments = ci_parse_comments();
|
|
}
|
|
$.each(comments, function(i, comment) {
|
|
if (comment.is_ci) {
|
|
$(comment.ref).toggle(showOrHide);
|
|
}
|
|
});
|
|
};
|
|
|
|
var ci_hide_ci_comments = function(comments) {
|
|
if (!comments) {
|
|
comments = ci_parse_comments();
|
|
}
|
|
$.each(comments, function(i, comment) {
|
|
if (comment.is_ci) {
|
|
$(comment.ref).hide();
|
|
}
|
|
});
|
|
};
|
|
|
|
var ci_page_loaded = function() {
|
|
if (hashRegex.test(window.location.hash)) {
|
|
dbg("Searching for ci results on " + window.location.hash);
|
|
$("#toggleci").show();
|
|
var comments = ci_parse_comments();
|
|
ci_display_results(comments);
|
|
var showOrHide = 'true' == read_cookie('show-ci-comments');
|
|
if (!showOrHide) {
|
|
ci_hide_ci_comments(comments);
|
|
}
|
|
if (zuul_inline === true) {
|
|
ci_zuul_for_change(comments);
|
|
}
|
|
} else {
|
|
$("#toggleci").hide();
|
|
}
|
|
};
|
|
|
|
var ci_current_change = function() {
|
|
var change = hashRegex.exec(window.location.hash);
|
|
if (change.length > 1) {
|
|
return change[1];
|
|
}
|
|
return null;
|
|
};
|
|
|
|
// recursively find the zuul status change, will be much more
|
|
// efficient once zuul supports since json status.
|
|
var ci_find_zuul_status = function (data, change_psnum) {
|
|
var objects = [];
|
|
for (var i in data) {
|
|
if (!data.hasOwnProperty(i)) continue;
|
|
if (typeof data[i] == 'object') {
|
|
objects = objects.concat(ci_find_zuul_status(data[i],
|
|
change_psnum));
|
|
} else if (i == 'id' && data.id == change_psnum) {
|
|
objects.push(data);
|
|
}
|
|
}
|
|
return objects;
|
|
};
|
|
|
|
var ci_zuul_all_status = function(jobs) {
|
|
var status = "passing";
|
|
for (var i = 0; i < jobs.length; i++) {
|
|
if (jobs[i].result && jobs[i].result != "SUCCESS") {
|
|
status = "failing";
|
|
break;
|
|
}
|
|
}
|
|
return status;
|
|
};
|
|
|
|
var ci_zuul_display_status = function(status) {
|
|
var zuul_table = $("table.zuul_result_table")[0];
|
|
if (!zuul_table) {
|
|
var test_results = $("table.test_result_table")[0];
|
|
zuul_table = document.createElement("table");
|
|
$(zuul_table).addClass("zuul_result_table");
|
|
$(zuul_table).addClass("infoTable").css({"margin-bottom":"1em"});
|
|
if (test_results) {
|
|
$(test_results).prepend(zuul_table);
|
|
}
|
|
}
|
|
$(zuul_table).empty();
|
|
$(zuul_table).show();
|
|
$(zuul_table).append("<tr><td class='header'>Change currently being tested (<a href='" + zuulStatusURL + "'>full status</a>)</td></tr>");
|
|
for (var i = 0; i < status.length; i++) {
|
|
var item = status[i];
|
|
var pipeline = item.jobs[0].pipeline;
|
|
var passing = (item.failing_reasons && item.failing_reasons.length > 0) ? "failing" : "passing";
|
|
var timeleft = item.remaining_time;
|
|
var row = "<tr><td>";
|
|
if (pipeline != null) {
|
|
row += pipeline + " pipeline: " + passing;
|
|
row += " (" + format_time(timeleft, false) + " left)";
|
|
} else {
|
|
row += "in between pipelines, status should update shortly";
|
|
}
|
|
row += "</td></tr>";
|
|
|
|
$(zuul_table).append(row);
|
|
}
|
|
};
|
|
|
|
var ci_zuul_clear_status = function () {
|
|
var zuul_table = $("table.zuul_result_table")[0];
|
|
if (zuul_table) {
|
|
$(zuul_table).hide();
|
|
}
|
|
};
|
|
|
|
var ci_zuul_process_changes = function(data, change_psnum) {
|
|
var zuul_status = ci_find_zuul_status(data, change_psnum);
|
|
if (zuul_status.length) {
|
|
ci_zuul_display_status(zuul_status);
|
|
} else {
|
|
ci_zuul_clear_status();
|
|
}
|
|
};
|
|
|
|
var ci_zuul_for_change = function(comments) {
|
|
if (!comments) {
|
|
comments = ci_parse_comments();
|
|
}
|
|
var change = ci_current_change();
|
|
var psnum = ci_latest_patchset(comments);
|
|
var change_psnum = change + "," + psnum;
|
|
|
|
// do the loop recursively in ajax
|
|
(function poll() {
|
|
$.ajax({
|
|
url: zuulStatusJSON + change_psnum,
|
|
type: "GET",
|
|
success: function(data) {
|
|
dbg("Found zuul data for " + change_psnum, data);
|
|
ci_zuul_process_changes(data, change_psnum);
|
|
},
|
|
dataType: "json",
|
|
complete: setTimeout(function() {
|
|
// once we are done with this cycle in the loop we
|
|
// schedule ourselves again in the future with
|
|
// setTimeout. However, by the time the function
|
|
// actually gets called, other things might have
|
|
// happened, and we may want to just dump the data
|
|
// instead.
|
|
//
|
|
// the UI might have gone hidden (user was bored,
|
|
// switched to another tab / window).
|
|
//
|
|
// the user may have navigated to another review url,
|
|
// so the data returned is not relevant.
|
|
//
|
|
// both cases are recoverable when the user navigates
|
|
// around, because a new "thread" gets started on
|
|
// ci_page_load.
|
|
//
|
|
// BUG(sdague): there is the possibility that the user
|
|
// navigates away from a page and back fast enough
|
|
// that the first "thread" is not dead, and a second
|
|
// one is started. greghaynes totally said he'd come
|
|
// up with a way to fix that.
|
|
if (window.zuul_enable_status_updates == false) {
|
|
return;
|
|
}
|
|
var current = ci_current_change();
|
|
if (current && change_psnum.indexOf(current) != 0) {
|
|
// window url is dead, so don't schedule any more future
|
|
// updates for this url.
|
|
return;
|
|
}
|
|
poll();
|
|
}, 15000),
|
|
timeout: 5000
|
|
});
|
|
})();
|
|
};
|
|
|
|
|
|
window.onload = function() {
|
|
var input = document.createElement("input");
|
|
input.id = "toggleci";
|
|
input.type = "button";
|
|
input.className = "gwt-Button";
|
|
input.value = "Toggle CI";
|
|
input.onclick = function () {
|
|
// Flip the cookie
|
|
var showOrHide = 'true' == read_cookie('show-ci-comments');
|
|
set_cookie('show-ci-comments', showOrHide ? 'false' : 'true');
|
|
// Hide or Show existing comments based on cookie
|
|
ci_toggle_visibility(null, !showOrHide);
|
|
};
|
|
document.body.appendChild(input);
|
|
|
|
MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
|
|
var observer = new MutationObserver(function(mutations, observer) {
|
|
var span = $("span.rpcStatus");
|
|
$.each(mutations, function(i, mutation) {
|
|
if (mutation.target === span[0] &&
|
|
mutation.attributeName === "style" &&
|
|
(!(span.is(":visible:")))) {
|
|
ci_page_loaded();
|
|
}
|
|
});
|
|
});
|
|
observer.observe(document, {
|
|
subtree: true,
|
|
attributes: true
|
|
});
|
|
|
|
$(document).on({
|
|
'show.visibility': function() {
|
|
window.zuul_enable_status_updates = true;
|
|
ci_page_loaded();
|
|
},
|
|
'hide.visibility': function() {
|
|
window.zuul_enable_status_updates = false;
|
|
}
|
|
});
|
|
};
|