feat: add graphviz output mode
This commit is contained in:
parent
cbf5c5195f
commit
ddc1d6b9c8
|
@ -4,6 +4,7 @@ import { VERSION_INFO } from '../version';
|
||||||
|
|
||||||
export enum Commands {
|
export enum Commands {
|
||||||
UNKNOWN = 'unknown',
|
UNKNOWN = 'unknown',
|
||||||
|
GRAPH = 'dot-graph',
|
||||||
ISSUES = 'sync-issues',
|
ISSUES = 'sync-issues',
|
||||||
LABELS = 'sync-labels',
|
LABELS = 'sync-labels',
|
||||||
}
|
}
|
||||||
|
@ -24,6 +25,11 @@ type Modeback = (mode: string) => void;
|
||||||
export function createParser(modeset: Modeback): Parser<ParsedArgs> {
|
export function createParser(modeset: Modeback): Parser<ParsedArgs> {
|
||||||
/* eslint-disable-next-line sonarjs/prefer-immediate-return */
|
/* eslint-disable-next-line sonarjs/prefer-immediate-return */
|
||||||
const parser = usage(`Usage: ${VERSION_INFO.package.name} <mode> [options]`)
|
const parser = usage(`Usage: ${VERSION_INFO.package.name} <mode> [options]`)
|
||||||
|
.command({
|
||||||
|
command: Commands.GRAPH,
|
||||||
|
describe: 'graph label state changes',
|
||||||
|
handler: () => modeset(Commands.GRAPH),
|
||||||
|
})
|
||||||
.command({
|
.command({
|
||||||
command: Commands.ISSUES,
|
command: Commands.ISSUES,
|
||||||
describe: 'sync issue labels',
|
describe: 'sync issue labels',
|
||||||
|
|
|
@ -0,0 +1,176 @@
|
||||||
|
import { BaseLabel, FlagLabel, getValueName, StateLabel } from './labels';
|
||||||
|
import { ChangeVerb } from './resolve';
|
||||||
|
|
||||||
|
export interface Node {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Edge {
|
||||||
|
source: string;
|
||||||
|
target: string;
|
||||||
|
type: ChangeVerb;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Graph {
|
||||||
|
edges: Array<Edge>;
|
||||||
|
name: string;
|
||||||
|
nodes: Array<Node>;
|
||||||
|
subs: Array<Graph>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GraphOptions {
|
||||||
|
flags: Array<FlagLabel>;
|
||||||
|
name: string;
|
||||||
|
states: Array<StateLabel>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function labelEdges(label: BaseLabel, edges: Array<Edge>) {
|
||||||
|
for (const add of label.adds) {
|
||||||
|
edges.push({
|
||||||
|
source: label.name,
|
||||||
|
target: add.name,
|
||||||
|
type: ChangeVerb.CREATED,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const remove of label.removes) {
|
||||||
|
edges.push({
|
||||||
|
source: label.name,
|
||||||
|
target: remove.name,
|
||||||
|
type: ChangeVerb.REMOVED,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const require of label.requires) {
|
||||||
|
edges.push({
|
||||||
|
source: label.name,
|
||||||
|
target: require.name,
|
||||||
|
type: ChangeVerb.REQUIRED,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function graphLabels(options: GraphOptions): Graph {
|
||||||
|
const edges: Array<Edge> = [];
|
||||||
|
const nodes: Array<Node> = [];
|
||||||
|
|
||||||
|
for (const flag of options.flags) {
|
||||||
|
nodes.push({
|
||||||
|
name: flag.name,
|
||||||
|
});
|
||||||
|
|
||||||
|
labelEdges(flag, edges);
|
||||||
|
}
|
||||||
|
|
||||||
|
const subs: Array<Graph> = [];
|
||||||
|
for (const state of options.states) {
|
||||||
|
const sub: Graph = {
|
||||||
|
edges: [],
|
||||||
|
name: state.name,
|
||||||
|
nodes: [],
|
||||||
|
subs: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const value of state.values) {
|
||||||
|
const name = getValueName(state, value);
|
||||||
|
sub.nodes.push({
|
||||||
|
name,
|
||||||
|
});
|
||||||
|
|
||||||
|
labelEdges({
|
||||||
|
...value,
|
||||||
|
name,
|
||||||
|
}, edges);
|
||||||
|
|
||||||
|
for (const become of value.becomes) {
|
||||||
|
const matchNames = become.matches.map((it) => it.name);
|
||||||
|
const becomeName = [name, 'with'].concat(matchNames).join(',');
|
||||||
|
|
||||||
|
sub.edges.push({
|
||||||
|
source: name,
|
||||||
|
target: becomeName,
|
||||||
|
type: ChangeVerb.EXISTING,
|
||||||
|
});
|
||||||
|
|
||||||
|
labelEdges({
|
||||||
|
adds: become.adds,
|
||||||
|
name: becomeName,
|
||||||
|
priority: value.priority,
|
||||||
|
removes: become.removes,
|
||||||
|
requires: become.matches,
|
||||||
|
}, sub.edges);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
subs.push(sub);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
edges,
|
||||||
|
name: options.name,
|
||||||
|
nodes,
|
||||||
|
subs,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cleanName(name: string): string {
|
||||||
|
return name.replace(/[^a-z0-9_]/g, '_');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function edgeStyle(edge: Edge) {
|
||||||
|
switch (edge.type) {
|
||||||
|
case ChangeVerb.CREATED:
|
||||||
|
return '[color="green"]';
|
||||||
|
case ChangeVerb.EXISTING:
|
||||||
|
return '[color="purple"]';
|
||||||
|
case ChangeVerb.REMOVED:
|
||||||
|
return '[color="red"]';
|
||||||
|
case ChangeVerb.CONFLICTED:
|
||||||
|
return '[color="orange"]';
|
||||||
|
case ChangeVerb.REQUIRED:
|
||||||
|
return '[color="blue"]';
|
||||||
|
default:
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function dotGraph(graph: Graph): string {
|
||||||
|
const lines = [];
|
||||||
|
const name = cleanName(graph.name);
|
||||||
|
lines.push(`digraph ${name} {`);
|
||||||
|
|
||||||
|
for (const sub of graph.subs) {
|
||||||
|
const subName = cleanName(sub.name);
|
||||||
|
lines.push(`subgraph cluster_${subName} {`);
|
||||||
|
lines.push(`label = "${subName}";`);
|
||||||
|
lines.push('color = blue');
|
||||||
|
|
||||||
|
for (const edge of sub.edges) {
|
||||||
|
const source = cleanName(edge.source);
|
||||||
|
const target = cleanName(edge.target);
|
||||||
|
lines.push(`${source} -> ${target} ${edgeStyle(edge)};`);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const node of sub.nodes) {
|
||||||
|
const nodeName = cleanName(node.name);
|
||||||
|
lines.push(`${nodeName} [style=filled];`);
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push('}');
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const edge of graph.edges) {
|
||||||
|
const source = cleanName(edge.source);
|
||||||
|
const target = cleanName(edge.target);
|
||||||
|
lines.push(`${source} -> ${target} ${edgeStyle(edge)};`);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const node of graph.nodes) {
|
||||||
|
const nodeName = cleanName(node.name);
|
||||||
|
lines.push(`${nodeName} [style=filled];`);
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push('}');
|
||||||
|
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
|
@ -11,6 +11,7 @@ import { BunyanLogger } from './logger/bunyan';
|
||||||
import { GithubRemote } from './remote/github';
|
import { GithubRemote } from './remote/github';
|
||||||
import { syncIssueLabels, SyncOptions, syncProjectLabels } from './sync';
|
import { syncIssueLabels, SyncOptions, syncProjectLabels } from './sync';
|
||||||
import { VERSION_INFO } from './version';
|
import { VERSION_INFO } from './version';
|
||||||
|
import { graphLabels, dotGraph } from './graph';
|
||||||
|
|
||||||
export { FlagLabel, StateLabel } from './labels';
|
export { FlagLabel, StateLabel } from './labels';
|
||||||
export { Remote, RemoteOptions } from './remote';
|
export { Remote, RemoteOptions } from './remote';
|
||||||
|
@ -85,6 +86,10 @@ export async function main(argv: Array<string>): Promise<number> {
|
||||||
states,
|
states,
|
||||||
};
|
};
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
|
case Commands.GRAPH:
|
||||||
|
const graph = graphLabels(project);
|
||||||
|
process.stdout.write(dotGraph(graph));
|
||||||
|
break;
|
||||||
case Commands.ISSUES:
|
case Commands.ISSUES:
|
||||||
await syncIssueLabels(options);
|
await syncIssueLabels(options);
|
||||||
break;
|
break;
|
||||||
|
|
Loading…
Reference in New Issue