Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 16 additions & 1 deletion src/templates.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,23 @@
use crate::database::{MergeableState::*, PullRequestModel, QueueStatus::*, TreeState};
use crate::database::{MergeableState::*, PullRequestModel, QueueStatus, TreeState};
use askama::Template;
use axum::response::{Html, IntoResponse, Response};
use http::StatusCode;

/// Build status to display on the queue page.
pub fn status_text(pr: &PullRequestModel) -> String {
if let Some(try_build) = &pr.try_build {
try_build.status.to_string()
} else {
match pr.queue_status() {
QueueStatus::Approved(_) => "approved".to_string(),
QueueStatus::ReadyForMerge(_, _) => "ready for merge".to_string(),
QueueStatus::Pending(_, _) => "pending".to_string(),
QueueStatus::Stalled(_, _) => "stalled".to_string(),
QueueStatus::NotApproved => String::new(),
}
}
}

pub struct HtmlTemplate<T>(pub T);

impl<T> IntoResponse for HtmlTemplate<T>
Expand Down
57 changes: 17 additions & 40 deletions src/utils/sort_queue.rs
Original file line number Diff line number Diff line change
@@ -1,63 +1,40 @@
use crate::bors::RollupMode;
use crate::database::{BuildStatus, MergeableState, PullRequestModel};
use crate::database::{MergeableState, PullRequestModel, QueueStatus};

/// Sorts pull requests according to merge queue priority rules.
/// Ordered by pending builds > success builds > approval > mergeability > priority value > rollup > age.
/// Ordered by: ready for merge > pending builds > approved > stalled > not approved > mergeability
/// > priority > rollup > age.
pub fn sort_queue_prs(mut prs: Vec<PullRequestModel>) -> Vec<PullRequestModel> {
prs.sort_by(|a, b| {
// 1. Pending builds come first (to block merge queue)
get_queue_blocking_priority(a)
.cmp(&get_queue_blocking_priority(b))
// 2. Compare approval status (approved PRs should come first)
.then_with(|| a.is_approved().cmp(&b.is_approved()).reverse())
// 3. Compare build status within approval groups
.then_with(|| get_status_priority(a).cmp(&get_status_priority(b)))
// 4. Compare mergeability state (0 = mergeable, 1 = conflicts/unknown)
// 1. Compare queue status (ready for merge > pending > approved > stalled > not approved)
get_queue_status_priority(&a.queue_status())
.cmp(&get_queue_status_priority(&b.queue_status()))
// 2. Compare mergeability state (0 = mergeable, 1 = conflicts/unknown)
.then_with(|| get_mergeable_priority(a).cmp(&get_mergeable_priority(b)))
// 5. Compare priority numbers (higher priority should come first)
// 3. Compare priority numbers (higher priority should come first)
.then_with(|| {
a.priority
.unwrap_or(0)
.cmp(&b.priority.unwrap_or(0))
.reverse()
})
// 6. Compare rollup mode (-1 = never/iffy, 0 = maybe, 1 = always)
// 4. Compare rollup mode (always > maybe > iffy > never)
.then_with(|| {
get_rollup_priority(a.rollup.as_ref()).cmp(&get_rollup_priority(b.rollup.as_ref()))
})
// 7. Compare PR numbers (older first)
// 5. Compare PR numbers (older first)
.then_with(|| a.number.cmp(&b.number))
});
prs
}

fn get_queue_blocking_priority(pr: &PullRequestModel) -> u32 {
match &pr.auto_build {
Some(build) => match build.status {
// Pending builds must come first to block the merge queue
BuildStatus::Pending => 0,
// All other statuses come after
_ => 1,
},
None => 1, // No build - can potentially start new build
}
}

fn get_status_priority(pr: &PullRequestModel) -> u32 {
match &pr.auto_build {
Some(build) => match build.status {
BuildStatus::Success => 0,
BuildStatus::Pending => 1,
BuildStatus::Failure => 3,
BuildStatus::Cancelled | BuildStatus::Timeouted => 2,
},
None => {
if pr.is_approved() {
1 // Approved but no build - should be prioritized
} else {
2 // No status
}
}
fn get_queue_status_priority(status: &QueueStatus) -> u32 {
match status {
QueueStatus::ReadyForMerge(_, _) => 0,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seeing this function, I wonder if we should add the mergeability status into QueueStatus, and have something like QueueStatus::Unmergeable. Not sure if that would help with anything though, and definitely not for this PR anyway.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We wouldn't be able to use that variant in the merge queue itself since it filters out un-mergeable PRs.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's not really different from NotApproved or Stalled, which should already never be returned from the SQL query, right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True. I guess there's no use for that variant (at least now).

QueueStatus::Pending(_, _) => 1,
QueueStatus::Approved(_) => 2,
QueueStatus::Stalled(_, _) => 3,
QueueStatus::NotApproved => 4,
}
}

Expand Down
215 changes: 152 additions & 63 deletions templates/queue.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,29 @@
{% block title %}Bors queue - {{ repo_name }} {% if tree_state.is_closed() %} [TREECLOSED] {% endif %}{% endblock %}

{% block head %}
<link rel="stylesheet" href="https://cdn.datatables.net/2.3.4/css/dataTables.dataTables.min.css" />
<link rel="stylesheet" href="https://cdn.datatables.net/rowgroup/1.5.1/css/rowGroup.dataTables.min.css" />
<style>
main {
max-width: 100rem;
width: 100%;
margin: 0 auto;
}

.table-wrapper {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}

table {
min-width: 100%;
white-space: nowrap;
}

table th,
table td {
padding: 0.5rem;
}
</style>
{% endblock %}

Expand All @@ -32,72 +49,144 @@ <h1>
{{ stats.failed_count }} failed, {{ stats.rolled_up_count }} rolled up
</p>

<table>
<thead>
<th>#</th>
<th>Status</th>
<th>Mergeable</th>
<th>Title</th>
<th>Author</th>
<th>Assignees</th>
<th>Approved by</th>
<th>Priority</th>
<th>Rollup</th>
</thead>

<tbody>
{% for pr in prs %}
<tr>
<td>
<a href="{{ repo_url }}/pull/{{ pr.number }}">{{ pr.number.0 }}</a>
</td>
<td>
{% if let Some(try_build) = pr.try_build %}
<a href="../results/{{ repo_name }}/{{ pr.number }}">{{ try_build.status }}</a> (try)
{% else %}
{% match pr.queue_status() %}
{% when Approved(_) %}
approved
{% when ReadyForMerge(_, _) %}
ready for merge
{% when Pending(_, _) %}
pending
{% when Stalled(_, _) %}
stalled
{% when NotApproved %}
{% endmatch %}
{% endif %}
</td>
<td>
{% match pr.mergeable_state %}
{% when Mergeable %}
yes
{% when HasConflicts %}
no
{% when Unknown %}
{% endmatch %}
</td>
<td>{{ pr.title }}</td>
<td>{{ pr.author }}</td>
<td>{{ pr.assignees|join(", ") }}</td>
<td>
{% if let Some(approver) = pr.approver() %}
{{ approver }}
{% endif %}
</td>
<td>{{ pr.priority.unwrap_or(0) }}</td>
<td>
{% if let Some(rollup) = pr.rollup %}
{{ rollup }}
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
<div style="margin-bottom: 1rem;">
<label for="groupBy">Group by: </label>
<select id="groupBy">
<option value="">None</option>
<option value="1">Status</option>
<option value="2">Mergeable</option>
<option value="4">Author</option>
<option value="7">Priority</option>
</select>
</div>

<div class="table-wrapper">
<table id="table">
<thead>
<tr>
<th>#</th>
<th>Status</th>
<th>Mergeable</th>
<th>Title</th>
<th>Author</th>
<th>Assignees</th>
<th>Approved by</th>
<th>Priority</th>
<th>Rollup</th>
</tr>
</thead>

<tbody>
{% for pr in prs %}
<tr>
<td>
<a href="{{ repo_url }}/pull/{{ pr.number }}">{{ pr.number.0 }}</a>
</td>
<td data-status="{{ crate::templates::status_text(pr) }}">
{% if let Some(try_build) = pr.try_build %}
<a href="../results/{{ repo_name }}/{{ pr.number }}">{{ crate::templates::status_text(pr) }}</a> (try)
{% else %}
{{ crate::templates::status_text(pr) }}
{% endif %}
</td>
<td>
{% match pr.mergeable_state %}
{% when Mergeable %}
yes
{% when HasConflicts %}
no
{% when Unknown %}
{% endmatch %}
</td>
<td>{{ pr.title }}</td>
<td>{{ pr.author }}</td>
<td>{{ pr.assignees|join(", ") }}</td>
<td>
{% if let Some(approver) = pr.approver() %}
{{ approver }}
{% endif %}
</td>
<td>{{ pr.priority.unwrap_or(0) }}</td>
<td>
{% if let Some(rollup) = pr.rollup %}
{{ rollup }}
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>

<div style="text-align: center; margin-top: 1em;">
<a href="https://github.com/rust-lang/bors">Contribute on GitHub</a>
</div>
</main>

<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://cdn.datatables.net/2.3.4/js/dataTables.min.js"></script>
<script src="https://cdn.datatables.net/rowgroup/1.5.1/js/dataTables.rowGroup.min.js"></script>

<script>
const getDataStatusFromCell = (cell) => cell?.dataset?.status || '';

function initializeTable(colIndex) {
let config = {
paging: false,
info: false,
columnDefs: [
{
targets: 1, // Column 1 (Status column)
render: function(data, type, row, meta) {
if (type === 'display') {
return data;
}

// Use data-status for everything else
if (meta && meta.settings && meta.row !== undefined) {
let rowNode = meta.settings.aoData[meta.row]?.nTr;
if (rowNode) {
return getDataStatusFromCell(rowNode.cells[meta.col]);
}
}

return data;
}
}
],
order: []
};

if (colIndex !== null) {
config.order = [[colIndex, "asc"]];
config.rowGroup = {
dataSrc: colIndex === 1
? ([_, html]) => {
let table = document.getElementById('table');
if (table && table.tBodies[0]) {
let rows = Array.from(table.tBodies[0].rows);
for (let row of rows) {
if (row.cells[1] && row.cells[1].innerHTML === html) {
return getDataStatusFromCell(row.cells[1]);
}
}
}
return html;
}
: colIndex
};
}

return new DataTable("#table", config);
}

let table = initializeTable(null);

// Handle group by dropdown changes
document.getElementById("groupBy").addEventListener("change", function() {
let colIndex = this.value === "" ? null : parseInt(this.value);
table.destroy();
table = initializeTable(colIndex);
});
</script>
{% endblock %}