Fetch and process commits

This commit is contained in:
Joscha 2023-10-23 20:04:35 +02:00
parent e1261aea96
commit 603e716638
6 changed files with 301 additions and 22 deletions

View file

@ -0,0 +1,26 @@
{
"db_name": "SQLite",
"query": "SELECT child, parent FROM commit_links JOIN commits ON hash = child WHERE reachable = 2 ORDER BY hash ASC ",
"describe": {
"columns": [
{
"name": "child",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "parent",
"ordinal": 1,
"type_info": "Text"
}
],
"parameters": {
"Right": 0
},
"nullable": [
false,
false
]
},
"hash": "f9eefc496eb423eb9a73767d3ff17784fd2f8597021bc22657b9211825916dc2"
}

View file

@ -0,0 +1,38 @@
{
"db_name": "SQLite",
"query": "SELECT hash, author, committer_date AS \"committer_date: time::OffsetDateTime\", message FROM commits WHERE reachable = 2 ORDER BY hash ASC ",
"describe": {
"columns": [
{
"name": "hash",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "author",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "committer_date: time::OffsetDateTime",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "message",
"ordinal": 3,
"type_info": "Text"
}
],
"parameters": {
"Right": 0
},
"nullable": [
false,
false,
false,
false
]
},
"hash": "fa9d5c2db5228e513a4c50f1b85b6782312deef803ed6231658c6721a26a3639"
}

130
scripts/graph/commits.ts Normal file
View file

@ -0,0 +1,130 @@
import { CommitsResponse } from "./requests";
type Commit = {
indexByHash: number;
indexByGraph: number;
hash: string;
parents: Commit[];
children: Commit[];
author: string;
committerDate: number;
summary: string;
};
export class Commits {
#graphId: number | null = null;
#commitsByGraph: Commit[] = [];
requiresUpdate(graphId: number): boolean {
return this.#graphId === null || this.#graphId < graphId;
}
update(response: CommitsResponse) {
console.assert(response.hashByHash.length == response.authorByHash.length);
console.assert(response.hashByHash.length == response.committerDateByHash.length);
console.assert(response.hashByHash.length == response.summaryByHash.length);
let commits = this.#loadCommits(response);
commits = this.#sortCommitsTopologically(commits);
this.#sortCommitsByCommitterDate(commits);
// Fill in indexes - "later" is now
for (const [idx, commit] of commits.entries()) {
commit.indexByGraph = idx;
}
this.#graphId = response.graphId;
this.#commitsByGraph = commits;
}
#loadCommits(response: CommitsResponse): Commit[] {
const commits = new Map<string, Commit>();
const commitsByHash = [];
for (const [idx, hash] of response.hashByHash.entries()) {
const commit = {
indexByHash: idx,
indexByGraph: NaN, // Filled in later
hash,
parents: [],
children: [],
author: response.authorByHash[idx]!,
committerDate: response.committerDateByHash[idx]!,
summary: response.summaryByHash[idx]!,
};
commits.set(hash, commit);
commitsByHash.push(commit);
}
// Fill in parents and children
for (const [childIdx, parentIdx] of response.childParentIndexPairs) {
const childHash = response.hashByHash[childIdx]!;
const parentHash = response.hashByHash[parentIdx]!;
const child = commits.get(childHash)!;
const parent = commits.get(parentHash)!;
child.parents.push(parent);
parent.children.push(child);
}
return commitsByHash;
}
#sortCommitsByCommitterDate(commits: Commit[]) {
commits.sort((a, b) => a.committerDate - b.committerDate);
}
/**
* Sort commits topologically such that parents come before their children.
*
* Assumes that there are no duplicated commits anywhere.
*
* 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
* ```
*
* [0]: https://en.wikipedia.org/wiki/Topological_sorting#Kahn's_algorithm
*/
#sortCommitsTopologically(commits: Commit[]): Commit[] {
// Track which unvisited parents are left for each commit
const childParentMap: Map<string, Set<string>> = new Map();
for (const commit of commits) {
childParentMap.set(commit.hash, new Set(commit.parents.map(p => p.hash)));
}
// Stack of parentless commits
const parentless = commits.filter(c => c.parents.length == 0);
const sorted: Commit[] = [];
while (parentless.length > 0) {
// Visit commit
const commit = parentless.pop()!;
sorted.push(commit);
for (const child of commit.children) {
const parents = childParentMap.get(child.hash)!;
parents.delete(commit.hash);
if (parents.size == 0) {
parentless.push(child);
}
}
}
for (const [child, parents] of childParentMap.entries()) {
console.assert(parents.size == 0, child, "still has parents");
}
console.assert(parentless.length == 0);
console.assert(commits.length == sorted.length, "topo sort changed commit amount");
return sorted;
}
}

View file

@ -14,8 +14,8 @@ export type CommitsResponse = {
hashByHash: string[]; hashByHash: string[];
authorByHash: string[]; authorByHash: string[];
committerDateByHash: number[]; committerDateByHash: number[];
messageByHash: string[]; summaryByHash: string[];
parentsByHash: number[][]; childParentIndexPairs: [number, number][];
}; };
/** /**

View file

@ -1,15 +1,17 @@
import { Commits } from "./commits.js";
import { Metrics } from "./metrics.js"; import { Metrics } from "./metrics.js";
import { getMetrics } from "./requests.js"; import { getCommits, getMetrics } from "./requests.js";
export class State { export class State {
#latestGraphId: number = -Infinity; #latestGraphId: number = -Infinity;
#latestDataId: number = -Infinity; #latestDataId: number = -Infinity;
#metrics: Metrics; #metrics: Metrics;
#commits: Commits = new Commits();
#requestingNewMetrics: boolean = false; #requestingMetrics: boolean = false;
#requestingCommits: boolean = false;
// commits (with graph id and data id)
// raw measurements (with graph id and data id) // raw measurements (with graph id and data id)
// processed measurements (with graph id and data id) // processed measurements (with graph id and data id)
@ -49,19 +51,37 @@ export class State {
if (this.#metrics.requiresUpdate(this.#latestDataId)) { if (this.#metrics.requiresUpdate(this.#latestDataId)) {
this.#requestMetrics(); this.#requestMetrics();
} }
if (this.#commits.requiresUpdate(this.#latestGraphId)) {
this.#requestCommits();
}
} }
async #requestMetrics() { async #requestMetrics() {
if (this.#requestingNewMetrics) return; if (this.#requestingMetrics) return;
console.log("Requesting new metrics"); console.log("Requesting metrics");
try { try {
this.#requestingNewMetrics = true; this.#requestingMetrics = true;
const response = await getMetrics(); const response = await getMetrics();
this.#updateDataId(response.dataId); this.#updateDataId(response.dataId);
this.#metrics.update(response); this.#metrics.update(response);
this.update(); this.update();
} finally { } finally {
this.#requestingNewMetrics = false; this.#requestingMetrics = false;
}
}
async #requestCommits() {
if (this.#requestingCommits) return;
console.log("Requesting commits");
try {
this.#requestingCommits = true;
const response = await getCommits();
this.#updateGraphId(response.graphId);
this.#commits.update(response);
this.update();
} finally {
this.#requestingCommits = false;
} }
} }
} }

View file

@ -3,16 +3,20 @@ 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::TryStreamExt;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::SqlitePool; use sqlx::{Acquire, SqlitePool};
use crate::{ use crate::{
config::ServerConfig, config::ServerConfig,
server::web::{ server::{
util,
web::{
base::{Base, Link, Tab}, base::{Base, Link, Tab},
paths::{PathGraph, PathGraphCommits, PathGraphMeasurements, PathGraphMetrics}, paths::{PathGraph, PathGraphCommits, PathGraphMeasurements, PathGraphMetrics},
r#static::{GRAPH_JS, UPLOT_CSS}, r#static::{GRAPH_JS, UPLOT_CSS},
}, },
},
somehow, somehow,
}; };
@ -65,21 +69,82 @@ struct CommitsResponse {
hash_by_hash: Vec<String>, hash_by_hash: Vec<String>,
author_by_hash: Vec<String>, author_by_hash: Vec<String>,
committer_date_by_hash: Vec<i64>, committer_date_by_hash: Vec<i64>,
message_by_hash: Vec<String>, summary_by_hash: Vec<String>,
parents_by_hash: Vec<Vec<usize>>, child_parent_index_pairs: Vec<(usize, usize)>,
} }
pub async fn get_graph_commits( pub async fn get_graph_commits(
_path: PathGraphCommits, _path: PathGraphCommits,
State(db): State<SqlitePool>, State(db): State<SqlitePool>,
) -> somehow::Result<impl IntoResponse> { ) -> somehow::Result<impl IntoResponse> {
let mut tx = db.begin().await?;
let conn = tx.acquire().await?;
let mut hash_by_hash = vec![];
let mut author_by_hash = vec![];
let mut committer_date_by_hash = vec![];
let mut summary_by_hash = vec![];
let mut child_parent_index_pairs = vec![];
// Fetch main commit info
let mut rows = sqlx::query!(
"\
SELECT \
hash, \
author, \
committer_date AS \"committer_date: time::OffsetDateTime\", \
message \
FROM commits \
WHERE reachable = 2 \
ORDER BY hash ASC \
"
)
.fetch(&mut *conn);
while let Some(row) = rows.try_next().await? {
hash_by_hash.push(row.hash);
author_by_hash.push(row.author);
committer_date_by_hash.push(row.committer_date.unix_timestamp());
summary_by_hash.push(util::format_commit_summary(&row.message));
}
drop(rows);
// Map from hash to index in "by hash" order
let index_of_hash = hash_by_hash
.iter()
.cloned()
.enumerate()
.map(|(idx, hash)| (hash, idx))
.collect::<HashMap<_, _>>();
// Fetch parent info
let mut rows = sqlx::query!(
"\
SELECT child, parent \
FROM commit_links \
JOIN commits ON hash = child \
WHERE reachable = 2 \
ORDER BY hash ASC \
"
)
.fetch(&mut *conn);
while let Some(row) = rows.try_next().await? {
// The child is tracked and must thus be in our map.
let child_index = *index_of_hash.get(&row.child).unwrap();
// The parent of a tracked commit must also be tracked.
let parent_index = *index_of_hash.get(&row.parent).unwrap();
child_parent_index_pairs.push((child_index, parent_index));
}
drop(rows);
Ok(Json(CommitsResponse { Ok(Json(CommitsResponse {
graph_id: 0, // TODO Implement graph_id: 0, // TODO Implement
hash_by_hash: vec![], // TODO Implement hash_by_hash,
author_by_hash: vec![], // TODO Implement author_by_hash,
committer_date_by_hash: vec![], // TODO Implement committer_date_by_hash,
message_by_hash: vec![], // TODO Implement summary_by_hash,
parents_by_hash: vec![], // TODO Implement child_parent_index_pairs,
})) }))
} }