Implement placeholder server responses
This commit is contained in:
parent
3dc54738fa
commit
f77ed130e1
10 changed files with 85 additions and 357 deletions
|
|
@ -1,20 +0,0 @@
|
||||||
{
|
|
||||||
"db_name": "SQLite",
|
|
||||||
"query": "WITH measurements AS ( SELECT hash, value, MAX(start) FROM runs JOIN run_measurements USING (id) WHERE metric = ? GROUP BY hash ) SELECT value FROM commits LEFT JOIN measurements USING (hash) WHERE reachable = 2 ORDER BY hash ASC ",
|
|
||||||
"describe": {
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"name": "value",
|
|
||||||
"ordinal": 0,
|
|
||||||
"type_info": "Float"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"parameters": {
|
|
||||||
"Right": 1
|
|
||||||
},
|
|
||||||
"nullable": [
|
|
||||||
true
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"hash": "0d2711c13a7835dd6a6708c2dc81ca15ab3a58a8553a5837351ec63265edb5c2"
|
|
||||||
}
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
{
|
|
||||||
"db_name": "SQLite",
|
|
||||||
"query": "SELECT hash, committer_date AS \"time: OffsetDateTime\" FROM commits WHERE reachable = 2 ORDER BY hash ASC ",
|
|
||||||
"describe": {
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"name": "hash",
|
|
||||||
"ordinal": 0,
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "time: OffsetDateTime",
|
|
||||||
"ordinal": 1,
|
|
||||||
"type_info": "Text"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"parameters": {
|
|
||||||
"Right": 0
|
|
||||||
},
|
|
||||||
"nullable": [
|
|
||||||
false,
|
|
||||||
false
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"hash": "0dc1d000038b42bbbcfe16a11cb8c89a86ba9bb71368a413eb90593f5da166e0"
|
|
||||||
}
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
{
|
|
||||||
"db_name": "SQLite",
|
|
||||||
"query": "SELECT parent, child FROM commit_links JOIN commits ON hash = parent WHERE reachable = 2 ORDER BY parent ASC, child ASC ",
|
|
||||||
"describe": {
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"name": "parent",
|
|
||||||
"ordinal": 0,
|
|
||||||
"type_info": "Text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "child",
|
|
||||||
"ordinal": 1,
|
|
||||||
"type_info": "Text"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"parameters": {
|
|
||||||
"Right": 0
|
|
||||||
},
|
|
||||||
"nullable": [
|
|
||||||
false,
|
|
||||||
false
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"hash": "4f3c635a7026015f95d20823a25a00870a19f9336c3566b1daeb639f81cb2154"
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +1,3 @@
|
||||||
import { Requests } from "./graph/requests.js";
|
|
||||||
import { State } from "./graph/state.js";
|
|
||||||
|
|
||||||
import uPlot from "./uPlot.js";
|
import uPlot from "./uPlot.js";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
@ -121,11 +118,6 @@ const COLORS = [
|
||||||
|
|
||||||
// Initialization
|
// Initialization
|
||||||
|
|
||||||
const plot_div = document.getElementById("plot")!;
|
const plotDiv = document.getElementById("plot")!;
|
||||||
const metricsDiv = document.getElementById("metrics")!;
|
const metricsDiv = document.getElementById("metrics")!;
|
||||||
let plot: uPlot | null = null;
|
let plot: uPlot | null = null;
|
||||||
|
|
||||||
let requests = new Requests();
|
|
||||||
let state = new State(requests, metricsDiv);
|
|
||||||
|
|
||||||
state.update();
|
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,7 @@
|
||||||
* `/graph/metrics` response data.
|
* `/graph/metrics` response data.
|
||||||
*/
|
*/
|
||||||
export type MetricsResponse = {
|
export type MetricsResponse = {
|
||||||
// data_id: number; // TODO Uncomment
|
dataId: number;
|
||||||
|
|
||||||
metrics: string[];
|
metrics: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -11,61 +10,38 @@ export type MetricsResponse = {
|
||||||
* `/graph/commits` response data.
|
* `/graph/commits` response data.
|
||||||
*/
|
*/
|
||||||
export type CommitsResponse = {
|
export type CommitsResponse = {
|
||||||
// graph_id: number; // TODO Uncomment
|
graphId: number;
|
||||||
|
hashByHash: string[];
|
||||||
hash_by_hash: string[];
|
authorByHash: string[];
|
||||||
author_by_hash: number[];
|
committerDateByHash: number[];
|
||||||
committer_date_by_hash: string[];
|
messageByHash: string[];
|
||||||
message_by_hash: string[];
|
parentsByHash: string[][];
|
||||||
parents: [string, string][];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* `/graph/measurements` response data.
|
* `/graph/measurements` response data.
|
||||||
*/
|
*/
|
||||||
export type MeasurementsResponse = {
|
export type MeasurementsResponse = {
|
||||||
// graph_id: number; // TODO Uncomment
|
graphId: number;
|
||||||
// data_id: number; // TODO Uncomment
|
dataId: number;
|
||||||
|
|
||||||
measurements: { [key: string]: (number | null)[]; };
|
measurements: { [key: string]: (number | null)[]; };
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
async function getData<R>(url: string): Promise<R> {
|
||||||
* Request different kinds of data from the server.
|
const response = await fetch(url);
|
||||||
*
|
const data: R = await response.json();
|
||||||
* This class has two main purposes:
|
return data;
|
||||||
*
|
}
|
||||||
* 1. Providing a nice interface for requesting data from the server
|
|
||||||
* 2. Preventing sending the same request again while still waiting for the server
|
export async function getMetrics(): Promise<MetricsResponse> {
|
||||||
*/
|
return getData("metrics");
|
||||||
export class Requests {
|
}
|
||||||
#requesting_metrics: Promise<MetricsResponse> | null = null;
|
|
||||||
#requesting_commits: Promise<CommitsResponse> | null = null;
|
export async function getCommits(): Promise<CommitsResponse> {
|
||||||
#requesting_measurements: Map<string, Promise<MeasurementsResponse>> = new Map();
|
return getData("commits");
|
||||||
|
}
|
||||||
async #request_data<R>(url: string): Promise<R> {
|
|
||||||
let response = await fetch(url);
|
export async function getMeasurements(metrics: string[]): Promise<MeasurementsResponse> {
|
||||||
let data: R = await response.json();
|
const params = new URLSearchParams(metrics.map(m => ["metric", m]));
|
||||||
return data;
|
return getData(`measurements?${params}`);
|
||||||
}
|
|
||||||
|
|
||||||
async get_metrics(): Promise<MetricsResponse | null> {
|
|
||||||
if (this.#requesting_metrics !== null) {
|
|
||||||
try {
|
|
||||||
return await this.#requesting_metrics;
|
|
||||||
} catch (error) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.#requesting_metrics = this.#request_data<MetricsResponse>("metrics");
|
|
||||||
try {
|
|
||||||
return await this.#requesting_metrics;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Could not get metrics:", error);
|
|
||||||
return null;
|
|
||||||
} finally {
|
|
||||||
this.#requesting_metrics = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,13 @@
|
||||||
import { updateMetricsDiv } from "./metrics.js";
|
import { updateMetricsDiv } from "./metrics.js";
|
||||||
import { Requests, MetricsResponse } from "./requests.js";
|
import { MetricsResponse, getMetrics } from "./requests.js";
|
||||||
|
|
||||||
export class State {
|
export class State {
|
||||||
#requests: Requests;
|
|
||||||
#metricsDiv: HTMLElement;
|
#metricsDiv: HTMLElement;
|
||||||
|
|
||||||
#updating: boolean = false;
|
#updating: boolean = false;
|
||||||
#metrics: MetricsResponse | null = null;
|
#metrics: MetricsResponse | null = null;
|
||||||
|
|
||||||
constructor(requests: Requests, metricsDiv: HTMLElement) {
|
constructor(metricsDiv: HTMLElement) {
|
||||||
this.#requests = requests;
|
|
||||||
this.#metricsDiv = metricsDiv;
|
this.#metricsDiv = metricsDiv;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -35,7 +33,7 @@ export class State {
|
||||||
}
|
}
|
||||||
|
|
||||||
async #update_metrics() {
|
async #update_metrics() {
|
||||||
this.#metrics = await this.#requests.get_metrics();
|
this.#metrics = await getMetrics();
|
||||||
if (this.#metrics === null) { return; }
|
if (this.#metrics === null) { return; }
|
||||||
updateMetricsDiv(this.#metricsDiv, this.#metrics.metrics);
|
updateMetricsDiv(this.#metricsDiv, this.#metrics.metrics);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ use self::{
|
||||||
},
|
},
|
||||||
pages::{
|
pages::{
|
||||||
commit::get_commit_by_hash,
|
commit::get_commit_by_hash,
|
||||||
graph::{get_graph, get_graph_data, get_graph_metrics},
|
graph::{get_graph, get_graph_commits, get_graph_measurements, get_graph_metrics},
|
||||||
index::get_index,
|
index::get_index,
|
||||||
queue::{get_queue, get_queue_delete, get_queue_inner},
|
queue::{get_queue, get_queue_delete, get_queue_inner},
|
||||||
run::get_run_by_id,
|
run::get_run_by_id,
|
||||||
|
|
@ -48,7 +48,8 @@ pub async fn run(server: Server) -> somehow::Result<()> {
|
||||||
.typed_get(get_api_worker_repo_by_hash_tree_tar_gz)
|
.typed_get(get_api_worker_repo_by_hash_tree_tar_gz)
|
||||||
.typed_get(get_commit_by_hash)
|
.typed_get(get_commit_by_hash)
|
||||||
.typed_get(get_graph)
|
.typed_get(get_graph)
|
||||||
.typed_get(get_graph_data)
|
.typed_get(get_graph_commits)
|
||||||
|
.typed_get(get_graph_measurements)
|
||||||
.typed_get(get_graph_metrics)
|
.typed_get(get_graph_metrics)
|
||||||
.typed_get(get_index)
|
.typed_get(get_index)
|
||||||
.typed_get(get_queue)
|
.typed_get(get_queue)
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,16 @@
|
||||||
mod util;
|
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
use axum::{extract::State, response::IntoResponse, Json};
|
use axum::{extract::State, response::IntoResponse, Json};
|
||||||
use axum_extra::extract::Query;
|
use axum_extra::extract::Query;
|
||||||
use futures::{StreamExt, TryStreamExt};
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::{Acquire, SqlitePool};
|
use sqlx::SqlitePool;
|
||||||
use time::OffsetDateTime;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::ServerConfig,
|
config::ServerConfig,
|
||||||
server::web::{
|
server::web::{
|
||||||
base::{Base, Link, Tab},
|
base::{Base, Link, Tab},
|
||||||
paths::{PathGraph, PathGraphData, PathGraphMetrics},
|
paths::{PathGraph, PathGraphCommits, PathGraphMeasurements, PathGraphMetrics},
|
||||||
r#static::{GRAPH_JS, UPLOT_CSS},
|
r#static::{GRAPH_JS, UPLOT_CSS},
|
||||||
},
|
},
|
||||||
somehow,
|
somehow,
|
||||||
|
|
@ -41,7 +37,9 @@ pub async fn get_graph(
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
struct MetricsResponse {
|
struct MetricsResponse {
|
||||||
|
data_id: i64,
|
||||||
metrics: Vec<String>,
|
metrics: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -54,139 +52,59 @@ pub async fn get_graph_metrics(
|
||||||
.fetch_all(&db)
|
.fetch_all(&db)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(Json(MetricsResponse { metrics }))
|
Ok(Json(MetricsResponse {
|
||||||
|
data_id: 0, // TODO Implement
|
||||||
|
metrics,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct CommitsResponse {
|
||||||
|
graph_id: i64,
|
||||||
|
hash_by_hash: Vec<String>,
|
||||||
|
author_by_hash: Vec<String>,
|
||||||
|
committer_date_by_hash: Vec<i64>,
|
||||||
|
message_by_hash: Vec<String>,
|
||||||
|
parents_by_hash: Vec<Vec<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_graph_commits(
|
||||||
|
_path: PathGraphCommits,
|
||||||
|
State(db): State<SqlitePool>,
|
||||||
|
) -> somehow::Result<impl IntoResponse> {
|
||||||
|
Ok(Json(CommitsResponse {
|
||||||
|
graph_id: 0, // TODO Implement
|
||||||
|
hash_by_hash: vec![], // TODO Implement
|
||||||
|
author_by_hash: vec![], // TODO Implement
|
||||||
|
committer_date_by_hash: vec![], // TODO Implement
|
||||||
|
message_by_hash: vec![], // TODO Implement
|
||||||
|
parents_by_hash: vec![], // TODO Implement
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct QueryGraphData {
|
pub struct QueryGraphMeasurements {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
metric: Vec<String>,
|
metric: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
struct GraphData {
|
#[serde(rename_all = "camelCase")]
|
||||||
hashes: Vec<String>,
|
struct MeasurementsResponse {
|
||||||
parents: HashMap<usize, Vec<usize>>,
|
graph_id: i64,
|
||||||
times: Vec<i64>,
|
data_id: i64,
|
||||||
|
measurements: HashMap<String, Vec<f64>>,
|
||||||
// TODO f32 for smaller transmission size?
|
|
||||||
measurements: HashMap<String, Vec<Option<f64>>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_graph_data(
|
pub async fn get_graph_measurements(
|
||||||
_path: PathGraphData,
|
_path: PathGraphMeasurements,
|
||||||
State(db): State<SqlitePool>,
|
State(db): State<SqlitePool>,
|
||||||
Query(form): Query<QueryGraphData>,
|
Query(form): Query<QueryGraphMeasurements>,
|
||||||
) -> somehow::Result<impl IntoResponse> {
|
) -> somehow::Result<impl IntoResponse> {
|
||||||
let mut tx = db.begin().await?;
|
Ok(Json(MeasurementsResponse {
|
||||||
let conn = tx.acquire().await?;
|
graph_id: 0, // TODO Implement
|
||||||
|
data_id: 0, // TODO Implement
|
||||||
// The SQL queries that return one result per commit *must* return the same
|
measurements: HashMap::new(), // TODO Implement
|
||||||
// amount of rows in the same order!
|
|
||||||
|
|
||||||
// TODO Limit by date or amount
|
|
||||||
|
|
||||||
let mut unsorted_hashes = Vec::<String>::new();
|
|
||||||
let mut times_by_hash = HashMap::<String, i64>::new();
|
|
||||||
let mut rows = sqlx::query!(
|
|
||||||
"\
|
|
||||||
SELECT \
|
|
||||||
hash, \
|
|
||||||
committer_date AS \"time: OffsetDateTime\" \
|
|
||||||
FROM commits \
|
|
||||||
WHERE reachable = 2 \
|
|
||||||
ORDER BY hash ASC \
|
|
||||||
"
|
|
||||||
)
|
|
||||||
.fetch(&mut *conn);
|
|
||||||
while let Some(row) = rows.next().await {
|
|
||||||
let row = row?;
|
|
||||||
unsorted_hashes.push(row.hash.clone());
|
|
||||||
times_by_hash.insert(row.hash, row.time.unix_timestamp());
|
|
||||||
}
|
|
||||||
drop(rows);
|
|
||||||
|
|
||||||
let parent_child_pairs = sqlx::query!(
|
|
||||||
"\
|
|
||||||
SELECT parent, child \
|
|
||||||
FROM commit_links \
|
|
||||||
JOIN commits ON hash = parent \
|
|
||||||
WHERE reachable = 2 \
|
|
||||||
ORDER BY parent ASC, child ASC \
|
|
||||||
"
|
|
||||||
)
|
|
||||||
.fetch(&mut *conn)
|
|
||||||
.map_ok(|r| (r.parent, r.child))
|
|
||||||
.try_collect::<Vec<_>>()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let mut hashes = util::sort_topologically(&unsorted_hashes, &parent_child_pairs);
|
|
||||||
hashes.sort_by_key(|hash| times_by_hash[hash]);
|
|
||||||
|
|
||||||
let sorted_hash_indices = hashes
|
|
||||||
.iter()
|
|
||||||
.cloned()
|
|
||||||
.enumerate()
|
|
||||||
.map(|(i, hash)| (hash, i))
|
|
||||||
.collect::<HashMap<_, _>>();
|
|
||||||
|
|
||||||
let mut parents = HashMap::<usize, Vec<usize>>::new();
|
|
||||||
for (parent, child) in &parent_child_pairs {
|
|
||||||
if let Some(parent_idx) = sorted_hash_indices.get(parent) {
|
|
||||||
if let Some(child_idx) = sorted_hash_indices.get(child) {
|
|
||||||
parents.entry(*parent_idx).or_default().push(*child_idx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collect times
|
|
||||||
let times = hashes
|
|
||||||
.iter()
|
|
||||||
.map(|hash| times_by_hash[hash])
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
// permutation[unsorted_index] = sorted_index
|
|
||||||
let permutation = unsorted_hashes
|
|
||||||
.iter()
|
|
||||||
.map(|hash| sorted_hash_indices[hash])
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
// Collect and permutate measurements
|
|
||||||
let mut measurements = HashMap::new();
|
|
||||||
for metric in form.metric {
|
|
||||||
let mut values = vec![None; hashes.len()];
|
|
||||||
let mut rows = sqlx::query_scalar!(
|
|
||||||
"\
|
|
||||||
WITH \
|
|
||||||
measurements AS ( \
|
|
||||||
SELECT hash, value, MAX(start) \
|
|
||||||
FROM runs \
|
|
||||||
JOIN run_measurements USING (id) \
|
|
||||||
WHERE metric = ? \
|
|
||||||
GROUP BY hash \
|
|
||||||
) \
|
|
||||||
SELECT value \
|
|
||||||
FROM commits \
|
|
||||||
LEFT JOIN measurements USING (hash) \
|
|
||||||
WHERE reachable = 2 \
|
|
||||||
ORDER BY hash ASC \
|
|
||||||
",
|
|
||||||
metric,
|
|
||||||
)
|
|
||||||
.fetch(&mut *conn)
|
|
||||||
.enumerate();
|
|
||||||
while let Some((i, value)) = rows.next().await {
|
|
||||||
values[permutation[i]] = value?;
|
|
||||||
}
|
|
||||||
drop(rows);
|
|
||||||
|
|
||||||
measurements.insert(metric, values);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Json(GraphData {
|
|
||||||
hashes,
|
|
||||||
parents,
|
|
||||||
times,
|
|
||||||
measurements,
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,89 +0,0 @@
|
||||||
use std::collections::{HashMap, HashSet};
|
|
||||||
|
|
||||||
/// Sort commits topologically such that parents come before their children.
|
|
||||||
///
|
|
||||||
/// Assumes that `parent_child_pairs` contains no duplicates and is in the
|
|
||||||
/// desired order (see below for more info on the order).
|
|
||||||
///
|
|
||||||
/// The algorithm used is a version of [Kahn's algorithm][0] that starts at the
|
|
||||||
/// nodes with no parents. It uses a stack for the set of parentless nodes,
|
|
||||||
/// meaning the resulting commit order is depth-first-y, not breadth-first-y.
|
|
||||||
/// For example, this commit graph (where children are ordered top to bottom)
|
|
||||||
/// results in the order `A, B, C, D, E, F` and not an interleaved order like
|
|
||||||
/// `A, B, D, C, E, F` (which a queue would produce):
|
|
||||||
///
|
|
||||||
/// ```text
|
|
||||||
/// A - B - C
|
|
||||||
/// \ \
|
|
||||||
/// D - E - F
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// When a node is visited and added to the list of sorted nodes, it is removed
|
|
||||||
/// as parent from all its children. Those who had no other parents are added to
|
|
||||||
/// the stack in reverse order. In the final list, the children appear in the
|
|
||||||
/// order they appeared in the parent child pairs, if possible. This means that
|
|
||||||
/// the order of the commits and of the pairs matters and should probably be
|
|
||||||
/// deterministic.
|
|
||||||
///
|
|
||||||
/// [0]: https://en.wikipedia.org/wiki/Topological_sorting#Kahn's_algorithm
|
|
||||||
pub fn sort_topologically(
|
|
||||||
commits: &[String],
|
|
||||||
parent_child_pairs: &[(String, String)],
|
|
||||||
) -> Vec<String> {
|
|
||||||
// These maps have entries for each commit hash we might want to inspect, so
|
|
||||||
// we know `.get()`, `.get_mut()` and `.remove()` must always succeed.
|
|
||||||
let mut parent_child_map = commits
|
|
||||||
.iter()
|
|
||||||
.map(|hash| (hash.clone(), Vec::<String>::new()))
|
|
||||||
.collect::<HashMap<_, _>>();
|
|
||||||
let mut child_parent_map = commits
|
|
||||||
.iter()
|
|
||||||
.map(|hash| (hash.clone(), HashSet::<String>::new()))
|
|
||||||
.collect::<HashMap<_, _>>();
|
|
||||||
for (parent, child) in parent_child_pairs {
|
|
||||||
if parent_child_map.contains_key(parent) && parent_child_map.contains_key(child) {
|
|
||||||
parent_child_map
|
|
||||||
.get_mut(parent)
|
|
||||||
.unwrap()
|
|
||||||
.push(child.clone());
|
|
||||||
child_parent_map
|
|
||||||
.get_mut(child)
|
|
||||||
.unwrap()
|
|
||||||
.insert(parent.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize parentless stack using commit list, in reverse order so that
|
|
||||||
// the order is right when popping.
|
|
||||||
let mut parentless = Vec::<String>::new();
|
|
||||||
for commit in commits.iter().rev() {
|
|
||||||
if child_parent_map[commit].is_empty() {
|
|
||||||
// A (quadratic-time) linear scan here is OK since the number of
|
|
||||||
// parentless commits is usually fairly small.
|
|
||||||
if !parentless.contains(commit) {
|
|
||||||
parentless.push(commit.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut sorted = Vec::<String>::new();
|
|
||||||
while let Some(hash) = parentless.pop() {
|
|
||||||
// Inspect children in reverse order so that the order is right when
|
|
||||||
// popping off the parentless stack.
|
|
||||||
for child in parent_child_map.remove(&hash).unwrap().into_iter().rev() {
|
|
||||||
let child_parents = child_parent_map.get_mut(&child).unwrap();
|
|
||||||
child_parents.remove(&hash);
|
|
||||||
if child_parents.is_empty() {
|
|
||||||
parentless.push(child);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sorted.push(hash);
|
|
||||||
}
|
|
||||||
|
|
||||||
assert!(parent_child_map.is_empty());
|
|
||||||
assert!(child_parent_map.values().all(|v| v.is_empty()));
|
|
||||||
assert!(parentless.is_empty());
|
|
||||||
assert_eq!(commits.len(), sorted.len());
|
|
||||||
sorted
|
|
||||||
}
|
|
||||||
|
|
@ -18,8 +18,12 @@ pub struct PathGraph {}
|
||||||
pub struct PathGraphMetrics {}
|
pub struct PathGraphMetrics {}
|
||||||
|
|
||||||
#[derive(Deserialize, TypedPath)]
|
#[derive(Deserialize, TypedPath)]
|
||||||
#[typed_path("/graph/data")]
|
#[typed_path("/graph/commits")]
|
||||||
pub struct PathGraphData {}
|
pub struct PathGraphCommits {}
|
||||||
|
|
||||||
|
#[derive(Deserialize, TypedPath)]
|
||||||
|
#[typed_path("/graph/measurements")]
|
||||||
|
pub struct PathGraphMeasurements {}
|
||||||
|
|
||||||
#[derive(Deserialize, TypedPath)]
|
#[derive(Deserialize, TypedPath)]
|
||||||
#[typed_path("/queue/")]
|
#[typed_path("/queue/")]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue