Fetch and process commits
This commit is contained in:
parent
e1261aea96
commit
603e716638
6 changed files with 301 additions and 22 deletions
130
scripts/graph/commits.ts
Normal file
130
scripts/graph/commits.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -14,8 +14,8 @@ export type CommitsResponse = {
|
|||
hashByHash: string[];
|
||||
authorByHash: string[];
|
||||
committerDateByHash: number[];
|
||||
messageByHash: string[];
|
||||
parentsByHash: number[][];
|
||||
summaryByHash: string[];
|
||||
childParentIndexPairs: [number, number][];
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,15 +1,17 @@
|
|||
import { Commits } from "./commits.js";
|
||||
import { Metrics } from "./metrics.js";
|
||||
import { getMetrics } from "./requests.js";
|
||||
import { getCommits, getMetrics } from "./requests.js";
|
||||
|
||||
export class State {
|
||||
#latestGraphId: number = -Infinity;
|
||||
#latestDataId: number = -Infinity;
|
||||
|
||||
#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)
|
||||
// processed measurements (with graph id and data id)
|
||||
|
||||
|
|
@ -49,19 +51,37 @@ export class State {
|
|||
if (this.#metrics.requiresUpdate(this.#latestDataId)) {
|
||||
this.#requestMetrics();
|
||||
}
|
||||
|
||||
if (this.#commits.requiresUpdate(this.#latestGraphId)) {
|
||||
this.#requestCommits();
|
||||
}
|
||||
}
|
||||
|
||||
async #requestMetrics() {
|
||||
if (this.#requestingNewMetrics) return;
|
||||
console.log("Requesting new metrics");
|
||||
if (this.#requestingMetrics) return;
|
||||
console.log("Requesting metrics");
|
||||
try {
|
||||
this.#requestingNewMetrics = true;
|
||||
this.#requestingMetrics = true;
|
||||
const response = await getMetrics();
|
||||
this.#updateDataId(response.dataId);
|
||||
this.#metrics.update(response);
|
||||
this.update();
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue